mcp-voice-hooks 1.0.1 → 1.0.2
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.local.md +25 -0
- package/README.md +46 -4
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/unified-server.js +3 -3
- package/dist/unified-server.js.map +1 -1
- package/package.json +1 -1
- package/public/app.js +1 -1
- package/test-npx-clean/mcp-voice-hooks-1.0.1.tgz +0 -0
- package/test-npx-clean/package/dist/chunk-IYGM5COW.js +12 -0
- package/test-npx-clean/package/dist/chunk-IYGM5COW.js.map +1 -0
- package/test-npx-clean/package/dist/index.d.ts +2 -0
- package/test-npx-clean/package/dist/index.js +125 -0
- package/test-npx-clean/package/dist/index.js.map +1 -0
- package/test-npx-clean/package/dist/unified-server.d.ts +1 -0
- package/test-npx-clean/package/dist/unified-server.js +352 -0
- package/test-npx-clean/package/dist/unified-server.js.map +1 -0
- package/.claude/hooks/hook-check.sh +0 -15
- /package/{mcp-voice-hooks-1.0.0.tgz → test-npx-clean/package/mcp-voice-hooks-1.0.0.tgz} +0 -0
- /package/{mcp-voice-hooks-1.0.1.tgz → test-npx-clean/package/mcp-voice-hooks-1.0.1.tgz} +0 -0
package/CLAUDE.local.md
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# To publish to npm
|
2
|
+
|
3
|
+
```bash
|
4
|
+
# 1. Build the project first
|
5
|
+
npm run build
|
6
|
+
|
7
|
+
# 2. Update roadmap.md with version info and stage it
|
8
|
+
git add roadmap.md
|
9
|
+
|
10
|
+
# 3. Bump version (patch, minor, or major) - creates a commit and tag
|
11
|
+
npm version patch --registry https://registry.npmjs.org/
|
12
|
+
|
13
|
+
# Alternative: Bump version without creating a commit/tag
|
14
|
+
# npm version patch --no-git-tag-version
|
15
|
+
# Then manually commit the changes
|
16
|
+
|
17
|
+
# 4. Publish to npm (this creates the .tgz file automatically)
|
18
|
+
npm publish --registry https://registry.npmjs.org/
|
19
|
+
|
20
|
+
# Note: It can take 1-5 minutes for the package to be available globally
|
21
|
+
# Check availability at: https://www.npmjs.com/package/mcp-voice-hooks
|
22
|
+
|
23
|
+
# Optional: Create .tgz file without publishing
|
24
|
+
npm pack
|
25
|
+
```
|
package/README.md
CHANGED
@@ -33,7 +33,15 @@ mcp-voice-hooks enables continuous voice conversations with AI assistants by:
|
|
33
33
|
- Install hook scripts to `~/.mcp-voice-hooks/hooks/`
|
34
34
|
- Configure your project's `.claude/settings.json`
|
35
35
|
|
36
|
-
2. **Add the MCP server
|
36
|
+
2. **Add the MCP server**:
|
37
|
+
|
38
|
+
Run the following command to automatically add the MCP server to your current project in `~/.claude.json`:
|
39
|
+
|
40
|
+
```bash
|
41
|
+
claude mcp add voice-hooks npx mcp-voice-hooks
|
42
|
+
```
|
43
|
+
|
44
|
+
or manually add the following to your project's `.mcp.json`:
|
37
45
|
|
38
46
|
```json
|
39
47
|
{
|
@@ -54,7 +62,17 @@ mcp-voice-hooks enables continuous voice conversations with AI assistants by:
|
|
54
62
|
claude
|
55
63
|
```
|
56
64
|
|
57
|
-
4. **Open the voice interface** at <http://localhost:
|
65
|
+
4. **Open the voice interface** at <http://localhost:5111> and start speaking! Note: you need to send one text message to Claude to trigger the voice hooks.
|
66
|
+
|
67
|
+
The default port is 5111. To use a different port, add to your project's `.claude/settings.json`:
|
68
|
+
|
69
|
+
```json
|
70
|
+
{
|
71
|
+
"env": {
|
72
|
+
"MCP_VOICE_HOOKS_PORT": "8080"
|
73
|
+
}
|
74
|
+
}
|
75
|
+
```
|
58
76
|
|
59
77
|
## Development Mode
|
60
78
|
|
@@ -78,7 +96,11 @@ npx mcp-voice-hooks install-hooks
|
|
78
96
|
claude
|
79
97
|
```
|
80
98
|
|
81
|
-
|
99
|
+
**Important**: When developing with `npm link`:
|
100
|
+
|
101
|
+
- Claude runs the compiled JavaScript from the `dist` folder, not your TypeScript source
|
102
|
+
- After making changes to the TypeScript code, you must run `npm run build`
|
103
|
+
- Then restart Claude Code to use the updated compiled code
|
82
104
|
|
83
105
|
### Hot Reload
|
84
106
|
|
@@ -103,7 +125,7 @@ and then configure claude to use the mcp proxy like so:
|
|
103
125
|
}
|
104
126
|
```
|
105
127
|
|
106
|
-
##
|
128
|
+
## Voice responses (Mac only)
|
107
129
|
|
108
130
|
Add the post tool hook to your claude settings:
|
109
131
|
|
@@ -145,3 +167,23 @@ Add to your Claude Code settings JSON:
|
|
145
167
|
```
|
146
168
|
|
147
169
|
To disable voice responses, set the value to `false` or remove the setting entirely.
|
170
|
+
|
171
|
+
### High quality voice responses
|
172
|
+
|
173
|
+
These voice responses are spoken by your Mac's system voice.
|
174
|
+
|
175
|
+
Configure in `System Settings > Accessibility > Spoken Content > System Voice`
|
176
|
+
|
177
|
+
I recommend using a Siri voice, as they are much higher quality.
|
178
|
+
|
179
|
+
Click the info icon next to the system voice dropdown. Search for "Siri" to find the highest quality voices. You'll have to trigger a download of the voice.
|
180
|
+
|
181
|
+
It may take a while to download.
|
182
|
+
|
183
|
+
Once it's downloaded, you can select it in the system voice dropdown.
|
184
|
+
|
185
|
+
Test it with the bash command:
|
186
|
+
|
187
|
+
```bash
|
188
|
+
say "Hi, this is your mac system voice"
|
189
|
+
```
|
package/dist/index.js
CHANGED
@@ -45,7 +45,7 @@ var HttpServer = class {
|
|
45
45
|
app;
|
46
46
|
utteranceQueue;
|
47
47
|
port;
|
48
|
-
constructor(utteranceQueue, port =
|
48
|
+
constructor(utteranceQueue, port = 5111) {
|
49
49
|
this.utteranceQueue = utteranceQueue;
|
50
50
|
this.port = port;
|
51
51
|
this.app = express();
|
@@ -115,10 +115,11 @@ var HttpServer = class {
|
|
115
115
|
// src/index.ts
|
116
116
|
async function main() {
|
117
117
|
const utteranceQueue = new InMemoryUtteranceQueue();
|
118
|
-
const
|
118
|
+
const port = process.env.MCP_VOICE_HOOKS_PORT ? parseInt(process.env.MCP_VOICE_HOOKS_PORT) : 5111;
|
119
|
+
const httpServer = new HttpServer(utteranceQueue, port);
|
119
120
|
await httpServer.start();
|
120
121
|
console.log("Voice Hooks servers ready!");
|
121
|
-
console.log(
|
122
|
+
console.log(`- HTTP server: http://localhost:${port}`);
|
122
123
|
console.log("- MCP server: Ready for stdio connection");
|
123
124
|
}
|
124
125
|
main().catch(console.error);
|
package/dist/index.js.map
CHANGED
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"sources":["../src/utterance-queue.ts","../src/http-server.ts","../src/index.ts"],"sourcesContent":["import { Utterance, UtteranceQueue } from './types.js';\nimport { randomUUID } from 'crypto';\nimport { debugLog } from './debug.js';\n\nexport class InMemoryUtteranceQueue implements UtteranceQueue {\n public utterances: Utterance[] = [];\n\n add(text: string, timestamp?: Date): Utterance {\n const utterance: Utterance = {\n id: randomUUID(),\n text: text.trim(),\n timestamp: timestamp || new Date(),\n status: 'pending'\n };\n \n this.utterances.push(utterance);\n debugLog(`[Queue] queued:\t\"${utterance.text}\"\t[id: ${utterance.id}]`);\n return utterance;\n }\n\n getRecent(limit: number = 10): Utterance[] {\n return this.utterances\n .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())\n .slice(0, limit);\n }\n\n markDelivered(id: string): void {\n const utterance = this.utterances.find(u => u.id === id);\n if (utterance) {\n utterance.status = 'delivered';\n debugLog(`[Queue] delivered:\t\"${utterance.text}\"\t[id: ${id}]`);\n }\n }\n\n clear(): void {\n const count = this.utterances.length;\n this.utterances = [];\n debugLog(`[Queue] Cleared ${count} utterances`);\n }\n}","import express from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { InMemoryUtteranceQueue } from './utterance-queue.js';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\nexport class HttpServer {\n private app: express.Application;\n private utteranceQueue: InMemoryUtteranceQueue;\n private port: number;\n\n constructor(utteranceQueue: InMemoryUtteranceQueue, port: number =
|
1
|
+
{"version":3,"sources":["../src/utterance-queue.ts","../src/http-server.ts","../src/index.ts"],"sourcesContent":["import { Utterance, UtteranceQueue } from './types.js';\nimport { randomUUID } from 'crypto';\nimport { debugLog } from './debug.js';\n\nexport class InMemoryUtteranceQueue implements UtteranceQueue {\n public utterances: Utterance[] = [];\n\n add(text: string, timestamp?: Date): Utterance {\n const utterance: Utterance = {\n id: randomUUID(),\n text: text.trim(),\n timestamp: timestamp || new Date(),\n status: 'pending'\n };\n \n this.utterances.push(utterance);\n debugLog(`[Queue] queued:\t\"${utterance.text}\"\t[id: ${utterance.id}]`);\n return utterance;\n }\n\n getRecent(limit: number = 10): Utterance[] {\n return this.utterances\n .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())\n .slice(0, limit);\n }\n\n markDelivered(id: string): void {\n const utterance = this.utterances.find(u => u.id === id);\n if (utterance) {\n utterance.status = 'delivered';\n debugLog(`[Queue] delivered:\t\"${utterance.text}\"\t[id: ${id}]`);\n }\n }\n\n clear(): void {\n const count = this.utterances.length;\n this.utterances = [];\n debugLog(`[Queue] Cleared ${count} utterances`);\n }\n}","import express from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { InMemoryUtteranceQueue } from './utterance-queue.js';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\nexport class HttpServer {\n private app: express.Application;\n private utteranceQueue: InMemoryUtteranceQueue;\n private port: number;\n\n constructor(utteranceQueue: InMemoryUtteranceQueue, port: number = 5111) {\n this.utteranceQueue = utteranceQueue;\n this.port = port;\n this.app = express();\n this.setupMiddleware();\n this.setupRoutes();\n }\n\n private setupMiddleware() {\n this.app.use(cors());\n this.app.use(express.json());\n this.app.use(express.static(path.join(__dirname, '..', 'public')));\n }\n\n private setupRoutes() {\n // API Routes\n this.app.post('/api/potential-utterances', (req: express.Request, res: express.Response) => {\n const { text, timestamp } = req.body;\n \n if (!text || !text.trim()) {\n res.status(400).json({ error: 'Text is required' });\n return;\n }\n\n const parsedTimestamp = timestamp ? new Date(timestamp) : undefined;\n const utterance = this.utteranceQueue.add(text, parsedTimestamp);\n res.json({\n success: true,\n utterance: {\n id: utterance.id,\n text: utterance.text,\n timestamp: utterance.timestamp,\n status: utterance.status,\n },\n });\n });\n\n this.app.get('/api/utterances', (req: express.Request, res: express.Response) => {\n const limit = parseInt(req.query.limit as string) || 10;\n const utterances = this.utteranceQueue.getRecent(limit);\n \n res.json({\n utterances: utterances.map(u => ({\n id: u.id,\n text: u.text,\n timestamp: u.timestamp,\n status: u.status,\n })),\n });\n });\n\n this.app.get('/api/utterances/status', (req: express.Request, res: express.Response) => {\n const total = this.utteranceQueue.utterances.length;\n const pending = this.utteranceQueue.utterances.filter(u => u.status === 'pending').length;\n const delivered = this.utteranceQueue.utterances.filter(u => u.status === 'delivered').length;\n\n res.json({\n total,\n pending,\n delivered,\n });\n });\n\n // Serve the browser client\n this.app.get('/', (req: express.Request, res: express.Response) => {\n res.sendFile(path.join(__dirname, '..', 'public', 'index.html'));\n });\n }\n\n start(): Promise<void> {\n return new Promise((resolve) => {\n this.app.listen(this.port, () => {\n console.log(`HTTP Server running on http://localhost:${this.port}`);\n resolve();\n });\n });\n }\n}","import { InMemoryUtteranceQueue } from './utterance-queue.js';\nimport { HttpServer } from './http-server.js';\n\nasync function main() {\n // Shared utterance queue between HTTP and MCP servers\n const utteranceQueue = new InMemoryUtteranceQueue();\n \n // Start HTTP server for browser client\n const port = process.env.MCP_VOICE_HOOKS_PORT ? parseInt(process.env.MCP_VOICE_HOOKS_PORT) : 5111;\n const httpServer = new HttpServer(utteranceQueue, port);\n await httpServer.start();\n \n // Note: MCP server runs separately via `npm run mcp` command\n \n console.log('Voice Hooks servers ready!');\n console.log(`- HTTP server: http://localhost:${port}`);\n console.log('- MCP server: Ready for stdio connection');\n}\n\nmain().catch(console.error);"],"mappings":"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","names":[]}
|
package/dist/unified-server.js
CHANGED
@@ -208,7 +208,7 @@ app.delete("/api/utterances", (req, res) => {
|
|
208
208
|
app.get("/", (req, res) => {
|
209
209
|
res.sendFile(path.join(__dirname, "..", "public", "index.html"));
|
210
210
|
});
|
211
|
-
var HTTP_PORT =
|
211
|
+
var HTTP_PORT = process.env.MCP_VOICE_HOOKS_PORT ? parseInt(process.env.MCP_VOICE_HOOKS_PORT) : 5111;
|
212
212
|
app.listen(HTTP_PORT, () => {
|
213
213
|
console.log(`[HTTP] Server listening on http://localhost:${HTTP_PORT}`);
|
214
214
|
console.log(`[Mode] Running in ${IS_MCP_MANAGED ? "MCP-managed" : "standalone"} mode`);
|
@@ -267,7 +267,7 @@ if (IS_MCP_MANAGED) {
|
|
267
267
|
try {
|
268
268
|
if (name === "dequeue_utterances") {
|
269
269
|
const limit = args?.limit ?? 10;
|
270
|
-
const response = await fetch(
|
270
|
+
const response = await fetch(`http://localhost:${HTTP_PORT}/api/dequeue-utterances`, {
|
271
271
|
method: "POST",
|
272
272
|
headers: { "Content-Type": "application/json" },
|
273
273
|
body: JSON.stringify({ limit })
|
@@ -301,7 +301,7 @@ ${data.utterances.reverse().map((u) => `"${u.text}" [time: ${new Date(u.timestam
|
|
301
301
|
Math.min(MAX_WAIT_TIMEOUT_SECONDS, requestedSeconds)
|
302
302
|
);
|
303
303
|
debugLog(`[MCP] Calling wait_for_utterance with ${secondsToWait}s timeout`);
|
304
|
-
const response = await fetch(
|
304
|
+
const response = await fetch(`http://localhost:${HTTP_PORT}/api/wait-for-utterances`, {
|
305
305
|
method: "POST",
|
306
306
|
headers: { "Content-Type": "application/json" },
|
307
307
|
body: JSON.stringify({ seconds_to_wait: secondsToWait })
|
@@ -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 = 3000;\napp.listen(HTTP_PORT, () => {\n console.log(`[HTTP] Server listening on http://localhost:${HTTP_PORT}`);\n console.log(`[Mode] Running in ${IS_MCP_MANAGED ? 'MCP-managed' : 'standalone'} mode`);\n});\n\n// MCP Server Setup (only if MCP-managed)\nif (IS_MCP_MANAGED) {\n console.log('[MCP] Initializing MCP server...');\n\n const mcpServer = new Server(\n {\n name: 'voice-hooks',\n version: '1.0.0',\n },\n {\n capabilities: {\n tools: {},\n },\n }\n );\n\n // Tool handlers\n mcpServer.setRequestHandler(ListToolsRequestSchema, async () => {\n return {\n tools: [\n {\n name: 'dequeue_utterances',\n description: 'Dequeue pending utterances and mark them as delivered',\n inputSchema: {\n type: 'object',\n properties: {\n limit: {\n type: 'number',\n description: 'Maximum number of utterances to dequeue (default: 10)',\n default: 10,\n },\n },\n },\n },\n {\n name: 'wait_for_utterance',\n description: 'Wait for an utterance to be available or until timeout. Returns immediately if no utterances since last timeout.',\n inputSchema: {\n type: 'object',\n properties: {\n seconds_to_wait: {\n type: 'number',\n description: `Maximum seconds to wait for an utterance (default: ${DEFAULT_WAIT_TIMEOUT_SECONDS}, min: ${MIN_WAIT_TIMEOUT_SECONDS}, max: ${MAX_WAIT_TIMEOUT_SECONDS})`,\n default: DEFAULT_WAIT_TIMEOUT_SECONDS,\n minimum: MIN_WAIT_TIMEOUT_SECONDS,\n maximum: MAX_WAIT_TIMEOUT_SECONDS,\n },\n },\n },\n },\n ],\n };\n });\n\n mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {\n const { name, arguments: args } = request.params;\n\n try {\n if (name === 'dequeue_utterances') {\n const limit = (args?.limit as number) ?? 10;\n const response = await fetch('http://localhost:3000/api/dequeue-utterances', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ limit }),\n });\n\n const data = await response.json() as any;\n\n if (data.utterances.length === 0) {\n return {\n content: [\n {\n type: 'text',\n text: 'No recent utterances found.',\n },\n ],\n };\n }\n\n return {\n content: [\n {\n type: 'text',\n text: `Dequeued ${data.utterances.length} utterance(s):\\n\\n${data.utterances.reverse().map((u: any) => `\"${u.text}\"\\t[time: ${new Date(u.timestamp).toISOString()}]`).join('\\n')\n }`,\n },\n ],\n };\n }\n\n if (name === 'wait_for_utterance') {\n const requestedSeconds = (args?.seconds_to_wait as number) ?? DEFAULT_WAIT_TIMEOUT_SECONDS;\n const secondsToWait = Math.max(\n MIN_WAIT_TIMEOUT_SECONDS,\n Math.min(MAX_WAIT_TIMEOUT_SECONDS, requestedSeconds)\n );\n debugLog(`[MCP] Calling wait_for_utterance with ${secondsToWait}s timeout`);\n\n const response = await fetch('http://localhost:3000/api/wait-for-utterances', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ seconds_to_wait: secondsToWait }),\n });\n\n const data = await response.json() as any;\n\n if (data.utterances && data.utterances.length > 0) {\n const utteranceTexts = data.utterances\n .map((u: any) => `[${u.timestamp}] \"${u.text}\"`)\n .join('\\n');\n\n return {\n content: [\n {\n type: 'text',\n text: `Found ${data.count} utterance(s):\\n\\n${utteranceTexts}`,\n },\n ],\n };\n } else {\n return {\n content: [\n {\n type: 'text',\n text: data.message || `No utterances found after waiting ${secondsToWait} seconds.`,\n },\n ],\n };\n }\n }\n\n throw new Error(`Unknown tool: ${name}`);\n } catch (error) {\n return {\n content: [\n {\n type: 'text',\n text: `Error: ${error instanceof Error ? error.message : String(error)}`,\n },\n ],\n isError: true,\n };\n }\n });\n\n // Connect via stdio\n const transport = new StdioServerTransport();\n mcpServer.connect(transport);\n console.log('[MCP] Server connected via stdio');\n} else {\n console.log('[MCP] Skipping MCP server initialization (not in MCP-managed mode)');\n}"],"mappings":"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","names":[]}
|
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":[]}
|
package/package.json
CHANGED
package/public/app.js
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
class VoiceHooksClient {
|
2
2
|
constructor() {
|
3
|
-
this.baseUrl =
|
3
|
+
this.baseUrl = window.location.origin;
|
4
4
|
this.debug = localStorage.getItem('voiceHooksDebug') === 'true';
|
5
5
|
this.utteranceInput = document.getElementById('utteranceInput');
|
6
6
|
this.sendBtn = document.getElementById('sendBtn');
|
Binary file
|
@@ -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,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
|
@@ -0,0 +1,352 @@
|
|
1
|
+
#!/usr/bin/env node
|
2
|
+
import {
|
3
|
+
debugLog
|
4
|
+
} from "./chunk-IYGM5COW.js";
|
5
|
+
|
6
|
+
// src/unified-server.ts
|
7
|
+
import express from "express";
|
8
|
+
import cors from "cors";
|
9
|
+
import path from "path";
|
10
|
+
import { fileURLToPath } from "url";
|
11
|
+
import { randomUUID } from "crypto";
|
12
|
+
import { exec } from "child_process";
|
13
|
+
import { promisify } from "util";
|
14
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
15
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
16
|
+
import {
|
17
|
+
CallToolRequestSchema,
|
18
|
+
ListToolsRequestSchema
|
19
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
20
|
+
var __filename = fileURLToPath(import.meta.url);
|
21
|
+
var __dirname = path.dirname(__filename);
|
22
|
+
var DEFAULT_WAIT_TIMEOUT_SECONDS = 30;
|
23
|
+
var MIN_WAIT_TIMEOUT_SECONDS = 30;
|
24
|
+
var MAX_WAIT_TIMEOUT_SECONDS = 60;
|
25
|
+
var execAsync = promisify(exec);
|
26
|
+
async function playNotificationSound() {
|
27
|
+
try {
|
28
|
+
await execAsync("afplay /System/Library/Sounds/Funk.aiff");
|
29
|
+
debugLog("[Sound] Played notification sound");
|
30
|
+
} catch (error) {
|
31
|
+
debugLog(`[Sound] Failed to play sound: ${error}`);
|
32
|
+
}
|
33
|
+
}
|
34
|
+
var UtteranceQueue = class {
|
35
|
+
utterances = [];
|
36
|
+
add(text, timestamp) {
|
37
|
+
const utterance = {
|
38
|
+
id: randomUUID(),
|
39
|
+
text: text.trim(),
|
40
|
+
timestamp: timestamp || /* @__PURE__ */ new Date(),
|
41
|
+
status: "pending"
|
42
|
+
};
|
43
|
+
this.utterances.push(utterance);
|
44
|
+
debugLog(`[Queue] queued: "${utterance.text}" [id: ${utterance.id}]`);
|
45
|
+
return utterance;
|
46
|
+
}
|
47
|
+
getRecent(limit = 10) {
|
48
|
+
return this.utterances.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()).slice(0, limit);
|
49
|
+
}
|
50
|
+
markDelivered(id) {
|
51
|
+
const utterance = this.utterances.find((u) => u.id === id);
|
52
|
+
if (utterance) {
|
53
|
+
utterance.status = "delivered";
|
54
|
+
debugLog(`[Queue] delivered: "${utterance.text}" [id: ${id}]`);
|
55
|
+
}
|
56
|
+
}
|
57
|
+
clear() {
|
58
|
+
const count = this.utterances.length;
|
59
|
+
this.utterances = [];
|
60
|
+
debugLog(`[Queue] Cleared ${count} utterances`);
|
61
|
+
}
|
62
|
+
};
|
63
|
+
var IS_MCP_MANAGED = process.argv.includes("--mcp-managed");
|
64
|
+
var queue = new UtteranceQueue();
|
65
|
+
var lastTimeoutTimestamp = null;
|
66
|
+
var app = express();
|
67
|
+
app.use(cors());
|
68
|
+
app.use(express.json());
|
69
|
+
app.use(express.static(path.join(__dirname, "..", "public")));
|
70
|
+
app.post("/api/potential-utterances", (req, res) => {
|
71
|
+
const { text, timestamp } = req.body;
|
72
|
+
if (!text || !text.trim()) {
|
73
|
+
res.status(400).json({ error: "Text is required" });
|
74
|
+
return;
|
75
|
+
}
|
76
|
+
const parsedTimestamp = timestamp ? new Date(timestamp) : void 0;
|
77
|
+
const utterance = queue.add(text, parsedTimestamp);
|
78
|
+
res.json({
|
79
|
+
success: true,
|
80
|
+
utterance: {
|
81
|
+
id: utterance.id,
|
82
|
+
text: utterance.text,
|
83
|
+
timestamp: utterance.timestamp,
|
84
|
+
status: utterance.status
|
85
|
+
}
|
86
|
+
});
|
87
|
+
});
|
88
|
+
app.get("/api/utterances", (req, res) => {
|
89
|
+
const limit = parseInt(req.query.limit) || 10;
|
90
|
+
const utterances = queue.getRecent(limit);
|
91
|
+
res.json({
|
92
|
+
utterances: utterances.map((u) => ({
|
93
|
+
id: u.id,
|
94
|
+
text: u.text,
|
95
|
+
timestamp: u.timestamp,
|
96
|
+
status: u.status
|
97
|
+
}))
|
98
|
+
});
|
99
|
+
});
|
100
|
+
app.get("/api/utterances/status", (req, res) => {
|
101
|
+
const total = queue.utterances.length;
|
102
|
+
const pending = queue.utterances.filter((u) => u.status === "pending").length;
|
103
|
+
const delivered = queue.utterances.filter((u) => u.status === "delivered").length;
|
104
|
+
res.json({
|
105
|
+
total,
|
106
|
+
pending,
|
107
|
+
delivered
|
108
|
+
});
|
109
|
+
});
|
110
|
+
app.post("/api/dequeue-utterances", (req, res) => {
|
111
|
+
const { limit = 10 } = req.body;
|
112
|
+
const pendingUtterances = queue.utterances.filter((u) => u.status === "pending").sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()).slice(0, limit);
|
113
|
+
pendingUtterances.forEach((u) => {
|
114
|
+
queue.markDelivered(u.id);
|
115
|
+
});
|
116
|
+
res.json({
|
117
|
+
success: true,
|
118
|
+
utterances: pendingUtterances.map((u) => ({
|
119
|
+
text: u.text,
|
120
|
+
timestamp: u.timestamp
|
121
|
+
}))
|
122
|
+
});
|
123
|
+
});
|
124
|
+
app.post("/api/wait-for-utterances", async (req, res) => {
|
125
|
+
const { seconds_to_wait = DEFAULT_WAIT_TIMEOUT_SECONDS } = req.body;
|
126
|
+
const secondsToWait = Math.max(
|
127
|
+
MIN_WAIT_TIMEOUT_SECONDS,
|
128
|
+
Math.min(MAX_WAIT_TIMEOUT_SECONDS, seconds_to_wait)
|
129
|
+
);
|
130
|
+
const maxWaitMs = secondsToWait * 1e3;
|
131
|
+
const startTime = Date.now();
|
132
|
+
debugLog(`[Server] Starting wait_for_utterance (${secondsToWait}s)`);
|
133
|
+
if (lastTimeoutTimestamp) {
|
134
|
+
const hasNewUtterances = queue.utterances.some(
|
135
|
+
(u) => u.timestamp > lastTimeoutTimestamp
|
136
|
+
);
|
137
|
+
if (!hasNewUtterances) {
|
138
|
+
debugLog("[Server] No new utterances since last timeout, returning immediately");
|
139
|
+
res.json({
|
140
|
+
success: true,
|
141
|
+
utterances: [],
|
142
|
+
message: `No utterances found after waiting ${secondsToWait} seconds.`,
|
143
|
+
waitTime: 0
|
144
|
+
});
|
145
|
+
return;
|
146
|
+
}
|
147
|
+
}
|
148
|
+
let firstTime = true;
|
149
|
+
while (Date.now() - startTime < maxWaitMs) {
|
150
|
+
const pendingUtterances = queue.utterances.filter(
|
151
|
+
(u) => u.status === "pending" && (!lastTimeoutTimestamp || u.timestamp > lastTimeoutTimestamp)
|
152
|
+
);
|
153
|
+
if (pendingUtterances.length > 0) {
|
154
|
+
lastTimeoutTimestamp = null;
|
155
|
+
const sortedUtterances = pendingUtterances.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
156
|
+
sortedUtterances.forEach((u) => {
|
157
|
+
queue.markDelivered(u.id);
|
158
|
+
});
|
159
|
+
res.json({
|
160
|
+
success: true,
|
161
|
+
utterances: sortedUtterances.map((u) => ({
|
162
|
+
id: u.id,
|
163
|
+
text: u.text,
|
164
|
+
timestamp: u.timestamp,
|
165
|
+
status: "delivered"
|
166
|
+
// They are now delivered
|
167
|
+
})),
|
168
|
+
count: pendingUtterances.length,
|
169
|
+
waitTime: Date.now() - startTime
|
170
|
+
});
|
171
|
+
return;
|
172
|
+
}
|
173
|
+
if (firstTime) {
|
174
|
+
firstTime = false;
|
175
|
+
await playNotificationSound();
|
176
|
+
}
|
177
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
178
|
+
}
|
179
|
+
lastTimeoutTimestamp = /* @__PURE__ */ new Date();
|
180
|
+
res.json({
|
181
|
+
success: true,
|
182
|
+
utterances: [],
|
183
|
+
message: `No utterances found after waiting ${secondsToWait} seconds.`,
|
184
|
+
waitTime: maxWaitMs
|
185
|
+
});
|
186
|
+
});
|
187
|
+
app.get("/api/should-wait", (req, res) => {
|
188
|
+
const shouldWait = !lastTimeoutTimestamp || queue.utterances.some((u) => u.timestamp > lastTimeoutTimestamp);
|
189
|
+
res.json({ shouldWait });
|
190
|
+
});
|
191
|
+
app.get("/api/has-pending-utterances", (req, res) => {
|
192
|
+
const pendingCount = queue.utterances.filter((u) => u.status === "pending").length;
|
193
|
+
const hasPending = pendingCount > 0;
|
194
|
+
res.json({
|
195
|
+
hasPending,
|
196
|
+
pendingCount
|
197
|
+
});
|
198
|
+
});
|
199
|
+
app.delete("/api/utterances", (req, res) => {
|
200
|
+
const clearedCount = queue.utterances.length;
|
201
|
+
queue.clear();
|
202
|
+
res.json({
|
203
|
+
success: true,
|
204
|
+
message: `Cleared ${clearedCount} utterances`,
|
205
|
+
clearedCount
|
206
|
+
});
|
207
|
+
});
|
208
|
+
app.get("/", (req, res) => {
|
209
|
+
res.sendFile(path.join(__dirname, "..", "public", "index.html"));
|
210
|
+
});
|
211
|
+
var HTTP_PORT = 3e3;
|
212
|
+
app.listen(HTTP_PORT, () => {
|
213
|
+
console.log(`[HTTP] Server listening on http://localhost:${HTTP_PORT}`);
|
214
|
+
console.log(`[Mode] Running in ${IS_MCP_MANAGED ? "MCP-managed" : "standalone"} mode`);
|
215
|
+
});
|
216
|
+
if (IS_MCP_MANAGED) {
|
217
|
+
console.log("[MCP] Initializing MCP server...");
|
218
|
+
const mcpServer = new Server(
|
219
|
+
{
|
220
|
+
name: "voice-hooks",
|
221
|
+
version: "1.0.0"
|
222
|
+
},
|
223
|
+
{
|
224
|
+
capabilities: {
|
225
|
+
tools: {}
|
226
|
+
}
|
227
|
+
}
|
228
|
+
);
|
229
|
+
mcpServer.setRequestHandler(ListToolsRequestSchema, async () => {
|
230
|
+
return {
|
231
|
+
tools: [
|
232
|
+
{
|
233
|
+
name: "dequeue_utterances",
|
234
|
+
description: "Dequeue pending utterances and mark them as delivered",
|
235
|
+
inputSchema: {
|
236
|
+
type: "object",
|
237
|
+
properties: {
|
238
|
+
limit: {
|
239
|
+
type: "number",
|
240
|
+
description: "Maximum number of utterances to dequeue (default: 10)",
|
241
|
+
default: 10
|
242
|
+
}
|
243
|
+
}
|
244
|
+
}
|
245
|
+
},
|
246
|
+
{
|
247
|
+
name: "wait_for_utterance",
|
248
|
+
description: "Wait for an utterance to be available or until timeout. Returns immediately if no utterances since last timeout.",
|
249
|
+
inputSchema: {
|
250
|
+
type: "object",
|
251
|
+
properties: {
|
252
|
+
seconds_to_wait: {
|
253
|
+
type: "number",
|
254
|
+
description: `Maximum seconds to wait for an utterance (default: ${DEFAULT_WAIT_TIMEOUT_SECONDS}, min: ${MIN_WAIT_TIMEOUT_SECONDS}, max: ${MAX_WAIT_TIMEOUT_SECONDS})`,
|
255
|
+
default: DEFAULT_WAIT_TIMEOUT_SECONDS,
|
256
|
+
minimum: MIN_WAIT_TIMEOUT_SECONDS,
|
257
|
+
maximum: MAX_WAIT_TIMEOUT_SECONDS
|
258
|
+
}
|
259
|
+
}
|
260
|
+
}
|
261
|
+
}
|
262
|
+
]
|
263
|
+
};
|
264
|
+
});
|
265
|
+
mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
266
|
+
const { name, arguments: args } = request.params;
|
267
|
+
try {
|
268
|
+
if (name === "dequeue_utterances") {
|
269
|
+
const limit = args?.limit ?? 10;
|
270
|
+
const response = await fetch("http://localhost:3000/api/dequeue-utterances", {
|
271
|
+
method: "POST",
|
272
|
+
headers: { "Content-Type": "application/json" },
|
273
|
+
body: JSON.stringify({ limit })
|
274
|
+
});
|
275
|
+
const data = await response.json();
|
276
|
+
if (data.utterances.length === 0) {
|
277
|
+
return {
|
278
|
+
content: [
|
279
|
+
{
|
280
|
+
type: "text",
|
281
|
+
text: "No recent utterances found."
|
282
|
+
}
|
283
|
+
]
|
284
|
+
};
|
285
|
+
}
|
286
|
+
return {
|
287
|
+
content: [
|
288
|
+
{
|
289
|
+
type: "text",
|
290
|
+
text: `Dequeued ${data.utterances.length} utterance(s):
|
291
|
+
|
292
|
+
${data.utterances.reverse().map((u) => `"${u.text}" [time: ${new Date(u.timestamp).toISOString()}]`).join("\n")}`
|
293
|
+
}
|
294
|
+
]
|
295
|
+
};
|
296
|
+
}
|
297
|
+
if (name === "wait_for_utterance") {
|
298
|
+
const requestedSeconds = args?.seconds_to_wait ?? DEFAULT_WAIT_TIMEOUT_SECONDS;
|
299
|
+
const secondsToWait = Math.max(
|
300
|
+
MIN_WAIT_TIMEOUT_SECONDS,
|
301
|
+
Math.min(MAX_WAIT_TIMEOUT_SECONDS, requestedSeconds)
|
302
|
+
);
|
303
|
+
debugLog(`[MCP] Calling wait_for_utterance with ${secondsToWait}s timeout`);
|
304
|
+
const response = await fetch("http://localhost:3000/api/wait-for-utterances", {
|
305
|
+
method: "POST",
|
306
|
+
headers: { "Content-Type": "application/json" },
|
307
|
+
body: JSON.stringify({ seconds_to_wait: secondsToWait })
|
308
|
+
});
|
309
|
+
const data = await response.json();
|
310
|
+
if (data.utterances && data.utterances.length > 0) {
|
311
|
+
const utteranceTexts = data.utterances.map((u) => `[${u.timestamp}] "${u.text}"`).join("\n");
|
312
|
+
return {
|
313
|
+
content: [
|
314
|
+
{
|
315
|
+
type: "text",
|
316
|
+
text: `Found ${data.count} utterance(s):
|
317
|
+
|
318
|
+
${utteranceTexts}`
|
319
|
+
}
|
320
|
+
]
|
321
|
+
};
|
322
|
+
} else {
|
323
|
+
return {
|
324
|
+
content: [
|
325
|
+
{
|
326
|
+
type: "text",
|
327
|
+
text: data.message || `No utterances found after waiting ${secondsToWait} seconds.`
|
328
|
+
}
|
329
|
+
]
|
330
|
+
};
|
331
|
+
}
|
332
|
+
}
|
333
|
+
throw new Error(`Unknown tool: ${name}`);
|
334
|
+
} catch (error) {
|
335
|
+
return {
|
336
|
+
content: [
|
337
|
+
{
|
338
|
+
type: "text",
|
339
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`
|
340
|
+
}
|
341
|
+
],
|
342
|
+
isError: true
|
343
|
+
};
|
344
|
+
}
|
345
|
+
});
|
346
|
+
const transport = new StdioServerTransport();
|
347
|
+
mcpServer.connect(transport);
|
348
|
+
console.log("[MCP] Server connected via stdio");
|
349
|
+
} else {
|
350
|
+
console.log("[MCP] Skipping MCP server initialization (not in MCP-managed mode)");
|
351
|
+
}
|
352
|
+
//# sourceMappingURL=unified-server.js.map
|
@@ -0,0 +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 = 3000;\napp.listen(HTTP_PORT, () => {\n console.log(`[HTTP] Server listening on http://localhost:${HTTP_PORT}`);\n console.log(`[Mode] Running in ${IS_MCP_MANAGED ? 'MCP-managed' : 'standalone'} mode`);\n});\n\n// MCP Server Setup (only if MCP-managed)\nif (IS_MCP_MANAGED) {\n console.log('[MCP] Initializing MCP server...');\n\n const mcpServer = new Server(\n {\n name: 'voice-hooks',\n version: '1.0.0',\n },\n {\n capabilities: {\n tools: {},\n },\n }\n );\n\n // Tool handlers\n mcpServer.setRequestHandler(ListToolsRequestSchema, async () => {\n return {\n tools: [\n {\n name: 'dequeue_utterances',\n description: 'Dequeue pending utterances and mark them as delivered',\n inputSchema: {\n type: 'object',\n properties: {\n limit: {\n type: 'number',\n description: 'Maximum number of utterances to dequeue (default: 10)',\n default: 10,\n },\n },\n },\n },\n {\n name: 'wait_for_utterance',\n description: 'Wait for an utterance to be available or until timeout. Returns immediately if no utterances since last timeout.',\n inputSchema: {\n type: 'object',\n properties: {\n seconds_to_wait: {\n type: 'number',\n description: `Maximum seconds to wait for an utterance (default: ${DEFAULT_WAIT_TIMEOUT_SECONDS}, min: ${MIN_WAIT_TIMEOUT_SECONDS}, max: ${MAX_WAIT_TIMEOUT_SECONDS})`,\n default: DEFAULT_WAIT_TIMEOUT_SECONDS,\n minimum: MIN_WAIT_TIMEOUT_SECONDS,\n maximum: MAX_WAIT_TIMEOUT_SECONDS,\n },\n },\n },\n },\n ],\n };\n });\n\n mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {\n const { name, arguments: args } = request.params;\n\n try {\n if (name === 'dequeue_utterances') {\n const limit = (args?.limit as number) ?? 10;\n const response = await fetch('http://localhost:3000/api/dequeue-utterances', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ limit }),\n });\n\n const data = await response.json() as any;\n\n if (data.utterances.length === 0) {\n return {\n content: [\n {\n type: 'text',\n text: 'No recent utterances found.',\n },\n ],\n };\n }\n\n return {\n content: [\n {\n type: 'text',\n text: `Dequeued ${data.utterances.length} utterance(s):\\n\\n${data.utterances.reverse().map((u: any) => `\"${u.text}\"\\t[time: ${new Date(u.timestamp).toISOString()}]`).join('\\n')\n }`,\n },\n ],\n };\n }\n\n if (name === 'wait_for_utterance') {\n const requestedSeconds = (args?.seconds_to_wait as number) ?? DEFAULT_WAIT_TIMEOUT_SECONDS;\n const secondsToWait = Math.max(\n MIN_WAIT_TIMEOUT_SECONDS,\n Math.min(MAX_WAIT_TIMEOUT_SECONDS, requestedSeconds)\n );\n debugLog(`[MCP] Calling wait_for_utterance with ${secondsToWait}s timeout`);\n\n const response = await fetch('http://localhost:3000/api/wait-for-utterances', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ seconds_to_wait: secondsToWait }),\n });\n\n const data = await response.json() as any;\n\n if (data.utterances && data.utterances.length > 0) {\n const utteranceTexts = data.utterances\n .map((u: any) => `[${u.timestamp}] \"${u.text}\"`)\n .join('\\n');\n\n return {\n content: [\n {\n type: 'text',\n text: `Found ${data.count} utterance(s):\\n\\n${utteranceTexts}`,\n },\n ],\n };\n } else {\n return {\n content: [\n {\n type: 'text',\n text: data.message || `No utterances found after waiting ${secondsToWait} seconds.`,\n },\n ],\n };\n }\n }\n\n throw new Error(`Unknown tool: ${name}`);\n } catch (error) {\n return {\n content: [\n {\n type: 'text',\n text: `Error: ${error instanceof Error ? error.message : String(error)}`,\n },\n ],\n isError: true,\n };\n }\n });\n\n // Connect via stdio\n const transport = new StdioServerTransport();\n mcpServer.connect(transport);\n console.log('[MCP] Server connected via stdio');\n} else {\n console.log('[MCP] Skipping MCP server initialization (not in MCP-managed mode)');\n}"],"mappings":"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","names":[]}
|
@@ -1,15 +0,0 @@
|
|
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
|
File without changes
|
File without changes
|