@williamzujkowski/live-coding-music-mcp 2.0.0 → 4.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.
@@ -1,126 +1,127 @@
1
1
  /**
2
2
  * editor domain — pattern editing in the CodeMirror editor.
3
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).
4
+ * Owns one consolidated mutation tool (`edit_pattern(mode)`) with
5
+ * write/append/insert/replace/clear modes, plus `get_pattern` (kept
6
+ * separate — hot read path).
8
7
  */
9
8
  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
- },
9
+ const SESSION_ID_PROP = {
10
+ session_id: {
11
+ type: 'string',
12
+ description: 'Optional session ID (#108). Omit to use default session.',
44
13
  },
14
+ };
15
+ export const tools = [
45
16
  {
46
- name: 'replace',
47
- description: 'Replace pattern section',
17
+ name: 'edit_pattern',
18
+ description: 'Mutate the current session pattern. ' +
19
+ 'mode=write replaces the editor contents (default; mirrors the old write tool exactly, including optional pattern validation and auto_play). ' +
20
+ 'mode=append concatenates `code` after the current pattern with a newline. ' +
21
+ 'mode=insert places `code` at the given line `position`. ' +
22
+ 'mode=replace runs a single string-replace from `search` to `replace`. ' +
23
+ 'mode=clear empties the editor. ' +
24
+ 'Example: edit_pattern({ mode: "write", pattern: "s(\\"bd\\")", auto_play: true }). ' +
25
+ 'For reading the editor without mutating it use get_pattern; for the on-disk pattern catalog use pattern_store.',
48
26
  inputSchema: {
49
27
  type: 'object',
50
28
  properties: {
51
- search: { type: 'string', description: 'Text to replace' },
52
- replace: { type: 'string', description: 'Replacement text' },
29
+ mode: {
30
+ type: 'string',
31
+ enum: ['write', 'append', 'insert', 'replace', 'clear'],
32
+ description: 'Which edit operation to perform (default: write)',
33
+ },
34
+ pattern: { type: 'string', description: 'Pattern code (mode=write)' },
35
+ code: { type: 'string', description: 'Code to append/insert (mode=append/insert)' },
36
+ position: { type: 'number', description: 'Line number (mode=insert)' },
37
+ search: { type: 'string', description: 'Text to replace (mode=replace)' },
38
+ replace: { type: 'string', description: 'Replacement text (mode=replace)' },
39
+ auto_play: { type: 'boolean', description: 'Start playback after write (mode=write only, default: false)' },
40
+ validate: { type: 'boolean', description: 'Validate pattern before write (mode=write only, default: true)' },
41
+ ...SESSION_ID_PROP,
53
42
  },
54
- required: ['search', 'replace'],
55
43
  },
56
44
  },
57
- {
58
- name: 'clear',
59
- description: 'Clear the editor',
60
- inputSchema: { type: 'object', properties: {} },
61
- },
62
45
  {
63
46
  name: 'get_pattern',
64
47
  description: 'Get current pattern code',
65
- inputSchema: { type: 'object', properties: {} },
48
+ inputSchema: { type: 'object', properties: { ...SESSION_ID_PROP } },
66
49
  },
67
50
  ];
68
51
  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
