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/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
- // Clear interim text
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('Hello! This is a test of the text-to-speech voice.');
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. Type something above to get started!</div>';
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
- display: block;
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"></div>
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</div>
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>
@@ -0,0 +1,12 @@
1
+ // src/debug.ts
2
+ var DEBUG = process.env.DEBUG === "true" || process.env.VOICE_HOOKS_DEBUG === "true";
3
+ function debugLog(...args) {
4
+ if (DEBUG) {
5
+ console.log(...args);
6
+ }
7
+ }
8
+
9
+ export {
10
+ debugLog
11
+ };
12
+ //# sourceMappingURL=chunk-IYGM5COW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/debug.ts"],"sourcesContent":["const DEBUG = process.env.DEBUG === 'true' || process.env.VOICE_HOOKS_DEBUG === 'true';\n\nexport function debugLog(...args: any[]): void {\n if (DEBUG) {\n console.log(...args);\n }\n}"],"mappings":";AAAA,IAAM,QAAQ,QAAQ,IAAI,UAAU,UAAU,QAAQ,IAAI,sBAAsB;AAEzE,SAAS,YAAY,MAAmB;AAC7C,MAAI,OAAO;AACT,YAAQ,IAAI,GAAG,IAAI;AAAA,EACrB;AACF;","names":[]}
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -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