Skip to main content
Glama

mcp-voice-hooks

settings-migration.test.ts10.6 kB
import fs from 'fs'; import path from 'path'; import { execSync } from 'child_process'; describe('Settings Migration', () => { const testDir = path.join(process.cwd(), 'test-project'); const claudeDir = path.join(testDir, '.claude'); const settingsPath = path.join(claudeDir, 'settings.json'); const settingsLocalPath = path.join(claudeDir, 'settings.local.json'); const cliPath = path.join(process.cwd(), 'bin', 'cli.js'); beforeEach(() => { // Clean up test directory if (fs.existsSync(testDir)) { fs.rmSync(testDir, { recursive: true }); } fs.mkdirSync(testDir, { recursive: true }); }); afterEach(() => { // Clean up after tests if (fs.existsSync(testDir)) { fs.rmSync(testDir, { recursive: true }); } }); describe('install-hooks', () => { it('should create settings.local.json when no settings exist', () => { // Run install-hooks execSync(`node ${cliPath} install-hooks`, { cwd: testDir }); // Check that settings.local.json was created expect(fs.existsSync(settingsLocalPath)).toBe(true); expect(fs.existsSync(settingsPath)).toBe(false); // Verify hooks were added const settings = JSON.parse(fs.readFileSync(settingsLocalPath, 'utf8')); expect(settings.hooks).toBeDefined(); expect(settings.hooks.Stop).toBeDefined(); expect(settings.hooks.PreToolUse).toBeDefined(); expect(settings.hooks.PostToolUse).toBeDefined(); }); it('should migrate hooks from settings.json to settings.local.json', () => { // Create old settings.json with voice hooks and custom hooks fs.mkdirSync(claudeDir, { recursive: true }); const oldSettings = { hooks: { Stop: [{ matcher: "", hooks: [{ type: "command", command: "curl -s -X POST \"http://localhost:${MCP_VOICE_HOOKS_PORT:-5111}/api/hooks/stop\" || echo '{\"decision\": \"approve\", \"reason\": \"voice-hooks not running\"}'" }] }], PreToolUse: [{ matcher: ".*", hooks: [{ type: "command", command: "echo 'custom hook'" // This is a custom hook, not a voice hook }] }, { matcher: "^(?!mcp__voice-hooks__).*", hooks: [{ type: "command", command: "curl -s -X POST \"http://localhost:${MCP_VOICE_HOOKS_PORT:-5111}/api/hooks/pre-tool\" || echo '{\"decision\": \"approve\", \"reason\": \"voice-hooks not running\"}'" }] }] } }; fs.writeFileSync(settingsPath, JSON.stringify(oldSettings, null, 2)); // Run install-hooks execSync(`node ${cliPath} install-hooks`, { cwd: testDir }); // Check that voice hooks were removed from settings.json, but custom hooks remain const cleanedSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); expect(cleanedSettings.hooks?.Stop).toBeUndefined(); // Voice hooks removed expect(cleanedSettings.hooks?.PreToolUse).toBeDefined(); // Custom hook remains expect(cleanedSettings.hooks.PreToolUse.length).toBe(1); expect(cleanedSettings.hooks.PreToolUse[0].hooks[0].command).toBe("echo 'custom hook'"); // Check that settings.local.json has the voice hooks const newSettings = JSON.parse(fs.readFileSync(settingsLocalPath, 'utf8')); expect(newSettings.hooks).toBeDefined(); expect(newSettings.hooks.Stop).toBeDefined(); expect(newSettings.hooks.PreToolUse).toBeDefined(); expect(newSettings.hooks.PostToolUse).toBeDefined(); }); it('should preserve custom hooks in settings.json when migrating', () => { // Create old settings.json with mixed hooks fs.mkdirSync(claudeDir, { recursive: true }); const oldSettings = { hooks: { Stop: [{ matcher: "", hooks: [{ type: "command", command: "curl -s -X POST \"http://localhost:${MCP_VOICE_HOOKS_PORT:-5111}/api/hooks/stop\" || echo '{\"decision\": \"approve\", \"reason\": \"voice-hooks not running\"}'" }] }], PreToolUse: [{ matcher: "^custom-tool$", hooks: [{ type: "command", command: "echo 'custom hook'" }] }, { matcher: "^(?!mcp__voice-hooks__).*", hooks: [{ type: "command", command: "curl -s -X POST \"http://localhost:${MCP_VOICE_HOOKS_PORT:-5111}/api/hooks/pre-tool\" || echo '{\"decision\": \"approve\", \"reason\": \"voice-hooks not running\"}'" }] }] } }; fs.writeFileSync(settingsPath, JSON.stringify(oldSettings, null, 2)); // Run install-hooks execSync(`node ${cliPath} install-hooks`, { cwd: testDir }); // Check that custom hooks remain in settings.json const cleanedSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); expect(cleanedSettings.hooks.PreToolUse).toBeDefined(); expect(cleanedSettings.hooks.PreToolUse.length).toBe(1); expect(cleanedSettings.hooks.PreToolUse[0].matcher).toBe("^custom-tool$"); expect(cleanedSettings.hooks.Stop).toBeUndefined(); // Voice hooks removed }); it('should update existing settings.local.json hooks', () => { // Create existing settings.local.json with outdated voice hooks fs.mkdirSync(claudeDir, { recursive: true }); const existingSettings = { env: { PORT: "3000" }, hooks: { Stop: [{ matcher: "", hooks: [{ type: "command", command: "curl -s -X POST \"http://localhost:${MCP_VOICE_HOOKS_PORT:-5111}/api/hooks/stop\" || echo 'old version'" }] }] } }; fs.writeFileSync(settingsLocalPath, JSON.stringify(existingSettings, null, 2)); // Run install-hooks execSync(`node ${cliPath} install-hooks`, { cwd: testDir }); // Check that settings were updated const updatedSettings = JSON.parse(fs.readFileSync(settingsLocalPath, 'utf8')); expect(updatedSettings.env.PORT).toBe("3000"); // Preserved expect(updatedSettings.hooks.Stop[0].hooks[0].command).toContain("voice-hooks not running"); // Updated to new version expect(updatedSettings.hooks.PreToolUse).toBeDefined(); // Added expect(updatedSettings.hooks.PostToolUse).toBeDefined(); // Added }); }); describe('uninstall', () => { it('should remove hooks from both settings files', () => { // Create both settings files with voice hooks fs.mkdirSync(claudeDir, { recursive: true }); // settings.json with voice hooks const settings = { hooks: { Stop: [{ matcher: "", hooks: [{ type: "command", command: "curl -s -X POST \"http://localhost:${MCP_VOICE_HOOKS_PORT:-5111}/api/hooks/stop\" || echo '{\"decision\": \"approve\", \"reason\": \"voice-hooks not running\"}'" }] }] } }; fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2)); // settings.local.json with voice hooks const settingsLocal = { env: { PORT: "3000" }, hooks: { Stop: [{ matcher: "", hooks: [{ type: "command", command: "curl -s -X POST \"http://localhost:${MCP_VOICE_HOOKS_PORT:-5111}/api/hooks/stop\" || echo '{\"decision\": \"approve\", \"reason\": \"voice-hooks not running\"}'" }] }], PreToolUse: [{ matcher: "^(?!mcp__voice-hooks__).*", hooks: [{ type: "command", command: "curl -s -X POST \"http://localhost:${MCP_VOICE_HOOKS_PORT:-5111}/api/hooks/pre-tool\" || echo '{\"decision\": \"approve\", \"reason\": \"voice-hooks not running\"}'" }] }] } }; fs.writeFileSync(settingsLocalPath, JSON.stringify(settingsLocal, null, 2)); // Run uninstall execSync(`node ${cliPath} uninstall`, { cwd: testDir }); // Check that hooks were removed from both files const cleanedSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); expect(cleanedSettings.hooks).toBeUndefined(); const cleanedSettingsLocal = JSON.parse(fs.readFileSync(settingsLocalPath, 'utf8')); expect(cleanedSettingsLocal.env.PORT).toBe("3000"); // Preserved expect(cleanedSettingsLocal.hooks).toBeUndefined(); // Removed }); it('should handle missing settings files gracefully', () => { // Run uninstall without any settings files const output = execSync(`node ${cliPath} uninstall`, { cwd: testDir, encoding: 'utf8' }); // Should complete without errors expect(output).toContain('No Claude settings files found'); expect(output).toContain('Uninstallation complete'); }); it('should preserve custom hooks during uninstall', () => { // Create settings with mixed hooks fs.mkdirSync(claudeDir, { recursive: true }); const settings = { hooks: { PreToolUse: [{ matcher: "^custom-tool$", hooks: [{ type: "command", command: "echo 'custom hook'" }] }, { matcher: "^(?!mcp__voice-hooks__).*", hooks: [{ type: "command", command: "curl -s -X POST \"http://localhost:${MCP_VOICE_HOOKS_PORT:-5111}/api/hooks/pre-tool\" || echo '{\"decision\": \"approve\", \"reason\": \"voice-hooks not running\"}'" }] }], Stop: [{ matcher: "", hooks: [{ type: "command", command: "curl -s -X POST \"http://localhost:${MCP_VOICE_HOOKS_PORT:-5111}/api/hooks/stop\" || echo '{\"decision\": \"approve\", \"reason\": \"voice-hooks not running\"}'" }] }] } }; fs.writeFileSync(settingsLocalPath, JSON.stringify(settings, null, 2)); // Run uninstall execSync(`node ${cliPath} uninstall`, { cwd: testDir }); // Check that only custom hooks remain const cleanedSettings = JSON.parse(fs.readFileSync(settingsLocalPath, 'utf8')); expect(cleanedSettings.hooks.PreToolUse).toBeDefined(); expect(cleanedSettings.hooks.PreToolUse.length).toBe(1); expect(cleanedSettings.hooks.PreToolUse[0].matcher).toBe("^custom-tool$"); expect(cleanedSettings.hooks.Stop).toBeUndefined(); }); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/johnmatthewtennant/mcp-voice-hooks'

If you have feedback or need assistance with the MCP directory API, please join our Discord server