- }
52
+ async function doWrite(args, ctx, sid) {
53
+ InputValidator.validateStringLength(args.pattern, 'pattern', 10000, true);
54
+ const controller = ctx.getController(sid);
55
+ // Validate pattern if requested (default: true) — issue #40
56
+ if (args.validate !== false && typeof controller.validatePattern === 'function') {
57
+ try {
58
+ const validation = await controller.validatePattern(args.pattern);
59
+ if (validation && !validation.valid) {
60
+ return {
61
+ success: false,
62
+ errors: validation.errors,
63
+ warnings: validation.warnings,
64
+ suggestions: validation.suggestions,
65
+ message: `Pattern validation failed: ${validation.errors.join('; ')}`,
66
+ };
90
67
  }
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
68
  }
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'));
69
+ catch {
70
+ ctx.logger.warn('Pattern validation threw error, continuing with write');
110
71
  }
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);
72
+ }
73
+ const writeResult = await ctx.writePatternSafe(args.pattern, sid);
74
+ // Auto-play if requested — issue #38
75
+ if (args.auto_play) {
76
+ await controller.play();
77
+ return `${writeResult}. Playing.`;
78
+ }
79
+ return writeResult;
80
+ }
81
+ async function doAppend(args, ctx, sid) {
82
+ InputValidator.validateStringLength(args.code, 'code', 10000, true);
83
+ const current = await ctx.getCurrentPatternSafe(sid);
84
+ return await ctx.writePatternSafe(current + '\n' + args.code, sid);
85
+ }
86
+ async function doInsert(args, ctx, sid) {
87
+ InputValidator.validatePositiveInteger(args.position, 'position');
88
+ InputValidator.validateStringLength(args.code, 'code', 10000, true);
89
+ const lines = (await ctx.getCurrentPatternSafe(sid)).split('\n');
90
+ lines.splice(args.position, 0, args.code);
91
+ return await ctx.writePatternSafe(lines.join('\n'), sid);
92
+ }
93
+ async function doReplace(args, ctx, sid) {
94
+ InputValidator.validateStringLength(args.search, 'search', 1000, true);
95
+ InputValidator.validateStringLength(args.replace, 'replace', 10000, true);
96
+ const pattern = await ctx.getCurrentPatternSafe(sid);
97
+ // Escape $ in replacement to prevent special sequence injection ($&, $1, $', etc.)
98
+ const safeReplacement = args.replace.replace(/\$/g, '$$$$');
99
+ const replaced = pattern.replace(args.search, safeReplacement);
100
+ return await ctx.writePatternSafe(replaced, sid);
101
+ }
102
+ async function doClear(ctx, sid) {
103
+ return await ctx.writePatternSafe('', sid);
104
+ }
105
+ export async function execute(name, args, ctx) {
106
+ const sid = args?.session_id;
107
+ if (!sid && !ctx.isInitialized()) {
108
+ return 'Browser not initialized. Run init first.';
109
+ }
110
+ switch (name) {
111
+ case 'edit_pattern': {
112
+ const mode = args?.mode ?? 'write';
113
+ switch (mode) {
114
+ case 'write': return await doWrite(args, ctx, sid);
115
+ case 'append': return await doAppend(args, ctx, sid);
116
+ case 'insert': return await doInsert(args, ctx, sid);
117
+ case 'replace': return await doReplace(args, ctx, sid);
118
+ case 'clear': return await doClear(ctx, sid);
119
+ default:
120
+ throw new Error(`Invalid mode: ${mode}. Must be one of: write, append, insert, replace, clear`);
121
+ }
119
122
  }
120
- case 'clear':
121
- return await ctx.writePatternSafe('');
122
123
  case 'get_pattern':
123
- return await ctx.getCurrentPatternSafe();
124
+ return await ctx.getCurrentPatternSafe(sid);
124
125
  default:
125
126
  throw new Error(`editor module does not handle tool: ${name}`);
126
127
  }
@@ -1,215 +1,186 @@
1
1
  /**
2
2
  * generate domain — pattern and music-theory generation (no browser needed).
3
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
4
+ * Owns (3 tools):
5
+ * generate_part(role) — drums/bass/melody/fill layers
6
+ * music_theory(query) — scale notes, chord progressions
7
+ * generate_rhythm(type) — euclidean / polyrhythm patterns
8
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`.
9
+ * Full-composition generation lives in `compose` (compose.ts), which
10
+ * orchestrates init + generate + play + optional AI feedback.
14
11
  */
15
12
  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
- },
13
+ const SESSION_ID_PROP = {
14
+ session_id: {
15
+ type: 'string',
16
+ description: 'Optional session ID (#108). Omit to use default session.',
91
17
  },
18
+ };
19
+ export const tools = [
92
20
  {
93
- name: 'generate_euclidean',
94
- description: 'Generate Euclidean rhythm',
21
+ name: 'generate_part',
22
+ description: 'Generate a single instrumental layer and append it to the current session pattern. ' +
23
+ 'role=drums takes `style` (e.g. "techno"/"house") and optional `complexity` 0-1. ' +
24
+ 'role=bass takes `key` (e.g. "C") + `style`. ' +
25
+ 'role=melody takes `root`/`scale` (e.g. C/minor) and optional `length` (notes). ' +
26
+ 'role=fill takes `style` and optional `bars`. ' +
27
+ 'Example: generate_part({ role: "drums", style: "techno", complexity: 0.7 }). ' +
28
+ 'For full compositions use compose; for rhythmic patterns use generate_rhythm; for music-theory queries use music_theory.',
95
29
  inputSchema: {
96
30
  type: 'object',
97
31
  properties: {
98
- hits: { type: 'number', description: 'Number of hits' },
99
- steps: { type: 'number', description: 'Total steps' },
100
- sound: { type: 'string', description: 'Sound to use' },
32
+ role: { type: 'string', enum: ['drums', 'bass', 'melody', 'fill'], description: 'Which part to generate' },
33
+ style: { type: 'string', description: 'role=drums/bass/fill: musical style' },
34
+ complexity: { type: 'number', description: 'role=drums: complexity 0-1 (default 0.5)' },
35
+ key: { type: 'string', description: 'role=bass: musical key' },
36
+ root: { type: 'string', description: 'role=melody: root note' },
37
+ scale: { type: 'string', description: 'role=melody: scale name' },
38
+ length: { type: 'number', description: 'role=melody: number of notes (default 8)' },
39
+ bars: { type: 'number', description: 'role=fill: number of bars (default 1)' },
40
+ ...SESSION_ID_PROP,
101
41
  },
102
- required: ['hits', 'steps'],
42
+ required: ['role'],
103
43
  },
104
44
  },
105
45
  {
106
- name: 'generate_polyrhythm',
107
- description: 'Generate polyrhythm',
46
+ name: 'music_theory',
47
+ description: 'Music-theory queries. ' +
48
+ 'query=scale returns the notes of a scale (e.g. "C major scale: C, D, E, F, G, A, B"). ' +
49
+ 'query=chord_progression returns a chord progression for the key/style AND writes the resulting chord pattern into the current session. ' +
50
+ 'Example: music_theory({ query: "scale", root: "C", scale: "major" }). ' +
51
+ 'For pattern generation (drums/bass/melody) use generate_part; for rhythmic patterns use generate_rhythm.',
108
52
  inputSchema: {
109
53
  type: 'object',
110
54
  properties: {
111
- sounds: { type: 'array', items: { type: 'string' }, description: 'Sounds to use' },
112
- patterns: { type: 'array', items: { type: 'number' }, description: 'Pattern numbers' },
55
+ query: { type: 'string', enum: ['scale', 'chord_progression'], description: 'Which theory query' },
56
+ root: { type: 'string', description: 'Root note (query=scale; also used as key for chord_progression)' },
57
+ scale: { type: 'string', description: 'Scale type (query=scale)' },
58
+ key: { type: 'string', description: 'Key (query=chord_progression)' },
59
+ style: { type: 'string', description: 'Style (query=chord_progression: pop/jazz/blues/etc)' },
60
+ ...SESSION_ID_PROP,
113
61
  },
114
- required: ['sounds', 'patterns'],
62
+ required: ['query'],
115
63
  },
116
64
  },
117
65
  {
118
- name: 'generate_fill',
119
- description: 'Generate drum fill',
66
+ name: 'generate_rhythm',
67
+ description: 'Generate a rhythmic pattern and append it to the current session. ' +
68
+ 'type=euclidean produces a Euclidean rhythm with `hits` evenly distributed across `steps` (optional `sound` param, default "bd"). ' +
69
+ 'type=polyrhythm overlays multiple sound layers with given pattern numbers. ' +
70
+ 'Example: generate_rhythm({ type: "euclidean", hits: 3, steps: 8, sound: "hh" }). ' +
71
+ 'For complete patterns (drums/bass/melody) use generate_part; for whole compositions use compose.',
120
72
  inputSchema: {
121
73
  type: 'object',
122
74
  properties: {
123
- style: { type: 'string', description: 'Fill style' },
124
- bars: { type: 'number', description: 'Number of bars' },
75
+ type: { type: 'string', enum: ['euclidean', 'polyrhythm'], description: 'Rhythm type' },
76
+ hits: { type: 'number', description: 'type=euclidean: hits count' },
77
+ steps: { type: 'number', description: 'type=euclidean: total steps' },
78
+ sound: { type: 'string', description: 'type=euclidean: sound to use (default bd)' },
79
+ sounds: { type: 'array', items: { type: 'string' }, description: 'type=polyrhythm: sounds per layer' },
80
+ patterns: { type: 'array', items: { type: 'number' }, description: 'type=polyrhythm: pattern numbers per layer' },
81
+ ...SESSION_ID_PROP,
125
82
  },
126
- required: ['style'],
83
+ required: ['type'],
127
84
  },
128
85
  },
129
86
  ];
