@williamzujkowski/live-coding-music-mcp 1.0.0

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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1735 -0
  3. package/dist/AudioAnalyzer.js +678 -0
  4. package/dist/PatternStore.js +169 -0
  5. package/dist/StrudelController.js +676 -0
  6. package/dist/__tests__/MusicTheory.test.js +245 -0
  7. package/dist/__tests__/PatternGenerator.test.js +337 -0
  8. package/dist/__tests__/PatternStore.test.js +153 -0
  9. package/dist/__tests__/utils/MockPlaywright.js +130 -0
  10. package/dist/index.js +21 -0
  11. package/dist/server/EnhancedMCPServer.js +654 -0
  12. package/dist/server/EnhancedMCPServerFixed.js +2506 -0
  13. package/dist/server/server.js +580 -0
  14. package/dist/server/tools/ai.js +530 -0
  15. package/dist/server/tools/analysis.js +132 -0
  16. package/dist/server/tools/capture.js +184 -0
  17. package/dist/server/tools/diagnostics.js +96 -0
  18. package/dist/server/tools/editor.js +128 -0
  19. package/dist/server/tools/generate.js +218 -0
  20. package/dist/server/tools/history.js +196 -0
  21. package/dist/server/tools/playback.js +41 -0
  22. package/dist/server/tools/session.js +115 -0
  23. package/dist/server/tools/storage.js +77 -0
  24. package/dist/server/tools/transform.js +399 -0
  25. package/dist/server/tools/types.js +9 -0
  26. package/dist/services/AudioCaptureService.js +279 -0
  27. package/dist/services/GeminiService.js +790 -0
  28. package/dist/services/MIDIExportService.js +412 -0
  29. package/dist/services/MusicTheory.js +186 -0
  30. package/dist/services/PatternGenerator.js +647 -0
  31. package/dist/services/SessionManager.js +316 -0
  32. package/dist/services/StrudelEngine.js +374 -0
  33. package/dist/services/__mocks__/StrudelEngine.js +157 -0
  34. package/dist/types/AudioAnalysis.js +4 -0
  35. package/dist/types/index.js +1 -0
  36. package/dist/utils/ErrorRecovery.js +248 -0
  37. package/dist/utils/InputValidator.js +281 -0
  38. package/dist/utils/Logger.js +19 -0
  39. package/dist/utils/PatternValidator.js +241 -0
  40. package/dist/utils/PerformanceMonitor.js +128 -0
  41. package/package.json +86 -0
