mcp-voice-hooks 1.0.8 ā 1.0.13
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/README.md +56 -81
- package/bin/cli.js +10 -100
- package/dist/hook-merger.d.ts +1 -1
- package/dist/hook-merger.js +1 -1
- package/dist/hook-merger.js.map +1 -1
- package/dist/unified-server.js +147 -95
- package/dist/unified-server.js.map +1 -1
- package/package.json +1 -1
- package/public/app.js +451 -45
- package/public/index.html +255 -61
- package/.claude/hooks/pre-speak-hook.sh +0 -3
- package/.claude/hooks/pre-tool-hook.sh +0 -3
- package/.claude/hooks/pre-wait-hook.sh +0 -3
- package/.claude/hooks/stop-hook.sh +0 -3
- package/CLAUDE.local.md +0 -25
- package/test-npx-clean/mcp-voice-hooks-1.0.1.tgz +0 -0
- package/test-npx-clean/package/dist/chunk-IYGM5COW.js +0 -12
- package/test-npx-clean/package/dist/chunk-IYGM5COW.js.map +0 -1
- package/test-npx-clean/package/dist/index.d.ts +0 -2
- package/test-npx-clean/package/dist/index.js +0 -125
- package/test-npx-clean/package/dist/index.js.map +0 -1
- package/test-npx-clean/package/dist/unified-server.d.ts +0 -1
- package/test-npx-clean/package/dist/unified-server.js +0 -352
- package/test-npx-clean/package/dist/unified-server.js.map +0 -1
- 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/README.md
CHANGED
@@ -2,6 +2,10 @@
|
|
2
2
|
|
3
3
|
Real-time voice interaction for Claude Code. Speak naturally while Claude works - interrupt, redirect, or provide continuous feedback without stopping.
|
4
4
|
|
5
|
+
Optionally enable text-to-speech to have Claude speak back to you.
|
6
|
+
|
7
|
+
Mac only for now.
|
8
|
+
|
5
9
|
## Demo
|
6
10
|
|
7
11
|
[](https://youtu.be/KpkxvJ65gbM)
|
@@ -15,11 +19,11 @@ mcp-voice-hooks enables continuous voice conversations with AI assistants by:
|
|
15
19
|
- Using hooks to ensure Claude checks for voice input before tool use and before stopping
|
16
20
|
- Allowing natural interruptions like "No, stop that" or "Wait, try something else"
|
17
21
|
|
18
|
-
##
|
22
|
+
## Browser Compatibility
|
19
23
|
|
20
|
-
-
|
21
|
-
-
|
22
|
-
-
|
24
|
+
- ā
**Chrome**: Full support for speech recognition, browser text-to-speech, and system text-to-speech
|
25
|
+
- ā ļø **Safari**: Full support for speech recognition, but only system text-to-speech is supported
|
26
|
+
- ā **Edge**: Speech recognition not working on Apple Silicon (language-not-supported error)
|
23
27
|
|
24
28
|
## Installation in Your Own Project
|
25
29
|
|
@@ -52,21 +56,42 @@ mcp-voice-hooks enables continuous voice conversations with AI assistants by:
|
|
52
56
|
claude
|
53
57
|
```
|
54
58
|
|
55
|
-
|
59
|
+
**Important**: After the first-time installation, you will need to restart Claude for the hooks to take effect. This is because the hooks are automatically installed when the MCP server starts for the first time.
|
56
60
|
|
57
|
-
|
61
|
+
3. **Open the voice interface** at <http://localhost:5111> and start speaking!
|
58
62
|
|
59
|
-
|
63
|
+
You need to send one text message to Claude to trigger the voice hooks.
|
64
|
+
|
65
|
+
## Voice responses
|
66
|
+
|
67
|
+
There are two options for voice responses:
|
68
|
+
|
69
|
+
1. Browser Text-to-Speech
|
70
|
+
2. System Text-to-Speech
|
71
|
+
|
72
|
+
### Selecting and downloading high quality System Voices (Mac only)
|
73
|
+
|
74
|
+
Mac has built-in text to speech, but high quality voices are not available by default.
|
75
|
+
|
76
|
+
You can download high quality voices from the system voice menu: `System Settings > Accessibility > Spoken Content > System Voice`
|
77
|
+
|
78
|
+
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.
|
79
|
+
|
80
|
+
Once it's downloaded, you can select it in the system voice menu.
|
81
|
+
|
82
|
+
Test it with the bash command:
|
83
|
+
|
84
|
+
```bash
|
85
|
+
say "Hi, this is your mac system voice"
|
86
|
+
```
|
87
|
+
|
88
|
+
For Siri voices you need to set your system voice and select "Mac System Voice" in the voice-hooks browser interface.
|
89
|
+
|
90
|
+
Other downloaded voices will show up in the voice dropdown in the voice-hooks browser interface.
|
91
|
+
|
92
|
+
### Selecting and downloading high quality Browser Voices
|
60
93
|
|
61
|
-
The default port is 5111. To use a different port, add to your project's `.claude/settings.json`:
|
62
94
|
|
63
|
-
```json
|
64
|
-
{
|
65
|
-
"env": {
|
66
|
-
"MCP_VOICE_HOOKS_PORT": "8080"
|
67
|
-
}
|
68
|
-
}
|
69
|
-
```
|
70
95
|
|
71
96
|
## Manual Hook Installation
|
72
97
|
|
@@ -76,30 +101,31 @@ The hooks are automatically installed/updated when the MCP server starts. Howeve
|
|
76
101
|
npx mcp-voice-hooks install-hooks
|
77
102
|
```
|
78
103
|
|
79
|
-
This will
|
80
|
-
|
81
|
-
- Clean up any existing `~/.mcp-voice-hooks` directory contents
|
82
|
-
- Install/update hook scripts to `~/.mcp-voice-hooks/hooks/`
|
83
|
-
- Configure your project's `.claude/settings.json`
|
104
|
+
This will configure your project's `.claude/settings.json` with the necessary hook commands.
|
84
105
|
|
85
106
|
## Uninstallation
|
86
107
|
|
87
108
|
To completely remove MCP Voice Hooks:
|
88
109
|
|
89
110
|
```bash
|
90
|
-
# Remove
|
91
|
-
npx mcp-voice-hooks uninstall
|
92
|
-
|
93
|
-
# Also remove from Claude MCP servers
|
111
|
+
# Remove from Claude MCP servers
|
94
112
|
claude mcp remove voice-hooks
|
95
113
|
```
|
96
114
|
|
115
|
+
```bash
|
116
|
+
# Also remove hooks and settings
|
117
|
+
npx mcp-voice-hooks uninstall
|
118
|
+
```
|
119
|
+
|
97
120
|
This will:
|
98
121
|
|
99
|
-
- Remove the `~/.mcp-voice-hooks` directory
|
100
122
|
- Clean up voice hooks from your project's `.claude/settings.json`
|
101
123
|
- Preserve any custom hooks you've added
|
102
124
|
|
125
|
+
## Known Limitations
|
126
|
+
|
127
|
+
- **Intermittent Stop Hook Execution**: Claude Code's Stop hooks are not triggered if the agent stops immediately after a tool call. This results in the assistant occasionally stopping without checking for voice input. This will be fixed in 1.0.45. [github issue](https://github.com/anthropics/claude-code/issues/3113#issuecomment-3047324928)
|
128
|
+
|
103
129
|
## Development Mode
|
104
130
|
|
105
131
|
If you're developing mcp-voice-hooks itself:
|
@@ -152,65 +178,14 @@ and then configure claude to use the mcp proxy like so:
|
|
152
178
|
}
|
153
179
|
```
|
154
180
|
|
155
|
-
|
181
|
+
### Port Configuration
|
156
182
|
|
157
|
-
|
183
|
+
The default port is 5111. To use a different port, add to your project's `.claude/settings.json`:
|
158
184
|
|
159
|
-
```json
|
160
|
-
{
|
185
|
+
```json
|
161
186
|
{
|
162
|
-
"hooks": {
|
163
|
-
"PostToolUse": [
|
164
|
-
{
|
165
|
-
"matcher": "^mcp__voice-hooks__",
|
166
|
-
"hooks": [
|
167
|
-
{
|
168
|
-
"type": "command",
|
169
|
-
"command": "./.claude/hooks/post-tool-voice-hook.sh"
|
170
|
-
}
|
171
|
-
]
|
172
|
-
}
|
173
|
-
]
|
174
|
-
},
|
175
187
|
"env": {
|
176
|
-
"
|
188
|
+
"MCP_VOICE_HOOKS_PORT": "8080"
|
177
189
|
}
|
178
190
|
}
|
179
|
-
|
180
|
-
```
|
181
|
-
|
182
|
-
### Configuration
|
183
|
-
|
184
|
-
Voice responses are disabled by default. To enable them:
|
185
|
-
|
186
|
-
Add to your Claude Code settings JSON:
|
187
|
-
|
188
|
-
```json
|
189
|
-
{
|
190
|
-
"env": {
|
191
|
-
"VOICE_RESPONSES_ENABLED": "true"
|
192
|
-
}
|
193
|
-
}
|
194
|
-
```
|
195
|
-
|
196
|
-
To disable voice responses, set the value to `false` or remove the setting entirely.
|
197
|
-
|
198
|
-
### High quality voice responses
|
199
|
-
|
200
|
-
These voice responses are spoken by your Mac's system voice.
|
201
|
-
|
202
|
-
Configure in `System Settings > Accessibility > Spoken Content > System Voice`
|
203
|
-
|
204
|
-
I recommend using a Siri voice, as they are much higher quality.
|
205
|
-
|
206
|
-
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.
|
207
|
-
|
208
|
-
It may take a while to download.
|
209
|
-
|
210
|
-
Once it's downloaded, you can select it in the system voice dropdown.
|
211
|
-
|
212
|
-
Test it with the bash command:
|
213
|
-
|
214
|
-
```bash
|
215
|
-
say "Hi, this is your mac system voice"
|
216
|
-
```
|
191
|
+
```
|
package/bin/cli.js
CHANGED
@@ -2,10 +2,9 @@
|
|
2
2
|
|
3
3
|
import fs from 'fs';
|
4
4
|
import path from 'path';
|
5
|
-
import os from 'os';
|
6
5
|
import { spawn } from 'child_process';
|
7
6
|
import { fileURLToPath } from 'url';
|
8
|
-
import { replaceVoiceHooks, areHooksEqual } from '../dist/hook-merger.js';
|
7
|
+
import { replaceVoiceHooks, areHooksEqual, removeVoiceHooks } from '../dist/hook-merger.js';
|
9
8
|
|
10
9
|
const __filename = fileURLToPath(import.meta.url);
|
11
10
|
const __dirname = path.dirname(__filename);
|
@@ -19,10 +18,7 @@ async function main() {
|
|
19
18
|
if (command === 'install-hooks') {
|
20
19
|
console.log('š§ Installing MCP Voice Hooks...');
|
21
20
|
|
22
|
-
//
|
23
|
-
await ensureUserDirectorySetup();
|
24
|
-
|
25
|
-
// Step 2: Configure Claude Code settings automatically
|
21
|
+
// Configure Claude Code settings automatically
|
26
22
|
await configureClaudeCodeSettings();
|
27
23
|
|
28
24
|
console.log('\nā
Installation complete!');
|
@@ -46,74 +42,6 @@ async function main() {
|
|
46
42
|
}
|
47
43
|
}
|
48
44
|
|
49
|
-
// Ensure ~/.mcp-voice-hooks/hooks/ directory exists and contains latest hook files
|
50
|
-
async function ensureUserDirectorySetup() {
|
51
|
-
const userDir = path.join(os.homedir(), '.mcp-voice-hooks');
|
52
|
-
const hooksDir = path.join(userDir, 'hooks');
|
53
|
-
|
54
|
-
console.log('š Setting up user directory:', userDir);
|
55
|
-
|
56
|
-
// Clean up existing directory contents (except README.md if it exists)
|
57
|
-
if (fs.existsSync(userDir)) {
|
58
|
-
console.log('š§¹ Cleaning up existing directory...');
|
59
|
-
|
60
|
-
// Remove hooks directory if it exists
|
61
|
-
if (fs.existsSync(hooksDir)) {
|
62
|
-
fs.rmSync(hooksDir, { recursive: true, force: true });
|
63
|
-
console.log('ā
Cleaned up old hooks');
|
64
|
-
}
|
65
|
-
|
66
|
-
// Remove any other files
|
67
|
-
const files = fs.readdirSync(userDir);
|
68
|
-
for (const file of files) {
|
69
|
-
const filePath = path.join(userDir, file);
|
70
|
-
const stat = fs.statSync(filePath);
|
71
|
-
if (stat.isDirectory()) {
|
72
|
-
fs.rmSync(filePath, { recursive: true, force: true });
|
73
|
-
} else {
|
74
|
-
fs.unlinkSync(filePath);
|
75
|
-
}
|
76
|
-
}
|
77
|
-
}
|
78
|
-
|
79
|
-
// Create directories
|
80
|
-
if (!fs.existsSync(userDir)) {
|
81
|
-
fs.mkdirSync(userDir, { recursive: true });
|
82
|
-
console.log('ā
Created user directory');
|
83
|
-
}
|
84
|
-
|
85
|
-
if (!fs.existsSync(hooksDir)) {
|
86
|
-
fs.mkdirSync(hooksDir, { recursive: true });
|
87
|
-
console.log('ā
Created hooks directory');
|
88
|
-
}
|
89
|
-
|
90
|
-
// Copy/update hook files from the package's .claude/hooks/ to user directory
|
91
|
-
const packageHooksDir = path.join(__dirname, '..', '.claude', 'hooks');
|
92
|
-
|
93
|
-
if (fs.existsSync(packageHooksDir)) {
|
94
|
-
const hookFiles = fs.readdirSync(packageHooksDir).filter(file => file.endsWith('.sh'));
|
95
|
-
|
96
|
-
for (const hookFile of hookFiles) {
|
97
|
-
const sourcePath = path.join(packageHooksDir, hookFile);
|
98
|
-
const destPath = path.join(hooksDir, hookFile);
|
99
|
-
|
100
|
-
// Copy hook file
|
101
|
-
fs.copyFileSync(sourcePath, destPath);
|
102
|
-
console.log(`ā
Updated hook: ${hookFile}`);
|
103
|
-
}
|
104
|
-
} else {
|
105
|
-
console.log('ā ļø Package hooks directory not found, skipping hook installation');
|
106
|
-
}
|
107
|
-
|
108
|
-
// Copy README.md from project root to user directory
|
109
|
-
const projectReadmePath = path.join(__dirname, '..', 'README.md');
|
110
|
-
const userReadmePath = path.join(userDir, 'README.md');
|
111
|
-
|
112
|
-
if (fs.existsSync(projectReadmePath)) {
|
113
|
-
fs.copyFileSync(projectReadmePath, userReadmePath);
|
114
|
-
console.log('ā
Copied README.md');
|
115
|
-
}
|
116
|
-
}
|
117
45
|
|
118
46
|
// Automatically configure Claude Code settings
|
119
47
|
async function configureClaudeCodeSettings() {
|
@@ -141,7 +69,7 @@ async function configureClaudeCodeSettings() {
|
|
141
69
|
}
|
142
70
|
}
|
143
71
|
|
144
|
-
// Add hook configuration
|
72
|
+
// Add hook configuration with inline commands
|
145
73
|
const hookConfig = {
|
146
74
|
"Stop": [
|
147
75
|
{
|
@@ -149,7 +77,7 @@ async function configureClaudeCodeSettings() {
|
|
149
77
|
"hooks": [
|
150
78
|
{
|
151
79
|
"type": "command",
|
152
|
-
"command": "
|
80
|
+
"command": "curl -s -X POST \"http://localhost:${MCP_VOICE_HOOKS_PORT:-5111}/api/hooks/stop\" || echo '{\"decision\": \"approve\", \"reason\": \"voice-hooks unavailable\"}'"
|
153
81
|
}
|
154
82
|
]
|
155
83
|
}
|
@@ -160,7 +88,7 @@ async function configureClaudeCodeSettings() {
|
|
160
88
|
"hooks": [
|
161
89
|
{
|
162
90
|
"type": "command",
|
163
|
-
"command": "
|
91
|
+
"command": "curl -s -X POST \"http://localhost:${MCP_VOICE_HOOKS_PORT:-5111}/api/hooks/pre-tool\" || echo '{\"decision\": \"approve\", \"reason\": \"voice-hooks unavailable\"}'"
|
164
92
|
}
|
165
93
|
]
|
166
94
|
},
|
@@ -169,7 +97,7 @@ async function configureClaudeCodeSettings() {
|
|
169
97
|
"hooks": [
|
170
98
|
{
|
171
99
|
"type": "command",
|
172
|
-
"command": "
|
100
|
+
"command": "curl -s -X POST \"http://localhost:${MCP_VOICE_HOOKS_PORT:-5111}/api/hooks/pre-speak\" || echo '{\"decision\": \"approve\", \"reason\": \"voice-hooks unavailable\"}'"
|
173
101
|
}
|
174
102
|
]
|
175
103
|
},
|
@@ -178,7 +106,7 @@ async function configureClaudeCodeSettings() {
|
|
178
106
|
"hooks": [
|
179
107
|
{
|
180
108
|
"type": "command",
|
181
|
-
"command": "
|
109
|
+
"command": "curl -s -X POST \"http://localhost:${MCP_VOICE_HOOKS_PORT:-5111}/api/hooks/pre-wait\" || echo '{\"decision\": \"approve\", \"reason\": \"voice-hooks unavailable\"}'"
|
182
110
|
}
|
183
111
|
]
|
184
112
|
}
|
@@ -204,19 +132,10 @@ async function configureClaudeCodeSettings() {
|
|
204
132
|
|
205
133
|
// Silent hook installation check - runs on every startup
|
206
134
|
async function ensureHooksInstalled() {
|
207
|
-
const userDir = path.join(os.homedir(), '.mcp-voice-hooks');
|
208
|
-
const hooksDir = path.join(userDir, 'hooks');
|
209
|
-
|
210
135
|
try {
|
211
136
|
console.log('š Updating hooks to latest version...');
|
212
137
|
|
213
|
-
//
|
214
|
-
if (fs.existsSync(hooksDir)) {
|
215
|
-
fs.rmSync(hooksDir, { recursive: true, force: true });
|
216
|
-
}
|
217
|
-
|
218
|
-
// Recreate with latest hooks
|
219
|
-
await ensureUserDirectorySetup();
|
138
|
+
// Update hooks configuration in settings.json
|
220
139
|
await configureClaudeCodeSettings();
|
221
140
|
console.log('ā
Hooks and settings updated');
|
222
141
|
} catch (error) {
|
@@ -259,19 +178,9 @@ async function runMCPServer() {
|
|
259
178
|
|
260
179
|
// Uninstall MCP Voice Hooks
|
261
180
|
async function uninstall() {
|
262
|
-
const userDir = path.join(os.homedir(), '.mcp-voice-hooks');
|
263
181
|
const claudeSettingsPath = path.join(process.cwd(), '.claude', 'settings.json');
|
264
182
|
|
265
|
-
//
|
266
|
-
if (fs.existsSync(userDir)) {
|
267
|
-
console.log('š Removing user directory:', userDir);
|
268
|
-
fs.rmSync(userDir, { recursive: true, force: true });
|
269
|
-
console.log('ā
Removed ~/.mcp-voice-hooks');
|
270
|
-
} else {
|
271
|
-
console.log('ā¹ļø ~/.mcp-voice-hooks directory not found');
|
272
|
-
}
|
273
|
-
|
274
|
-
// Step 2: Remove voice hooks from Claude settings
|
183
|
+
// Remove voice hooks from Claude settings
|
275
184
|
if (fs.existsSync(claudeSettingsPath)) {
|
276
185
|
try {
|
277
186
|
console.log('āļø Removing voice hooks from Claude settings...');
|
@@ -303,6 +212,7 @@ async function uninstall() {
|
|
303
212
|
console.log('ā¹ļø No Claude settings file found in current project');
|
304
213
|
}
|
305
214
|
|
215
|
+
|
306
216
|
console.log('\nā
Uninstallation complete!');
|
307
217
|
console.log('š MCP Voice Hooks has been removed.');
|
308
218
|
}
|
package/dist/hook-merger.d.ts
CHANGED
@@ -10,7 +10,7 @@ interface HookSettings {
|
|
10
10
|
[hookType: string]: HookConfig[];
|
11
11
|
}
|
12
12
|
/**
|
13
|
-
* Removes any existing voice hooks that
|
13
|
+
* Removes any existing voice hooks that use MCP_VOICE_HOOKS_PORT
|
14
14
|
* @param hooks - The current hooks configuration
|
15
15
|
* @returns The hooks with voice hooks removed
|
16
16
|
*/
|
package/dist/hook-merger.js
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
// src/hook-merger.ts
|
2
2
|
function removeVoiceHooks(hooks = {}) {
|
3
3
|
const cleaned = {};
|
4
|
-
const voiceHookPattern =
|
4
|
+
const voiceHookPattern = /MCP_VOICE_HOOKS_PORT/;
|
5
5
|
for (const [hookType, hookArray] of Object.entries(hooks)) {
|
6
6
|
cleaned[hookType] = hookArray.filter((hookConfig) => {
|
7
7
|
return !hookConfig.hooks.some((hook) => voiceHookPattern.test(hook.command));
|
package/dist/hook-merger.js.map
CHANGED
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"sources":["../src/hook-merger.ts"],"sourcesContent":["interface Hook {\n type: string;\n command: string;\n}\n\ninterface HookConfig {\n matcher: string;\n hooks: Hook[];\n}\n\nexport interface HookSettings {\n [hookType: string]: HookConfig[];\n}\n\n/**\n * Removes any existing voice hooks that
|
1
|
+
{"version":3,"sources":["../src/hook-merger.ts"],"sourcesContent":["interface Hook {\n type: string;\n command: string;\n}\n\ninterface HookConfig {\n matcher: string;\n hooks: Hook[];\n}\n\nexport interface HookSettings {\n [hookType: string]: HookConfig[];\n}\n\n/**\n * Removes any existing voice hooks that use MCP_VOICE_HOOKS_PORT\n * @param hooks - The current hooks configuration\n * @returns The hooks with voice hooks removed\n */\nexport function removeVoiceHooks(hooks: HookSettings = {}): HookSettings {\n const cleaned: HookSettings = {};\n // Pattern to match voice hooks by the unique environment variable\n const voiceHookPattern = /MCP_VOICE_HOOKS_PORT/;\n \n for (const [hookType, hookArray] of Object.entries(hooks)) {\n cleaned[hookType] = hookArray.filter(hookConfig => {\n // Keep this hook config only if none of its commands match our pattern\n return !hookConfig.hooks.some(hook => voiceHookPattern.test(hook.command));\n });\n \n // Remove empty arrays\n if (cleaned[hookType].length === 0) {\n delete cleaned[hookType];\n }\n }\n \n return cleaned;\n}\n\n/**\n * Replaces voice hooks - removes any existing ones and adds new ones\n * @param existingHooks - The current hooks configuration\n * @param voiceHooks - The voice hooks to add\n * @returns The updated hooks configuration\n */\nexport function replaceVoiceHooks(existingHooks: HookSettings = {}, voiceHooks: HookSettings): HookSettings {\n // First, remove any existing voice hooks\n const cleaned = removeVoiceHooks(existingHooks);\n \n // Then merge in the new voice hooks\n const result: HookSettings = JSON.parse(JSON.stringify(cleaned)); // Deep clone\n \n for (const [hookType, hookArray] of Object.entries(voiceHooks)) {\n if (!result[hookType]) {\n result[hookType] = hookArray;\n } else {\n result[hookType].push(...hookArray);\n }\n }\n \n return result;\n}\n\n/**\n * Checks if two hook settings are semantically equal (ignoring order)\n * @param hooks1 - First hooks configuration\n * @param hooks2 - Second hooks configuration\n * @returns True if they contain the same hooks regardless of order\n */\nexport function areHooksEqual(hooks1: HookSettings = {}, hooks2: HookSettings = {}): boolean {\n const types1 = Object.keys(hooks1).sort();\n const types2 = Object.keys(hooks2).sort();\n \n // Different hook types\n if (types1.join(',') !== types2.join(',')) {\n return false;\n }\n \n // Check each hook type\n for (const hookType of types1) {\n const configs1 = hooks1[hookType];\n const configs2 = hooks2[hookType];\n \n if (configs1.length !== configs2.length) {\n return false;\n }\n \n // Create normalized strings for comparison\n const normalized1 = configs1\n .map(config => JSON.stringify(config))\n .sort();\n const normalized2 = configs2\n .map(config => JSON.stringify(config))\n .sort();\n \n // Compare sorted arrays\n for (let i = 0; i < normalized1.length; i++) {\n if (normalized1[i] !== normalized2[i]) {\n return false;\n }\n }\n }\n \n return true;\n}"],"mappings":";AAmBO,SAAS,iBAAiB,QAAsB,CAAC,GAAiB;AACvE,QAAM,UAAwB,CAAC;AAE/B,QAAM,mBAAmB;AAEzB,aAAW,CAAC,UAAU,SAAS,KAAK,OAAO,QAAQ,KAAK,GAAG;AACzD,YAAQ,QAAQ,IAAI,UAAU,OAAO,gBAAc;AAEjD,aAAO,CAAC,WAAW,MAAM,KAAK,UAAQ,iBAAiB,KAAK,KAAK,OAAO,CAAC;AAAA,IAC3E,CAAC;AAGD,QAAI,QAAQ,QAAQ,EAAE,WAAW,GAAG;AAClC,aAAO,QAAQ,QAAQ;AAAA,IACzB;AAAA,EACF;AAEA,SAAO;AACT;AAQO,SAAS,kBAAkB,gBAA8B,CAAC,GAAG,YAAwC;AAE1G,QAAM,UAAU,iBAAiB,aAAa;AAG9C,QAAM,SAAuB,KAAK,MAAM,KAAK,UAAU,OAAO,CAAC;AAE/D,aAAW,CAAC,UAAU,SAAS,KAAK,OAAO,QAAQ,UAAU,GAAG;AAC9D,QAAI,CAAC,OAAO,QAAQ,GAAG;AACrB,aAAO,QAAQ,IAAI;AAAA,IACrB,OAAO;AACL,aAAO,QAAQ,EAAE,KAAK,GAAG,SAAS;AAAA,IACpC;AAAA,EACF;AAEA,SAAO;AACT;AAQO,SAAS,cAAc,SAAuB,CAAC,GAAG,SAAuB,CAAC,GAAY;AAC3F,QAAM,SAAS,OAAO,KAAK,MAAM,EAAE,KAAK;AACxC,QAAM,SAAS,OAAO,KAAK,MAAM,EAAE,KAAK;AAGxC,MAAI,OAAO,KAAK,GAAG,MAAM,OAAO,KAAK,GAAG,GAAG;AACzC,WAAO;AAAA,EACT;AAGA,aAAW,YAAY,QAAQ;AAC7B,UAAM,WAAW,OAAO,QAAQ;AAChC,UAAM,WAAW,OAAO,QAAQ;AAEhC,QAAI,SAAS,WAAW,SAAS,QAAQ;AACvC,aAAO;AAAA,IACT;AAGA,UAAM,cAAc,SACjB,IAAI,YAAU,KAAK,UAAU,MAAM,CAAC,EACpC,KAAK;AACR,UAAM,cAAc,SACjB,IAAI,YAAU,KAAK,UAAU,MAAM,CAAC,EACpC,KAAK;AAGR,aAAS,IAAI,GAAG,IAAI,YAAY,QAAQ,KAAK;AAC3C,UAAI,YAAY,CAAC,MAAM,YAAY,CAAC,GAAG;AACrC,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;","names":[]}
|