130
87
  export const toolNames = new Set(tools.map(t => t.name));
131
- async function appendOrSet(generated, ctx) {
132
- const current = await ctx.getCurrentPatternSafe();
88
+ async function appendOrSet(generated, ctx, sessionId) {
89
+ const current = await ctx.getCurrentPatternSafe(sessionId);
133
90
  const combined = current ? current + '\n' + generated : generated;
134
- await ctx.writePatternSafe(combined);
91
+ await ctx.writePatternSafe(combined, sessionId);
92
+ }
93
+ function doScale(args, ctx) {
94
+ InputValidator.validateRootNote(args.root);
95
+ InputValidator.validateScaleName(args.scale);
96
+ const notes = ctx.theory.generateScale(args.root, args.scale);
97
+ return `${args.root} ${args.scale} scale: ${notes.join(', ')}`;
98
+ }
99
+ async function doChordProgression(args, ctx, sid) {
100
+ InputValidator.validateRootNote(args.key);
101
+ InputValidator.validateChordStyle(args.style);
102
+ const progression = ctx.theory.generateChordProgression(args.key, args.style);
103
+ const chordPattern = ctx.generator.generateChords(progression);
104
+ await appendOrSet(chordPattern, ctx, sid);
105
+ return `Generated ${args.style} progression in ${args.key}: ${progression}`;
106
+ }
107
+ async function doEuclidean(args, ctx, sid) {
108
+ InputValidator.validateEuclidean(args.hits, args.steps);
109
+ if (args.sound)
110
+ InputValidator.validateStringLength(args.sound, 'sound', 100, false);
111
+ const euclidean = ctx.generator.generateEuclideanPattern(args.hits, args.steps, args.sound || 'bd');
112
+ await appendOrSet(euclidean, ctx, sid);
113
+ return `Generated Euclidean rhythm (${args.hits}/${args.steps})`;
114
+ }
115
+ async function doPolyrhythm(args, ctx, sid) {
116
+ args.sounds.forEach((s) => InputValidator.validateStringLength(s, 'sound', 100, false));
117
+ args.patterns.forEach((p) => InputValidator.validatePositiveInteger(p, 'pattern'));
118
+ const poly = ctx.generator.generatePolyrhythm(args.sounds, args.patterns);
119
+ await appendOrSet(poly, ctx, sid);
120
+ return 'Generated polyrhythm';
121
+ }
122
+ async function doDrums(args, ctx, sid) {
123
+ InputValidator.validateStringLength(args.style, 'style', 100, false);
124
+ if (args.complexity !== undefined)
125
+ InputValidator.validateNormalizedValue(args.complexity, 'complexity');
126
+ const drums = ctx.generator.generateDrumPattern(args.style, args.complexity || 0.5);
127
+ await appendOrSet(drums, ctx, sid);
128
+ return `Generated ${args.style} drums`;
129
+ }
130
+ async function doBassline(args, ctx, sid) {
131
+ InputValidator.validateRootNote(args.key);
132
+ InputValidator.validateStringLength(args.style, 'style', 100, false);
133
+ const bass = ctx.generator.generateBassline(args.key, args.style);
134
+ await appendOrSet(bass, ctx, sid);
135
+ return `Generated ${args.style} bassline in ${args.key}`;
136
+ }
137
+ async function doMelody(args, ctx, sid) {
138
+ InputValidator.validateRootNote(args.root);
139
+ InputValidator.validateScaleName(args.scale);
140
+ if (args.length !== undefined)
141
+ InputValidator.validatePositiveInteger(args.length, 'length');
142
+ const scale = ctx.theory.generateScale(args.root, args.scale);
143
+ const melody = ctx.generator.generateMelody(scale, args.length || 8);
144
+ await appendOrSet(melody, ctx, sid);
145
+ return `Generated melody in ${args.root} ${args.scale}`;
146
+ }
147
+ async function doFill(args, ctx, sid) {
148
+ InputValidator.validateStringLength(args.style, 'style', 100, false);
149
+ if (args.bars !== undefined)
150
+ InputValidator.validatePositiveInteger(args.bars, 'bars');
151
+ const fill = ctx.generator.generateFill(args.style, args.bars || 1);
152
+ await appendOrSet(fill, ctx, sid);
153
+ return `Generated ${args.bars || 1} bar fill`;
135
154
  }
136
155
  export async function execute(name, args, ctx) {
156
+ const sid = args?.session_id;
137
157
  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.`;
158
+ case 'generate_part': {
159
+ const role = args?.role;
160
+ switch (role) {
161
+ case 'drums': return await doDrums(args, ctx, sid);
162
+ case 'bass': return await doBassline(args, ctx, sid);
163
+ case 'melody': return await doMelody(args, ctx, sid);
164
+ case 'fill': return await doFill(args, ctx, sid);
165
+ default:
166
+ throw new Error(`Invalid role: ${role}. Must be one of: drums, bass, melody, fill`);
149
167
  }
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
168
  }
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';
169
+ case 'music_theory': {
170
+ const q = args?.query;
171
+ if (q !== 'scale' && q !== 'chord_progression') {
172
+ throw new Error(`Invalid query: ${q}. Must be one of: scale, chord_progression`);
173
+ }
174
+ return q === 'scale' ? doScale(args, ctx) : await doChordProgression(args, ctx, sid);
205
175
  }
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`;
176
+ case 'generate_rhythm': {
177
+ const t = args?.type;
178
+ if (t !== 'euclidean' && t !== 'polyrhythm') {
179
+ throw new Error(`Invalid type: ${t}. Must be one of: euclidean, polyrhythm`);
180
+ }
181
+ return t === 'euclidean'
182
+ ? await doEuclidean(args, ctx, sid)
183
+ : await doPolyrhythm(args, ctx, sid);
213
184
  }
214
185
  default:
215
186
  throw new Error(`generate module does not handle tool: ${name}`);