mcp-voice-hooks 1.0.2 → 1.0.6
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/hooks/pre-tool-hook.sh +4 -1
- package/.claude/hooks/stop-hook.sh +4 -1
- package/README.md +43 -16
- package/bin/cli.js +156 -5
- package/dist/hook-merger.d.ts +33 -0
- package/dist/hook-merger.js +54 -0
- package/dist/hook-merger.js.map +1 -0
- package/package.json +1 -1
- package/public/app.js +0 -3
@@ -3,8 +3,11 @@
|
|
3
3
|
# Pre-Tool Hook - Checks for pending utterances before allowing tool execution
|
4
4
|
# Forces Claude to use dequeue_utterances tool if there are pending utterances
|
5
5
|
|
6
|
+
# Get port from environment variable or use default
|
7
|
+
PORT="${MCP_VOICE_HOOKS_PORT:-5111}"
|
8
|
+
|
6
9
|
# Check has-pending-utterances endpoint
|
7
|
-
response=$(curl -s http://localhost
|
10
|
+
response=$(curl -s http://localhost:${PORT}/api/has-pending-utterances 2>/dev/null)
|
8
11
|
|
9
12
|
if [ $? -ne 0 ]; then
|
10
13
|
# Server not available, allow tool execution
|
@@ -3,8 +3,11 @@
|
|
3
3
|
# Stop Hook - Intelligently decides whether to wait for voice input
|
4
4
|
# Checks if there have been any utterances since the last timeout
|
5
5
|
|
6
|
+
# Get port from environment variable or use default
|
7
|
+
PORT="${MCP_VOICE_HOOKS_PORT:-5111}"
|
8
|
+
|
6
9
|
# Check should-wait endpoint
|
7
|
-
response=$(curl -s http://localhost
|
10
|
+
response=$(curl -s http://localhost:${PORT}/api/should-wait 2>/dev/null)
|
8
11
|
|
9
12
|
if [ $? -ne 0 ]; then
|
10
13
|
# Server not available, allow stop
|
package/README.md
CHANGED
@@ -23,17 +23,7 @@ mcp-voice-hooks enables continuous voice conversations with AI assistants by:
|
|
23
23
|
|
24
24
|
## Installation in Your Own Project
|
25
25
|
|
26
|
-
1. **
|
27
|
-
|
28
|
-
```bash
|
29
|
-
npx mcp-voice-hooks install-hooks
|
30
|
-
```
|
31
|
-
|
32
|
-
This will:
|
33
|
-
- Install hook scripts to `~/.mcp-voice-hooks/hooks/`
|
34
|
-
- Configure your project's `.claude/settings.json`
|
35
|
-
|
36
|
-
2. **Add the MCP server**:
|
26
|
+
1. **Add the MCP server**:
|
37
27
|
|
38
28
|
Run the following command to automatically add the MCP server to your current project in `~/.claude.json`:
|
39
29
|
|
@@ -56,13 +46,17 @@ mcp-voice-hooks enables continuous voice conversations with AI assistants by:
|
|
56
46
|
}
|
57
47
|
```
|
58
48
|
|
59
|
-
|
49
|
+
2. **Start Claude Code**:
|
60
50
|
|
61
51
|
```bash
|
62
52
|
claude
|
63
53
|
```
|
64
54
|
|
65
|
-
|
55
|
+
3. **Open the voice interface** at <http://localhost:5111> and start speaking!
|
56
|
+
|
57
|
+
The hooks are automatically installed when the MCP server starts. You need to send one text message to Claude to trigger the voice hooks.
|
58
|
+
|
59
|
+
**Note**: After the first-time installation, you may need to restart Claude for the hooks to take effect.
|
66
60
|
|
67
61
|
The default port is 5111. To use a different port, add to your project's `.claude/settings.json`:
|
68
62
|
|
@@ -74,13 +68,45 @@ mcp-voice-hooks enables continuous voice conversations with AI assistants by:
|
|
74
68
|
}
|
75
69
|
```
|
76
70
|
|
71
|
+
## Manual Hook Installation
|
72
|
+
|
73
|
+
The hooks are automatically installed/updated when the MCP server starts. However, if you need to manually install or reconfigure the hooks:
|
74
|
+
|
75
|
+
```bash
|
76
|
+
npx mcp-voice-hooks install-hooks
|
77
|
+
```
|
78
|
+
|
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`
|
84
|
+
|
85
|
+
## Uninstallation
|
86
|
+
|
87
|
+
To completely remove MCP Voice Hooks:
|
88
|
+
|
89
|
+
```bash
|
90
|
+
# Remove hooks and settings
|
91
|
+
npx mcp-voice-hooks uninstall
|
92
|
+
|
93
|
+
# Also remove from Claude MCP servers
|
94
|
+
claude mcp remove voice-hooks
|
95
|
+
```
|
96
|
+
|
97
|
+
This will:
|
98
|
+
|
99
|
+
- Remove the `~/.mcp-voice-hooks` directory
|
100
|
+
- Clean up voice hooks from your project's `.claude/settings.json`
|
101
|
+
- Preserve any custom hooks you've added
|
102
|
+
|
77
103
|
## Development Mode
|
78
104
|
|
79
105
|
If you're developing mcp-voice-hooks itself:
|
80
106
|
|
81
107
|
```bash
|
82
108
|
# 1. Clone the repository
|
83
|
-
git clone https://github.com/
|
109
|
+
git clone https://github.com/johnmatthewtennant/mcp-voice-hooks.git
|
84
110
|
cd mcp-voice-hooks
|
85
111
|
|
86
112
|
# 2. Install dependencies
|
@@ -99,8 +125,9 @@ claude
|
|
99
125
|
**Important**: When developing with `npm link`:
|
100
126
|
|
101
127
|
- Claude runs the compiled JavaScript from the `dist` folder, not your TypeScript source
|
102
|
-
- After making changes to
|
103
|
-
-
|
128
|
+
- After making changes to **TypeScript files** (`src/*.ts`), you must run `npm run build`
|
129
|
+
- For changes to **browser files** (`public/*`), just restart Claude Code
|
130
|
+
- Then restart Claude Code to use the updated code
|
104
131
|
|
105
132
|
### Hot Reload
|
106
133
|
|
package/bin/cli.js
CHANGED
@@ -5,6 +5,7 @@ import path from 'path';
|
|
5
5
|
import os from 'os';
|
6
6
|
import { spawn } from 'child_process';
|
7
7
|
import { fileURLToPath } from 'url';
|
8
|
+
import { replaceVoiceHooks, areHooksEqual } from '../dist/hook-merger.js';
|
8
9
|
|
9
10
|
const __filename = fileURLToPath(import.meta.url);
|
10
11
|
const __dirname = path.dirname(__filename);
|
@@ -26,10 +27,16 @@ async function main() {
|
|
26
27
|
|
27
28
|
console.log('\n✅ Installation complete!');
|
28
29
|
console.log('📝 To start the server, run: npx mcp-voice-hooks');
|
30
|
+
} else if (command === 'uninstall') {
|
31
|
+
console.log('🗑️ Uninstalling MCP Voice Hooks...');
|
32
|
+
await uninstall();
|
29
33
|
} else {
|
30
|
-
// Default behavior:
|
34
|
+
// Default behavior: ensure hooks are installed/updated, then run the MCP server
|
31
35
|
console.log('🎤 MCP Voice Hooks - Starting server...');
|
32
|
-
|
36
|
+
|
37
|
+
// Auto-install/update hooks on every startup
|
38
|
+
await ensureHooksInstalled();
|
39
|
+
|
33
40
|
console.log('');
|
34
41
|
await runMCPServer();
|
35
42
|
}
|
@@ -46,7 +53,30 @@ async function ensureUserDirectorySetup() {
|
|
46
53
|
|
47
54
|
console.log('📁 Setting up user directory:', userDir);
|
48
55
|
|
49
|
-
//
|
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
|
50
80
|
if (!fs.existsSync(userDir)) {
|
51
81
|
fs.mkdirSync(userDir, { recursive: true });
|
52
82
|
console.log('✅ Created user directory');
|
@@ -74,6 +104,15 @@ async function ensureUserDirectorySetup() {
|
|
74
104
|
} else {
|
75
105
|
console.log('⚠️ Package hooks directory not found, skipping hook installation');
|
76
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
|
+
}
|
77
116
|
}
|
78
117
|
|
79
118
|
// Automatically configure Claude Code settings
|
@@ -139,14 +178,76 @@ async function configureClaudeCodeSettings() {
|
|
139
178
|
]
|
140
179
|
};
|
141
180
|
|
142
|
-
//
|
143
|
-
settings.hooks
|
181
|
+
// Replace voice hooks intelligently
|
182
|
+
const updatedHooks = replaceVoiceHooks(settings.hooks || {}, hookConfig);
|
183
|
+
|
184
|
+
// Check if hooks actually changed (ignoring order)
|
185
|
+
if (areHooksEqual(settings.hooks || {}, updatedHooks)) {
|
186
|
+
console.log('✅ Claude settings already up to date');
|
187
|
+
return;
|
188
|
+
}
|
189
|
+
|
190
|
+
// Update settings with new hooks
|
191
|
+
settings.hooks = updatedHooks;
|
144
192
|
|
145
193
|
// Write settings back
|
146
194
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
147
195
|
console.log('✅ Updated project Claude Code settings');
|
148
196
|
}
|
149
197
|
|
198
|
+
// Silent hook installation check - runs on every startup
|
199
|
+
async function ensureHooksInstalled() {
|
200
|
+
const userDir = path.join(os.homedir(), '.mcp-voice-hooks');
|
201
|
+
const hooksDir = path.join(userDir, 'hooks');
|
202
|
+
const packageHooksDir = path.join(__dirname, '..', '.claude', 'hooks');
|
203
|
+
|
204
|
+
try {
|
205
|
+
// Only create directories if they don't exist
|
206
|
+
if (!fs.existsSync(hooksDir)) {
|
207
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
208
|
+
console.log('🔧 Installing hooks for first time...');
|
209
|
+
}
|
210
|
+
|
211
|
+
// Check if hooks need updating
|
212
|
+
let needsUpdate = false;
|
213
|
+
if (fs.existsSync(packageHooksDir)) {
|
214
|
+
const hookFiles = fs.readdirSync(packageHooksDir).filter(file => file.endsWith('.sh'));
|
215
|
+
|
216
|
+
for (const hookFile of hookFiles) {
|
217
|
+
const sourcePath = path.join(packageHooksDir, hookFile);
|
218
|
+
const destPath = path.join(hooksDir, hookFile);
|
219
|
+
|
220
|
+
// Check if hook doesn't exist or is different
|
221
|
+
if (!fs.existsSync(destPath)) {
|
222
|
+
needsUpdate = true;
|
223
|
+
break;
|
224
|
+
}
|
225
|
+
|
226
|
+
const sourceContent = fs.readFileSync(sourcePath, 'utf8');
|
227
|
+
const destContent = fs.readFileSync(destPath, 'utf8');
|
228
|
+
|
229
|
+
if (sourceContent !== destContent) {
|
230
|
+
needsUpdate = true;
|
231
|
+
break;
|
232
|
+
}
|
233
|
+
}
|
234
|
+
}
|
235
|
+
|
236
|
+
if (needsUpdate) {
|
237
|
+
console.log('🔄 Updating hooks to latest version...');
|
238
|
+
await ensureUserDirectorySetup();
|
239
|
+
await configureClaudeCodeSettings();
|
240
|
+
console.log('✅ Hooks and settings updated');
|
241
|
+
} else {
|
242
|
+
// Even if hooks are up to date, ensure settings are correct
|
243
|
+
await configureClaudeCodeSettings();
|
244
|
+
}
|
245
|
+
} catch (error) {
|
246
|
+
// Silently continue if hooks can't be updated
|
247
|
+
console.warn('⚠️ Could not auto-update hooks:', error.message);
|
248
|
+
}
|
249
|
+
}
|
250
|
+
|
150
251
|
// Run the MCP server
|
151
252
|
async function runMCPServer() {
|
152
253
|
const serverPath = path.join(__dirname, '..', 'dist', 'unified-server.js');
|
@@ -179,6 +280,56 @@ async function runMCPServer() {
|
|
179
280
|
});
|
180
281
|
}
|
181
282
|
|
283
|
+
// Uninstall MCP Voice Hooks
|
284
|
+
async function uninstall() {
|
285
|
+
const userDir = path.join(os.homedir(), '.mcp-voice-hooks');
|
286
|
+
const claudeSettingsPath = path.join(process.cwd(), '.claude', 'settings.json');
|
287
|
+
|
288
|
+
// Step 1: Remove ~/.mcp-voice-hooks directory
|
289
|
+
if (fs.existsSync(userDir)) {
|
290
|
+
console.log('📁 Removing user directory:', userDir);
|
291
|
+
fs.rmSync(userDir, { recursive: true, force: true });
|
292
|
+
console.log('✅ Removed ~/.mcp-voice-hooks');
|
293
|
+
} else {
|
294
|
+
console.log('ℹ️ ~/.mcp-voice-hooks directory not found');
|
295
|
+
}
|
296
|
+
|
297
|
+
// Step 2: Remove voice hooks from Claude settings
|
298
|
+
if (fs.existsSync(claudeSettingsPath)) {
|
299
|
+
try {
|
300
|
+
console.log('⚙️ Removing voice hooks from Claude settings...');
|
301
|
+
|
302
|
+
const settingsContent = fs.readFileSync(claudeSettingsPath, 'utf8');
|
303
|
+
const settings = JSON.parse(settingsContent);
|
304
|
+
|
305
|
+
if (settings.hooks) {
|
306
|
+
// Remove voice hooks
|
307
|
+
const cleanedHooks = removeVoiceHooks(settings.hooks);
|
308
|
+
|
309
|
+
if (Object.keys(cleanedHooks).length === 0) {
|
310
|
+
// If no hooks remain, remove the hooks property entirely
|
311
|
+
delete settings.hooks;
|
312
|
+
} else {
|
313
|
+
settings.hooks = cleanedHooks;
|
314
|
+
}
|
315
|
+
|
316
|
+
// Write updated settings
|
317
|
+
fs.writeFileSync(claudeSettingsPath, JSON.stringify(settings, null, 2));
|
318
|
+
console.log('✅ Removed voice hooks from Claude settings');
|
319
|
+
} else {
|
320
|
+
console.log('ℹ️ No hooks found in Claude settings');
|
321
|
+
}
|
322
|
+
} catch (error) {
|
323
|
+
console.log('⚠️ Could not update Claude settings:', error.message);
|
324
|
+
}
|
325
|
+
} else {
|
326
|
+
console.log('ℹ️ No Claude settings file found in current project');
|
327
|
+
}
|
328
|
+
|
329
|
+
console.log('\n✅ Uninstallation complete!');
|
330
|
+
console.log('👋 MCP Voice Hooks has been removed.');
|
331
|
+
}
|
332
|
+
|
182
333
|
// Run the main function
|
183
334
|
main().catch(error => {
|
184
335
|
console.error('❌ Unexpected error:', error);
|
@@ -0,0 +1,33 @@
|
|
1
|
+
interface Hook {
|
2
|
+
type: string;
|
3
|
+
command: string;
|
4
|
+
}
|
5
|
+
interface HookConfig {
|
6
|
+
matcher: string;
|
7
|
+
hooks: Hook[];
|
8
|
+
}
|
9
|
+
interface HookSettings {
|
10
|
+
[hookType: string]: HookConfig[];
|
11
|
+
}
|
12
|
+
/**
|
13
|
+
* Removes any existing voice hooks that reference our directory
|
14
|
+
* @param hooks - The current hooks configuration
|
15
|
+
* @returns The hooks with voice hooks removed
|
16
|
+
*/
|
17
|
+
declare function removeVoiceHooks(hooks?: HookSettings): HookSettings;
|
18
|
+
/**
|
19
|
+
* Replaces voice hooks - removes any existing ones and adds new ones
|
20
|
+
* @param existingHooks - The current hooks configuration
|
21
|
+
* @param voiceHooks - The voice hooks to add
|
22
|
+
* @returns The updated hooks configuration
|
23
|
+
*/
|
24
|
+
declare function replaceVoiceHooks(existingHooks: HookSettings | undefined, voiceHooks: HookSettings): HookSettings;
|
25
|
+
/**
|
26
|
+
* Checks if two hook settings are semantically equal (ignoring order)
|
27
|
+
* @param hooks1 - First hooks configuration
|
28
|
+
* @param hooks2 - Second hooks configuration
|
29
|
+
* @returns True if they contain the same hooks regardless of order
|
30
|
+
*/
|
31
|
+
declare function areHooksEqual(hooks1?: HookSettings, hooks2?: HookSettings): boolean;
|
32
|
+
|
33
|
+
export { type HookSettings, areHooksEqual, removeVoiceHooks, replaceVoiceHooks };
|
@@ -0,0 +1,54 @@
|
|
1
|
+
// src/hook-merger.ts
|
2
|
+
function removeVoiceHooks(hooks = {}) {
|
3
|
+
const cleaned = {};
|
4
|
+
const voiceHookPattern = /\.mcp-voice-hooks/;
|
5
|
+
for (const [hookType, hookArray] of Object.entries(hooks)) {
|
6
|
+
cleaned[hookType] = hookArray.filter((hookConfig) => {
|
7
|
+
return !hookConfig.hooks.some((hook) => voiceHookPattern.test(hook.command));
|
8
|
+
});
|
9
|
+
if (cleaned[hookType].length === 0) {
|
10
|
+
delete cleaned[hookType];
|
11
|
+
}
|
12
|
+
}
|
13
|
+
return cleaned;
|
14
|
+
}
|
15
|
+
function replaceVoiceHooks(existingHooks = {}, voiceHooks) {
|
16
|
+
const cleaned = removeVoiceHooks(existingHooks);
|
17
|
+
const result = JSON.parse(JSON.stringify(cleaned));
|
18
|
+
for (const [hookType, hookArray] of Object.entries(voiceHooks)) {
|
19
|
+
if (!result[hookType]) {
|
20
|
+
result[hookType] = hookArray;
|
21
|
+
} else {
|
22
|
+
result[hookType].push(...hookArray);
|
23
|
+
}
|
24
|
+
}
|
25
|
+
return result;
|
26
|
+
}
|
27
|
+
function areHooksEqual(hooks1 = {}, hooks2 = {}) {
|
28
|
+
const types1 = Object.keys(hooks1).sort();
|
29
|
+
const types2 = Object.keys(hooks2).sort();
|
30
|
+
if (types1.join(",") !== types2.join(",")) {
|
31
|
+
return false;
|
32
|
+
}
|
33
|
+
for (const hookType of types1) {
|
34
|
+
const configs1 = hooks1[hookType];
|
35
|
+
const configs2 = hooks2[hookType];
|
36
|
+
if (configs1.length !== configs2.length) {
|
37
|
+
return false;
|
38
|
+
}
|
39
|
+
const normalized1 = configs1.map((config) => JSON.stringify(config)).sort();
|
40
|
+
const normalized2 = configs2.map((config) => JSON.stringify(config)).sort();
|
41
|
+
for (let i = 0; i < normalized1.length; i++) {
|
42
|
+
if (normalized1[i] !== normalized2[i]) {
|
43
|
+
return false;
|
44
|
+
}
|
45
|
+
}
|
46
|
+
}
|
47
|
+
return true;
|
48
|
+
}
|
49
|
+
export {
|
50
|
+
areHooksEqual,
|
51
|
+
removeVoiceHooks,
|
52
|
+
replaceVoiceHooks
|
53
|
+
};
|
54
|
+
//# sourceMappingURL=hook-merger.js.map
|
@@ -0,0 +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 reference our directory\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 const voiceHookPattern = /\\.mcp-voice-hooks/;\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 reference our directory\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;AAC/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":[]}
|
package/package.json
CHANGED
package/public/app.js
CHANGED