mcp-voice-hooks 1.0.13 → 1.0.14
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 +18 -0
- package/README.md +21 -47
- package/bin/cli.js +26 -26
- package/dist/index.js +24 -1
- package/dist/index.js.map +1 -1
- package/dist/unified-server.js +26 -12
- package/dist/unified-server.js.map +1 -1
- package/package.json +1 -1
- package/public/app.js +5 -47
- package/public/index.html +13 -9
- 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/test-npx-clean/package/mcp-voice-hooks-1.0.0.tgz +0 -0
- package/test-npx-clean/package/mcp-voice-hooks-1.0.1.tgz +0 -0
package/public/app.js
CHANGED
@@ -2,8 +2,6 @@ class VoiceHooksClient {
|
|
2
2
|
constructor() {
|
3
3
|
this.baseUrl = window.location.origin;
|
4
4
|
this.debug = localStorage.getItem('voiceHooksDebug') === 'true';
|
5
|
-
this.utteranceInput = document.getElementById('utteranceInput');
|
6
|
-
this.sendBtn = document.getElementById('sendBtn');
|
7
5
|
this.refreshBtn = document.getElementById('refreshBtn');
|
8
6
|
this.clearAllBtn = document.getElementById('clearAllBtn');
|
9
7
|
this.utterancesList = document.getElementById('utterancesList');
|
@@ -76,8 +74,8 @@ class VoiceHooksClient {
|
|
76
74
|
if (event.results[i].isFinal) {
|
77
75
|
// User paused - send as complete utterance
|
78
76
|
this.sendVoiceUtterance(transcript);
|
79
|
-
//
|
80
|
-
this.interimText.textContent = '';
|
77
|
+
// Restore placeholder text
|
78
|
+
this.interimText.textContent = 'Start speaking and your words will appear here...';
|
81
79
|
this.interimText.classList.remove('active');
|
82
80
|
} else {
|
83
81
|
// Still speaking - show interim results
|
@@ -124,16 +122,10 @@ class VoiceHooksClient {
|
|
124
122
|
}
|
125
123
|
|
126
124
|
setupEventListeners() {
|
127
|
-
this.sendBtn.addEventListener('click', () => this.sendUtterance());
|
128
125
|
this.refreshBtn.addEventListener('click', () => this.loadData());
|
129
126
|
this.clearAllBtn.addEventListener('click', () => this.clearAllUtterances());
|
130
127
|
this.listenBtn.addEventListener('click', () => this.toggleListening());
|
131
128
|
|
132
|
-
this.utteranceInput.addEventListener('keypress', (e) => {
|
133
|
-
if (e.key === 'Enter') {
|
134
|
-
this.sendUtterance();
|
135
|
-
}
|
136
|
-
});
|
137
129
|
|
138
130
|
// TTS controls
|
139
131
|
this.voiceSelect.addEventListener('change', (e) => {
|
@@ -164,7 +156,7 @@ class VoiceHooksClient {
|
|
164
156
|
});
|
165
157
|
|
166
158
|
this.testTTSBtn.addEventListener('click', () => {
|
167
|
-
this.speakText('
|
159
|
+
this.speakText('Hi, this is Voice Mode for Claude Code. How can I help you today?');
|
168
160
|
});
|
169
161
|
|
170
162
|
// Voice toggle listeners
|
@@ -176,40 +168,6 @@ class VoiceHooksClient {
|
|
176
168
|
});
|
177
169
|
}
|
178
170
|
|
179
|
-
async sendUtterance() {
|
180
|
-
const text = this.utteranceInput.value.trim();
|
181
|
-
if (!text) return;
|
182
|
-
|
183
|
-
this.sendBtn.disabled = true;
|
184
|
-
this.sendBtn.textContent = 'Sending...';
|
185
|
-
|
186
|
-
try {
|
187
|
-
const response = await fetch(`${this.baseUrl}/api/potential-utterances`, {
|
188
|
-
method: 'POST',
|
189
|
-
headers: {
|
190
|
-
'Content-Type': 'application/json',
|
191
|
-
},
|
192
|
-
body: JSON.stringify({
|
193
|
-
text: text,
|
194
|
-
timestamp: new Date().toISOString()
|
195
|
-
}),
|
196
|
-
});
|
197
|
-
|
198
|
-
if (response.ok) {
|
199
|
-
this.utteranceInput.value = '';
|
200
|
-
this.loadData(); // Refresh the list
|
201
|
-
} else {
|
202
|
-
const error = await response.json();
|
203
|
-
alert(`Error: ${error.error || 'Failed to send utterance'}`);
|
204
|
-
}
|
205
|
-
} catch (error) {
|
206
|
-
console.error('Failed to send utterance:', error);
|
207
|
-
alert('Failed to send utterance. Make sure the server is running.');
|
208
|
-
} finally {
|
209
|
-
this.sendBtn.disabled = false;
|
210
|
-
this.sendBtn.textContent = 'Send';
|
211
|
-
}
|
212
|
-
}
|
213
171
|
|
214
172
|
async loadData() {
|
215
173
|
try {
|
@@ -239,7 +197,7 @@ class VoiceHooksClient {
|
|
239
197
|
|
240
198
|
updateUtterancesList(utterances) {
|
241
199
|
if (utterances.length === 0) {
|
242
|
-
this.utterancesList.innerHTML = '<div class="empty-state">No utterances yet.
|
200
|
+
this.utterancesList.innerHTML = '<div class="empty-state">No utterances yet. Click "Start Listening" to begin!</div>';
|
243
201
|
this.infoMessage.style.display = 'none';
|
244
202
|
return;
|
245
203
|
}
|
@@ -315,7 +273,7 @@ class VoiceHooksClient {
|
|
315
273
|
this.listenBtn.classList.remove('listening');
|
316
274
|
this.listenBtnText.textContent = 'Start Listening';
|
317
275
|
this.listeningIndicator.classList.remove('active');
|
318
|
-
this.interimText.textContent = '';
|
276
|
+
this.interimText.textContent = 'Start speaking and your words will appear here...';
|
319
277
|
this.interimText.classList.remove('active');
|
320
278
|
this.debugLog('Stopped listening');
|
321
279
|
|
package/public/index.html
CHANGED
@@ -246,7 +246,6 @@
|
|
246
246
|
}
|
247
247
|
|
248
248
|
.interim-text {
|
249
|
-
display: none;
|
250
249
|
padding: 12px;
|
251
250
|
background: #F8F9FA;
|
252
251
|
border: 1px solid #DEE2E6;
|
@@ -254,10 +253,19 @@
|
|
254
253
|
margin-bottom: 16px;
|
255
254
|
font-style: italic;
|
256
255
|
color: #6C757D;
|
256
|
+
min-height: 44px;
|
257
|
+
/* Maintain minimum height to prevent collapse */
|
258
|
+
max-height: 120px;
|
259
|
+
/* Prevent excessive growth */
|
260
|
+
overflow-y: auto;
|
261
|
+
/* Add scroll if content is too long */
|
262
|
+
transition: color 0.2s ease-in-out;
|
257
263
|
}
|
258
264
|
|
259
265
|
.interim-text.active {
|
260
|
-
|
266
|
+
color: #333;
|
267
|
+
/* Darker color for actual speech */
|
268
|
+
font-style: normal;
|
261
269
|
}
|
262
270
|
|
263
271
|
.mic-icon {
|
@@ -471,12 +479,7 @@
|
|
471
479
|
</div>
|
472
480
|
</div>
|
473
481
|
|
474
|
-
<div class="interim-text" id="interimText"
|
475
|
-
|
476
|
-
<div class="input-group">
|
477
|
-
<input type="text" id="utteranceInput" placeholder="Type your utterance here..." autofocus>
|
478
|
-
<button id="sendBtn">Send</button>
|
479
|
-
</div>
|
482
|
+
<div class="interim-text" id="interimText">Start speaking and your words will appear here...</div>
|
480
483
|
</div>
|
481
484
|
|
482
485
|
<div class="status">
|
@@ -502,7 +505,8 @@
|
|
502
505
|
All</button>
|
503
506
|
</h3>
|
504
507
|
<div class="info-message" id="infoMessage" style="display: none;">
|
505
|
-
<div class="empty-state">You need to send one message in the Claude code CLI to start voice interaction
|
508
|
+
<div class="empty-state">You need to send one message in the Claude code CLI to start voice interaction
|
509
|
+
</div>
|
506
510
|
</div>
|
507
511
|
<div class="utterances-list" id="utterancesList">
|
508
512
|
<div class="empty-state">No utterances yet. Type something above to get started!</div>
|
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
|