@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,184 @@
1
+ /**
2
+ * capture domain — audio recording and MIDI export.
3
+ *
4
+ * Owns (4 tools): start_audio_capture, stop_audio_capture,
5
+ * capture_audio_sample, export_midi.
6
+ *
7
+ * Per the #110 audit, the three audio_capture tools collapse into a
8
+ * single `audio_capture(action=start|stop|sample)` post-split.
9
+ * `export_midi` stays separate — MIDI is symbolic, audio is waveform.
10
+ *
11
+ * The AudioCaptureService is lazy-instantiated module-side: we need
12
+ * the current Playwright page for `injectRecorder()`, and that only
13
+ * exists after `init`. The singleton survives across tool calls.
14
+ */
15
+ import { InputValidator } from '../../utils/InputValidator.js';
16
+ // AudioCaptureService lifecycle lives on the server; we fetch a shared
17
+ // instance via ctx.getAudioCaptureService() so tests can mock the class
18
+ // at module boundary without the extractor caching a stale instance.
19
+ function blobToBase64(arrayBuffer) {
20
+ const bytes = new Uint8Array(arrayBuffer);
21
+ let binary = '';
22
+ for (let i = 0; i < bytes.length; i++) {
23
+ binary += String.fromCharCode(bytes[i]);
24
+ }
25
+ return btoa(binary);
26
+ }
27
+ export const tools = [
28
+ {
29
+ name: 'start_audio_capture',
30
+ description: 'Start capturing audio from Strudel output. Audio must be playing for capture to work.',
31
+ inputSchema: {
32
+ type: 'object',
33
+ properties: {
34
+ format: { type: 'string', enum: ['webm', 'opus'], description: 'Audio format (default: webm)' },
35
+ maxDuration: { type: 'number', description: 'Maximum capture duration in milliseconds' },
36
+ },
37
+ },
38
+ },
39
+ {
40
+ name: 'stop_audio_capture',
41
+ description: 'Stop audio capture and return the recorded audio as base64-encoded data.',
42
+ inputSchema: { type: 'object', properties: {} },
43
+ },
44
+ {
45
+ name: 'capture_audio_sample',
46
+ description: 'Capture a fixed-duration audio sample from Strudel output. Audio must be playing.',
47
+ inputSchema: {
48
+ type: 'object',
49
+ properties: { duration: { type: 'number', description: 'Duration in milliseconds (default: 5000)' } },
50
+ },
51
+ },
52
+ {
53
+ name: 'export_midi',
54
+ description: 'Export current pattern to MIDI file. Parses note(), n(), and chord() functions.',
55
+ inputSchema: {
56
+ type: 'object',
57
+ properties: {
58
+ filename: { type: 'string', description: 'Output filename (optional, default: pattern.mid)' },
59
+ duration: { type: 'number', description: 'Export duration in bars (default: 4)' },
60
+ bpm: { type: 'number', description: 'Tempo in BPM (default: 120)' },
61
+ format: { type: 'string', enum: ['file', 'base64'], description: 'Output format: file or base64 (default: base64)' },
62
+ },
63
+ },
64
+ },
65
+ ];
66
+ export const toolNames = new Set(tools.map(t => t.name));
67
+ export async function execute(name, args, ctx) {
68
+ switch (name) {
69
+ case 'start_audio_capture':
70
+ return await startAudioCapture(args?.format, args?.maxDuration, ctx);
71
+ case 'stop_audio_capture':
72
+ return await stopAudioCapture(ctx);
73
+ case 'capture_audio_sample':
74
+ return await captureAudioSample(args?.duration, ctx);
75
+ case 'export_midi':
76
+ return await exportMidi(args?.filename, args?.duration, args?.bpm, args?.format, ctx);
77
+ default:
78
+ throw new Error(`capture module does not handle tool: ${name}`);
79
+ }
80
+ }
81
+ async function startAudioCapture(format, _maxDuration, ctx) {
82
+ try {
83
+ const service = await ctx.getAudioCaptureService();
84
+ if (service.isCapturing()) {
85
+ return { success: false, message: 'Audio capture already in progress. Stop it first.' };
86
+ }
87
+ await service.startCapture(ctx.controller.page, { format });
88
+ return {
89
+ success: true,
90
+ message: 'Audio capture started. Use stop_audio_capture to get the recorded audio.',
91
+ format: format || 'webm',
92
+ };
93
+ }
94
+ catch (error) {
95
+ const message = error instanceof Error ? error.message : String(error);
96
+ return { success: false, message: `Failed to start audio capture: ${message}` };
97
+ }
98
+ }
99
+ async function stopAudioCapture(ctx) {
100
+ try {
101
+ const service = await ctx.getAudioCaptureService();
102
+ if (!service.isCapturing()) {
103
+ return { success: false, message: 'No audio capture in progress. Start capture first.' };
104
+ }
105
+ const result = await service.stopCapture(ctx.controller.page);
106
+ const buf = await result.blob.arrayBuffer();
107
+ return {
108
+ success: true,
109
+ message: `Captured ${result.duration}ms of audio`,
110
+ audio: blobToBase64(buf),
111
+ duration: result.duration,
112
+ format: result.format,
113
+ };
114
+ }
115
+ catch (error) {
116
+ const message = error instanceof Error ? error.message : String(error);
117
+ return { success: false, message: `Failed to stop audio capture: ${message}` };
118
+ }
119
+ }
120
+ async function captureAudioSample(duration, ctx) {
121
+ const durationMs = duration || 5000;
122
+ if (durationMs < 100 || durationMs > 60000) {
123
+ return { success: false, message: 'Duration must be between 100ms and 60000ms (1 minute)' };
124
+ }
125
+ try {
126
+ const service = await ctx.getAudioCaptureService();
127
+ if (service.isCapturing()) {
128
+ return { success: false, message: 'Audio capture already in progress. Stop it first.' };
129
+ }
130
+ const result = await service.captureForDuration(ctx.controller.page, durationMs);
131
+ const buf = await result.blob.arrayBuffer();
132
+ return {
133
+ success: true,
134
+ message: `Captured ${result.duration}ms audio sample`,
135
+ audio: blobToBase64(buf),
136
+ duration: result.duration,
137
+ format: result.format,
138
+ };
139
+ }
140
+ catch (error) {
141
+ const message = error instanceof Error ? error.message : String(error);
142
+ return { success: false, message: `Failed to capture audio sample: ${message}` };
143
+ }
144
+ }
145
+ async function exportMidi(filename, bars, bpm, format, ctx) {
146
+ if (bpm !== undefined)
147
+ InputValidator.validateBPM(bpm);
148
+ if (bars !== undefined && (bars < 1 || bars > 128)) {
149
+ return { success: false, message: 'Bars must be between 1 and 128' };
150
+ }
151
+ const pattern = await ctx.getCurrentPatternSafe();
152
+ if (!pattern || pattern.trim().length === 0) {
153
+ return { success: false, message: 'No pattern to export. Write a pattern first.' };
154
+ }
155
+ const exportOptions = { bpm: bpm || 120, bars: bars || 4 };
156
+ const outputFormat = format || 'base64';
157
+ if (outputFormat === 'file') {
158
+ const result = ctx.midiExportService.exportToFile(pattern, filename, exportOptions);
159
+ return {
160
+ success: result.success,
161
+ message: result.success
162
+ ? `Exported ${result.noteCount} notes to ${result.output}`
163
+ : result.error || 'Export failed',
164
+ output: result.output,
165
+ noteCount: result.noteCount,
166
+ bars: result.bars,
167
+ bpm: result.bpm,
168
+ error: result.error,
169
+ };
170
+ }
171
+ const result = ctx.midiExportService.exportToBase64(pattern, exportOptions);
172
+ return {
173
+ success: result.success,
174
+ message: result.success
175
+ ? `Exported ${result.noteCount} notes as base64 MIDI data`
176
+ : result.error || 'Export failed',
177
+ output: result.output,
178
+ noteCount: result.noteCount,
179
+ bars: result.bars,
180
+ bpm: result.bpm,
181
+ error: result.error,
182
+ };
183
+ }
184
+ export const captureModule = { tools, toolNames, execute };
@@ -0,0 +1,96 @@
1
+ /**
2
+ * diagnostics domain — performance, memory, status, errors, screenshots.
3
+ *
4
+ * Owns (6 tools today): performance_report, memory_usage, screenshot,
5
+ * status, diagnostics, show_errors.
6
+ *
7
+ * Post-consolidation target (per #110 audit): one `diagnostics` tool with
8
+ * a `level` enum. For now we keep the individual verbs and only move them
9
+ * out of server.ts; consolidation happens later under #120.
10
+ */
11
+ export const tools = [
12
+ {
13
+ name: 'performance_report',
14
+ description: 'Get performance metrics and bottlenecks',
15
+ inputSchema: { type: 'object', properties: {} },
16
+ },
17
+ {
18
+ name: 'memory_usage',
19
+ description: 'Get current memory usage statistics',
20
+ inputSchema: { type: 'object', properties: {} },
21
+ },
22
+ {
23
+ name: 'screenshot',
24
+ description: 'Take a screenshot of the current Strudel editor state',
25
+ inputSchema: {
26
+ type: 'object',
27
+ properties: {
28
+ filename: { type: 'string', description: 'Optional filename for screenshot' },
29
+ },
30
+ },
31
+ },
32
+ {
33
+ name: 'status',
34
+ description: 'Get current browser and playback status (quick state check)',
35
+ inputSchema: { type: 'object', properties: {} },
36
+ },
37
+ {
38
+ name: 'diagnostics',
39
+ description: 'Get detailed browser diagnostics including cache, errors, and performance',
40
+ inputSchema: { type: 'object', properties: {} },
41
+ },
42
+ {
43
+ name: 'show_errors',
44
+ description: 'Display captured console errors and warnings from Strudel',
45
+ inputSchema: { type: 'object', properties: {} },
46
+ },
47
+ ];
48
+ export const toolNames = new Set(tools.map(t => t.name));
49
+ export async function execute(name, args, ctx) {
50
+ switch (name) {
51
+ case 'performance_report': {
52
+ const report = ctx.perfMonitor.getReport();
53
+ const bottlenecks = ctx.perfMonitor.getBottlenecks(5);
54
+ return `${report}\n\nTop 5 Bottlenecks:\n${JSON.stringify(bottlenecks, null, 2)}`;
55
+ }
56
+ case 'memory_usage': {
57
+ const memory = ctx.perfMonitor.getMemoryUsage();
58
+ return memory ? JSON.stringify(memory, null, 2) : 'Memory usage not available';
59
+ }
60
+ case 'screenshot': {
61
+ if (!ctx.isInitialized()) {
62
+ return 'Browser not initialized. Run init first.';
63
+ }
64
+ return await ctx.controller.takeScreenshot(args?.filename);
65
+ }
66
+ case 'status':
67
+ return ctx.controller.getStatus();
68
+ case 'diagnostics': {
69
+ if (!ctx.isInitialized()) {
70
+ return {
71
+ initialized: false,
72
+ message: 'Browser not initialized. Run init first for full diagnostics.',
73
+ };
74
+ }
75
+ return await ctx.controller.getDiagnostics();
76
+ }
77
+ case 'show_errors': {
78
+ const errors = ctx.controller.getConsoleErrors();
79
+ const warnings = ctx.controller.getConsoleWarnings();
80
+ if (errors.length === 0 && warnings.length === 0) {
81
+ return 'No errors or warnings captured.';
82
+ }
83
+ let result = '';
84
+ if (errors.length > 0) {
85
+ result += `❌ Errors (${errors.length}):\n${errors.map(e => ` • ${e}`).join('\n')}\n`;
86
+ }
87
+ if (warnings.length > 0) {
88
+ result += `⚠️ Warnings (${warnings.length}):\n${warnings.map(w => ` • ${w}`).join('\n')}`;
89
+ }
90
+ return result.trim();
91
+ }
92
+ default:
93
+ throw new Error(`diagnostics module does not handle tool: ${name}`);
94
+ }
95
+ }
96
+ export const diagnosticsModule = { tools, toolNames, execute };
@@ -0,0 +1,128 @@
1
+ /**
2
+ * editor domain — pattern editing in the CodeMirror editor.
3
+ *
4
+ * Owns (6 tools): write, append, insert, replace, clear, get_pattern.
5
+ * Post-consolidation per the #110 audit: the five mutating tools
6
+ * collapse into an `edit_pattern` tool with a `mode` enum, while
7
+ * `get_pattern` stays separate (hot read path).
8
+ */
9
+ import { InputValidator } from '../../utils/InputValidator.js';
10
+ export const tools = [
11
+ {
12
+ name: 'write',
13
+ description: 'Write pattern to editor with optional auto-play and validation',
14
+ inputSchema: {
15
+ type: 'object',
16
+ properties: {
17
+ pattern: { type: 'string', description: 'Pattern code' },
18
+ auto_play: { type: 'boolean', description: 'Start playback immediately after writing (default: false)' },
19
+ validate: { type: 'boolean', description: 'Validate pattern before writing (default: true)' },
20
+ },
21
+ required: ['pattern'],
22
+ },
23
+ },
24
+ {
25
+ name: 'append',
26
+ description: 'Append code to current pattern',
27
+ inputSchema: {
28
+ type: 'object',
29
+ properties: { code: { type: 'string', description: 'Code to append' } },
30
+ required: ['code'],
31
+ },
32
+ },
33
+ {
34
+ name: 'insert',
35
+ description: 'Insert code at specific line',
36
+ inputSchema: {
37
+ type: 'object',
38
+ properties: {
39
+ position: { type: 'number', description: 'Line number' },
40
+ code: { type: 'string', description: 'Code to insert' },
41
+ },
42
+ required: ['position', 'code'],
43
+ },
44
+ },
45
+ {
46
+ name: 'replace',
47
+ description: 'Replace pattern section',
48
+ inputSchema: {
49
+ type: 'object',
50
+ properties: {
51
+ search: { type: 'string', description: 'Text to replace' },
52
+ replace: { type: 'string', description: 'Replacement text' },
53
+ },
54
+ required: ['search', 'replace'],
55
+ },
56
+ },
57
+ {
58
+ name: 'clear',
59
+ description: 'Clear the editor',
60
+ inputSchema: { type: 'object', properties: {} },
61
+ },
62
+ {
63
+ name: 'get_pattern',
64
+ description: 'Get current pattern code',
65
+ inputSchema: { type: 'object', properties: {} },
66
+ },
67
+ ];
68
+ export const toolNames = new Set(tools.map(t => t.name));
69
+ export async function execute(name, args, ctx) {
70
+ switch (name) {
71
+ case 'write': {
72
+ InputValidator.validateStringLength(args.pattern, 'pattern', 10000, true);
73
+ // Validate pattern if requested (default: true) — issue #40
74
+ if (args.validate !== false && ctx.isInitialized() && typeof ctx.controller.validatePattern === 'function') {
75
+ try {
76
+ const validation = await ctx.controller.validatePattern(args.pattern);
77
+ if (validation && !validation.valid) {
78
+ return {
79
+ success: false,
80
+ errors: validation.errors,
81
+ warnings: validation.warnings,
82
+ suggestions: validation.suggestions,
83
+ message: `Pattern validation failed: ${validation.errors.join('; ')}`,
84
+ };
85
+ }
86
+ }
87
+ catch {
88
+ ctx.logger.warn('Pattern validation threw error, continuing with write');
89
+ }
90
+ }
91
+ const writeResult = await ctx.writePatternSafe(args.pattern);
92
+ // Auto-play if requested — issue #38
93
+ if (args.auto_play && ctx.isInitialized()) {
94
+ await ctx.controller.play();
95
+ return `${writeResult}. Playing.`;
96
+ }
97
+ return writeResult;
98
+ }
99
+ case 'append': {
100
+ InputValidator.validateStringLength(args.code, 'code', 10000, true);
101
+ const current = await ctx.getCurrentPatternSafe();
102
+ return await ctx.writePatternSafe(current + '\n' + args.code);
103
+ }
104
+ case 'insert': {
105
+ InputValidator.validatePositiveInteger(args.position, 'position');
106
+ InputValidator.validateStringLength(args.code, 'code', 10000, true);
107
+ const lines = (await ctx.getCurrentPatternSafe()).split('\n');
108
+ lines.splice(args.position, 0, args.code);
109
+ return await ctx.writePatternSafe(lines.join('\n'));
110
+ }
111
+ case 'replace': {
112
+ InputValidator.validateStringLength(args.search, 'search', 1000, true);
113
+ InputValidator.validateStringLength(args.replace, 'replace', 10000, true);
114
+ const pattern = await ctx.getCurrentPatternSafe();
115
+ // Escape $ in replacement to prevent special sequence injection ($&, $1, $', etc.)
116
+ const safeReplacement = args.replace.replace(/\$/g, '$$$$');
117
+ const replaced = pattern.replace(args.search, safeReplacement);
118
+ return await ctx.writePatternSafe(replaced);
119
+ }
120
+ case 'clear':
121
+ return await ctx.writePatternSafe('');
122
+ case 'get_pattern':
123
+ return await ctx.getCurrentPatternSafe();
124
+ default:
125
+ throw new Error(`editor module does not handle tool: ${name}`);
126
+ }
127
+ }
128
+ export const editorModule = { tools, toolNames, execute };
@@ -0,0 +1,218 @@
1
+ /**
2
+ * generate domain — pattern and music-theory generation (no browser needed).
3
+ *
4
+ * Owns (9 tools):
5
+ * generate_pattern, generate_drums, generate_bassline, generate_melody,
6
+ * generate_fill, generate_scale, generate_chord_progression,
7
+ * generate_euclidean, generate_polyrhythm
8
+ *
9
+ * `compose` stays in server.ts for now — it orchestrates init + generate
10
+ * + play + optional AI feedback and pairs with ai.ts when that extraction
11
+ * happens. Per the #110 audit, compose eventually absorbs generate_pattern
12
+ * and the four generate_* drums/bass/melody/fill tools collapse into
13
+ * `generate_part(role)`, while scale+chord_prog become `music_theory`.
14
+ */
15
+ import { InputValidator } from '../../utils/InputValidator.js';
16
+ export const tools = [
17
+ {
18
+ name: 'generate_pattern',
19
+ description: 'Generate complete pattern from style with optional auto-play',
20
+ inputSchema: {
21
+ type: 'object',
22
+ properties: {
23
+ style: { type: 'string', description: 'Music style (techno/house/dnb/ambient/etc)' },
24
+ key: { type: 'string', description: 'Musical key' },
25
+ bpm: { type: 'number', description: 'Tempo in BPM' },
26
+ auto_play: { type: 'boolean', description: 'Start playback immediately (default: false)' },
27
+ },
28
+ required: ['style'],
29
+ },
30
+ },
31
+ {
32
+ name: 'generate_drums',
33
+ description: 'Generate drum pattern',
34
+ inputSchema: {
35
+ type: 'object',
36
+ properties: {
37
+ style: { type: 'string', description: 'Drum style' },
38
+ complexity: { type: 'number', description: 'Complexity (0-1)' },
39
+ },
40
+ required: ['style'],
41
+ },
42
+ },
43
+ {
44
+ name: 'generate_bassline',
45
+ description: 'Generate bassline',
46
+ inputSchema: {
47
+ type: 'object',
48
+ properties: {
49
+ key: { type: 'string', description: 'Musical key' },
50
+ style: { type: 'string', description: 'Bass style' },
51
+ },
52
+ required: ['key', 'style'],
53
+ },
54
+ },
55
+ {
56
+ name: 'generate_melody',
57
+ description: 'Generate melody from scale',
58
+ inputSchema: {
59
+ type: 'object',
60
+ properties: {
61
+ scale: { type: 'string', description: 'Scale name' },
62
+ root: { type: 'string', description: 'Root note' },
63
+ length: { type: 'number', description: 'Number of notes' },
64
+ },
65
+ required: ['scale', 'root'],
66
+ },
67
+ },
68
+ {
69
+ name: 'generate_scale',
70
+ description: 'Generate scale notes',
71
+ inputSchema: {
72
+ type: 'object',
73
+ properties: {
74
+ root: { type: 'string', description: 'Root note' },
75
+ scale: { type: 'string', description: 'Scale type' },
76
+ },
77
+ required: ['root', 'scale'],
78
+ },
79
+ },
80
+ {
81
+ name: 'generate_chord_progression',
82
+ description: 'Generate chord progression',
83
+ inputSchema: {
84
+ type: 'object',
85
+ properties: {
86
+ key: { type: 'string', description: 'Key' },
87
+ style: { type: 'string', description: 'Style (pop/jazz/blues/etc)' },
88
+ },
89
+ required: ['key', 'style'],
90
+ },
91
+ },
92
+ {
93
+ name: 'generate_euclidean',
94
+ description: 'Generate Euclidean rhythm',
95
+ inputSchema: {
96
+ type: 'object',
97
+ properties: {
98
+ hits: { type: 'number', description: 'Number of hits' },
99
+ steps: { type: 'number', description: 'Total steps' },
100
+ sound: { type: 'string', description: 'Sound to use' },
101
+ },
102
+ required: ['hits', 'steps'],
103
+ },
104
+ },
105
+ {
106
+ name: 'generate_polyrhythm',
107
+ description: 'Generate polyrhythm',
108
+ inputSchema: {
109
+ type: 'object',
110
+ properties: {
111
+ sounds: { type: 'array', items: { type: 'string' }, description: 'Sounds to use' },
112
+ patterns: { type: 'array', items: { type: 'number' }, description: 'Pattern numbers' },
113
+ },
114
+ required: ['sounds', 'patterns'],
115
+ },
116
+ },
117
+ {
118
+ name: 'generate_fill',
119
+ description: 'Generate drum fill',
120
+ inputSchema: {
121
+ type: 'object',
122
+ properties: {
123
+ style: { type: 'string', description: 'Fill style' },
124
+ bars: { type: 'number', description: 'Number of bars' },
125
+ },
126
+ required: ['style'],
127
+ },
128
+ },
129
+ ];
130
+ export const toolNames = new Set(tools.map(t => t.name));
131
+ async function appendOrSet(generated, ctx) {
132
+ const current = await ctx.getCurrentPatternSafe();
133
+ const combined = current ? current + '\n' + generated : generated;
134
+ await ctx.writePatternSafe(combined);
135
+ }
136
+ export async function execute(name, args, ctx) {
137
+ switch (name) {
138
+ case 'generate_pattern': {
139
+ InputValidator.validateStringLength(args.style, 'style', 100, false);
140
+ if (args.key)
141
+ InputValidator.validateRootNote(args.key);
142
+ if (args.bpm !== undefined)
143
+ InputValidator.validateBPM(args.bpm);
144
+ const generated = ctx.generator.generateCompletePattern(args.style, args.key || 'C', args.bpm || 120);
145
+ await ctx.writePatternSafe(generated);
146
+ if (args.auto_play && ctx.isInitialized()) {
147
+ await ctx.controller.play();
148
+ return `Generated ${args.style} pattern. Playing.`;
149
+ }
150
+ return `Generated ${args.style} pattern`;
151
+ }
152
+ case 'generate_drums': {
153
+ InputValidator.validateStringLength(args.style, 'style', 100, false);
154
+ if (args.complexity !== undefined)
155
+ InputValidator.validateNormalizedValue(args.complexity, 'complexity');
156
+ const drums = ctx.generator.generateDrumPattern(args.style, args.complexity || 0.5);
157
+ await appendOrSet(drums, ctx);
158
+ return `Generated ${args.style} drums`;
159
+ }
160
+ case 'generate_bassline': {
161
+ InputValidator.validateRootNote(args.key);
162
+ InputValidator.validateStringLength(args.style, 'style', 100, false);
163
+ const bass = ctx.generator.generateBassline(args.key, args.style);
164
+ await appendOrSet(bass, ctx);
165
+ return `Generated ${args.style} bassline in ${args.key}`;
166
+ }
167
+ case 'generate_melody': {
168
+ InputValidator.validateRootNote(args.root);
169
+ InputValidator.validateScaleName(args.scale);
170
+ if (args.length !== undefined)
171
+ InputValidator.validatePositiveInteger(args.length, 'length');
172
+ const scale = ctx.theory.generateScale(args.root, args.scale);
173
+ const melody = ctx.generator.generateMelody(scale, args.length || 8);
174
+ await appendOrSet(melody, ctx);
175
+ return `Generated melody in ${args.root} ${args.scale}`;
176
+ }
177
+ case 'generate_scale': {
178
+ InputValidator.validateRootNote(args.root);
179
+ InputValidator.validateScaleName(args.scale);
180
+ const notes = ctx.theory.generateScale(args.root, args.scale);
181
+ return `${args.root} ${args.scale} scale: ${notes.join(', ')}`;
182
+ }
183
+ case 'generate_chord_progression': {
184
+ InputValidator.validateRootNote(args.key);
185
+ InputValidator.validateChordStyle(args.style);
186
+ const progression = ctx.theory.generateChordProgression(args.key, args.style);
187
+ const chordPattern = ctx.generator.generateChords(progression);
188
+ await appendOrSet(chordPattern, ctx);
189
+ return `Generated ${args.style} progression in ${args.key}: ${progression}`;
190
+ }
191
+ case 'generate_euclidean': {
192
+ InputValidator.validateEuclidean(args.hits, args.steps);
193
+ if (args.sound)
194
+ InputValidator.validateStringLength(args.sound, 'sound', 100, false);
195
+ const euclidean = ctx.generator.generateEuclideanPattern(args.hits, args.steps, args.sound || 'bd');
196
+ await appendOrSet(euclidean, ctx);
197
+ return `Generated Euclidean rhythm (${args.hits}/${args.steps})`;
198
+ }
199
+ case 'generate_polyrhythm': {
200
+ args.sounds.forEach((s) => InputValidator.validateStringLength(s, 'sound', 100, false));
201
+ args.patterns.forEach((p) => InputValidator.validatePositiveInteger(p, 'pattern'));
202
+ const poly = ctx.generator.generatePolyrhythm(args.sounds, args.patterns);
203
+ await appendOrSet(poly, ctx);
204
+ return 'Generated polyrhythm';
205
+ }
206
+ case 'generate_fill': {
207
+ InputValidator.validateStringLength(args.style, 'style', 100, false);
208
+ if (args.bars !== undefined)
209
+ InputValidator.validatePositiveInteger(args.bars, 'bars');
210
+ const fill = ctx.generator.generateFill(args.style, args.bars || 1);
211
+ await appendOrSet(fill, ctx);
212
+ return `Generated ${args.bars || 1} bar fill`;
213
+ }
214
+ default:
215
+ throw new Error(`generate module does not handle tool: ${name}`);
216
+ }
217
+ }
218
+ export const generateModule = { tools, toolNames, execute };