@@ -0,0 +1,169 @@
1
+ import { promises as fs } from 'fs';
2
+ import * as path from 'path';
3
+ import { Logger } from './utils/Logger.js';
4
+ export class PatternStore {
5
+ basePath;
6
+ patternCache = new Map();
7
+ listCache = null;
8
+ LIST_CACHE_TTL = 5000; // 5 seconds
9
+ MAX_CACHE_SIZE = 100; // LRU cache limit
10
+ directoryEnsured = false;
11
+ logger;
12
+ constructor(basePath) {
13
+ this.basePath = basePath;
14
+ this.logger = new Logger();
15
+ this.ensureDirectory();
16
+ }
17
+ /**
18
+ * Sets a value in the cache with LRU eviction
19
+ * Maintains max cache size by evicting oldest entries
20
+ */
21
+ setCacheWithLRU(name, data) {
22
+ // If already exists, delete first to update insertion order (LRU)
23
+ if (this.patternCache.has(name)) {
24
+ this.patternCache.delete(name);
25
+ }
26
+ // Evict oldest entries if at capacity
27
+ while (this.patternCache.size >= this.MAX_CACHE_SIZE) {
28
+ const oldestKey = this.patternCache.keys().next().value;
29
+ if (oldestKey) {
30
+ this.patternCache.delete(oldestKey);
31
+ }
32
+ }
33
+ this.patternCache.set(name, data);
34
+ }
35
+ /**
36
+ * Gets a value from cache and updates its LRU position
37
+ */
38
+ getCacheWithLRU(name) {
39
+ const data = this.patternCache.get(name);
40
+ if (data) {
41
+ // Move to end (most recently used)
42
+ this.patternCache.delete(name);
43
+ this.patternCache.set(name, data);
44
+ }
45
+ return data;
46
+ }
47
+ async ensureDirectory() {
48
+ if (this.directoryEnsured)
49
+ return;
50
+ try {
51
+ await fs.mkdir(this.basePath, { recursive: true });
52
+ this.directoryEnsured = true;
53
+ }
54
+ catch (error) {
55
+ console.error('Failed to create patterns directory:', error);
56
+ }
57
+ }
58
+ async save(name, content, tags = []) {
59
+ await this.ensureDirectory();
60
+ const filename = this.sanitizeFilename(name) + '.json';
61
+ const filepath = path.join(this.basePath, filename);
62
+ const data = {
63
+ name,
64
+ content,
65
+ tags,
66
+ timestamp: new Date().toISOString(),
67
+ };
68
+ // Update cache with LRU eviction
69
+ this.setCacheWithLRU(name, data);
70
+ this.listCache = null; // Invalidate list cache
71
+ // Write file asynchronously
72
+ await fs.writeFile(filepath, JSON.stringify(data, null, 2));
73
+ }
74
+ async load(name) {
75
+ // Check cache first with LRU update
76
+ const cached = this.getCacheWithLRU(name);
77
+ if (cached) {
78
+ return cached;
79
+ }
80
+ const filename = this.sanitizeFilename(name) + '.json';
81
+ const filepath = path.join(this.basePath, filename);
82
+ try {
83
+ const data = await fs.readFile(filepath, 'utf-8');
84
+ const pattern = JSON.parse(data);
85
+ // Update cache with LRU eviction
86
+ this.setCacheWithLRU(name, pattern);
87
+ return pattern;
88
+ }
89
+ catch (error) {
90
+ this.logger.warn(`Failed to load pattern: ${name}`, error);
91
+ return null;
92
+ }
93
+ }
94
+ async list(tag) {
95
+ // Use cached list if available and not expired
96
+ const now = Date.now();
97
+ if (!tag && this.listCache && (now - this.listCache.timestamp) < this.LIST_CACHE_TTL) {
98
+ return this.listCache.patterns;
99
+ }
100
+ try {
101
+ const files = await fs.readdir(this.basePath);
102
+ const patterns = [];
103
+ // Parallel file reading for better performance
104
+ const readPromises = files
105
+ .filter(file => file.endsWith('.json'))
106
+ .map(async (file) => {
107
+ const filepath = path.join(this.basePath, file);
108
+ const data = await fs.readFile(filepath, 'utf-8');
109
+ return JSON.parse(data);
110
+ });
111
+ const allPatterns = await Promise.all(readPromises);
112
+ // Filter and sort
113
+ const filteredPatterns = tag
114
+ ? allPatterns.filter(p => p.tags.includes(tag))
115
+ : allPatterns;
116
+ const sorted = filteredPatterns.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
117
+ // Update cache only for non-filtered lists
118
+ if (!tag) {
119
+ this.listCache = { patterns: sorted, timestamp: now };
120
+ }
121
+ return sorted;
122
+ }
123
+ catch (error) {
124
+ this.logger.warn(`Failed to list patterns${tag ? ` with tag: ${tag}` : ''}`, error);
125
+ return [];
126
+ }
127
+ }
128
+ // Clear cache method
129
+ clearCache() {
130
+ this.patternCache.clear();
131
+ this.listCache = null;
132
+ }
133
+ /**
134
+ * Returns cache statistics for monitoring
135
+ */
136
+ getCacheStats() {
137
+ return {
138
+ size: this.patternCache.size,
139
+ maxSize: this.MAX_CACHE_SIZE,
140
+ listCacheValid: this.listCache !== null &&
141
+ (Date.now() - this.listCache.timestamp) < this.LIST_CACHE_TTL
142
+ };
143
+ }
144
+ /**
145
+ * Sanitizes a pattern name to create a safe filename
146
+ * Prevents path traversal attacks and ensures cross-platform compatibility
147
+ * @param name - The pattern name to sanitize
148
+ * @returns A safe filename without extension
149
+ * @throws Error if the name is invalid or uses reserved names
150
+ */
151
+ sanitizeFilename(name) {
152
+ // Use path.basename to prevent path traversal attacks
153
+ const baseName = path.basename(name);
154
+ // Remove dangerous characters and normalize
155
+ const cleaned = baseName
156
+ .replace(/[^a-z0-9_-]/gi, '_')
157
+ .toLowerCase();
158
+ // Validate length
159
+ if (cleaned.length === 0 || cleaned.length > 255) {
160
+ throw new Error('Pattern name must be between 1 and 255 characters');
161
+ }
162
+ // Prevent reserved filenames on Windows
163
+ const reserved = ['con', 'prn', 'aux', 'nul', 'com1', 'com2', 'com3', 'com4', 'com5', 'com6', 'com7', 'com8', 'com9', 'lpt1', 'lpt2', 'lpt3', 'lpt4', 'lpt5', 'lpt6', 'lpt7', 'lpt8', 'lpt9'];
164
+ if (reserved.includes(cleaned.toLowerCase())) {
165
+ throw new Error('Pattern name uses a reserved filename');
166
+ }
167
+ return cleaned;
168
+ }
169
+ }