@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.
- package/LICENSE +21 -0
- package/README.md +1735 -0
- package/dist/AudioAnalyzer.js +678 -0
- package/dist/PatternStore.js +169 -0
- package/dist/StrudelController.js +676 -0
- package/dist/__tests__/MusicTheory.test.js +245 -0
- package/dist/__tests__/PatternGenerator.test.js +337 -0
- package/dist/__tests__/PatternStore.test.js +153 -0
- package/dist/__tests__/utils/MockPlaywright.js +130 -0
- package/dist/index.js +21 -0
- package/dist/server/EnhancedMCPServer.js +654 -0
- package/dist/server/EnhancedMCPServerFixed.js +2506 -0
- package/dist/server/server.js +580 -0
- package/dist/server/tools/ai.js +530 -0
- package/dist/server/tools/analysis.js +132 -0
- package/dist/server/tools/capture.js +184 -0
- package/dist/server/tools/diagnostics.js +96 -0
- package/dist/server/tools/editor.js +128 -0
- package/dist/server/tools/generate.js +218 -0
- package/dist/server/tools/history.js +196 -0
- package/dist/server/tools/playback.js +41 -0
- package/dist/server/tools/session.js +115 -0
- package/dist/server/tools/storage.js +77 -0
- package/dist/server/tools/transform.js +399 -0
- package/dist/server/tools/types.js +9 -0
- package/dist/services/AudioCaptureService.js +279 -0
- package/dist/services/GeminiService.js +790 -0
- package/dist/services/MIDIExportService.js +412 -0
- package/dist/services/MusicTheory.js +186 -0
- package/dist/services/PatternGenerator.js +647 -0
- package/dist/services/SessionManager.js +316 -0
- package/dist/services/StrudelEngine.js +374 -0
- package/dist/services/__mocks__/StrudelEngine.js +157 -0
- package/dist/types/AudioAnalysis.js +4 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/ErrorRecovery.js +248 -0
- package/dist/utils/InputValidator.js +281 -0
- package/dist/utils/Logger.js +19 -0
- package/dist/utils/PatternValidator.js +241 -0
- package/dist/utils/PerformanceMonitor.js +128 -0
- 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
|
+
}
|