@williamzujkowski/live-coding-music-mcp 1.0.0 → 3.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,190 @@
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)`) plus
5
+ * `get_pattern` (kept separate, hot read path) and five deprecated
6
+ * aliases (write/append/insert/replace/clear) per #120 / #148.
7
+ *
8
+ * Aliases forward to `edit_pattern` and will be removed in a future
9
+ * release per the deprecation policy.
8
10
  */
9
11
  import { InputValidator } from '../../utils/InputValidator.js';
12
+ const SESSION_ID_PROP = {
13
+ session_id: {
14
+ type: 'string',
15
+ description: 'Optional session ID (#108). Omit to use default session.',
16
+ },
17
+ };
10
18
  export const tools = [
19
+ {
20
+ name: 'edit_pattern',
21
+ description: 'Mutate the current session pattern. ' +
22
+ 'mode=write replaces the editor contents (default; mirrors the old write tool exactly, including optional pattern validation and auto_play). ' +
23
+ 'mode=append concatenates `code` after the current pattern with a newline. ' +
24
+ 'mode=insert places `code` at the given line `position`. ' +
25
+ 'mode=replace runs a single string-replace from `search` to `replace`. ' +
26
+ 'mode=clear empties the editor. ' +
27
+ 'Example: edit_pattern({ mode: "write", pattern: "s(\\"bd\\")", auto_play: true }). ' +
28
+ 'For reading the editor without mutating it use get_pattern; for the on-disk pattern catalog use pattern_store.',
29
+ inputSchema: {
30
+ type: 'object',
31
+ properties: {
32
+ mode: {
33
+ type: 'string',
34
+ enum: ['write', 'append', 'insert', 'replace', 'clear'],
35
+ description: 'Which edit operation to perform (default: write)',
36
+ },
37
+ pattern: { type: 'string', description: 'Pattern code (mode=write)' },
38
+ code: { type: 'string', description: 'Code to append/insert (mode=append/insert)' },
39
+ position: { type: 'number', description: 'Line number (mode=insert)' },
40
+ search: { type: 'string', description: 'Text to replace (mode=replace)' },
41
+ replace: { type: 'string', description: 'Replacement text (mode=replace)' },
42
+ auto_play: { type: 'boolean', description: 'Start playback after write (mode=write only, default: false)' },
43
+ validate: { type: 'boolean', description: 'Validate pattern before write (mode=write only, default: true)' },
44
+ ...SESSION_ID_PROP,
45
+ },
46
+ },
47
+ },
48
+ {
49
+ name: 'get_pattern',
50
+ description: 'Get current pattern code',
51
+ inputSchema: { type: 'object', properties: { ...SESSION_ID_PROP } },
52
+ },
11
53
  {
12
54
  name: 'write',
13
- description: 'Write pattern to editor with optional auto-play and validation',
55
+ description: '[DEPRECATED — use edit_pattern({ mode: "write" }) instead] Write pattern to editor with optional auto-play and validation',
14
56
  inputSchema: {
15
57
  type: 'object',
16
58
  properties: {
17
59
  pattern: { type: 'string', description: 'Pattern code' },
18
60
  auto_play: { type: 'boolean', description: 'Start playback immediately after writing (default: false)' },
19
61
  validate: { type: 'boolean', description: 'Validate pattern before writing (default: true)' },
62
+ ...SESSION_ID_PROP,
20
63
  },
21
64
  required: ['pattern'],
22
65
  },
23
66
  },
24
67
  {
25
68
  name: 'append',
26
- description: 'Append code to current pattern',
69
+ description: '[DEPRECATED — use edit_pattern({ mode: "append" }) instead] Append code to current pattern',
27
70
  inputSchema: {
28
71
  type: 'object',
29
- properties: { code: { type: 'string', description: 'Code to append' } },
72
+ properties: { code: { type: 'string', description: 'Code to append' }, ...SESSION_ID_PROP },
30
73
  required: ['code'],
31
74
  },
32
75
  },
33
76
  {
34
77
  name: 'insert',
35
- description: 'Insert code at specific line',
78
+ description: '[DEPRECATED — use edit_pattern({ mode: "insert" }) instead] Insert code at specific line',
36
79
  inputSchema: {
37
80
  type: 'object',
38
81
  properties: {
39
82
  position: { type: 'number', description: 'Line number' },
40
83
  code: { type: 'string', description: 'Code to insert' },
84
+ ...SESSION_ID_PROP,
41
85
  },
42
86
  required: ['position', 'code'],
43
87
  },
44
88
  },
45
89
  {
46
90
  name: 'replace',
47
- description: 'Replace pattern section',
91
+ description: '[DEPRECATED — use edit_pattern({ mode: "replace" }) instead] Replace pattern section',
48
92
  inputSchema: {
49
93
  type: 'object',
50
94
  properties: {
51
95
  search: { type: 'string', description: 'Text to replace' },
52
96
  replace: { type: 'string', description: 'Replacement text' },
97
+ ...SESSION_ID_PROP,
53
98
  },
54
99
  required: ['search', 'replace'],
55
100
  },
56
101
  },
57
102
  {
58
103
  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: {} },
104
+ description: '[DEPRECATED — use edit_pattern({ mode: "clear" }) instead] Clear the editor',
105
+ inputSchema: { type: 'object', properties: { ...SESSION_ID_PROP } },
66
106
  },
67
107
  ];
68
108
  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.`;
109
+ async function doWrite(args, ctx, sid) {
110
+ InputValidator.validateStringLength(args.pattern, 'pattern', 10000, true);
111
+ const controller = ctx.getController(sid);
112
+ // Validate pattern if requested (default: true) — issue #40
113
+ if (args.validate !== false && typeof controller.validatePattern === 'function') {
114
+ try {
115
+ const validation = await controller.validatePattern(args.pattern);
116
+ if (validation && !validation.valid) {
117
+ return {
118
+ success: false,
119
+ errors: validation.errors,
120
+ warnings: validation.warnings,
121
+ suggestions: validation.suggestions,
122
+ message: `Pattern validation failed: ${validation.errors.join('; ')}`,
123
+ };
96
124
  }
97
- return writeResult;
98
125
  }
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);
126
+ catch {
127
+ ctx.logger.warn('Pattern validation threw error, continuing with write');
103
128
  }
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);
129
+ }
130
+ const writeResult = await ctx.writePatternSafe(args.pattern, sid);
131
+ // Auto-play if requested — issue #38
132
+ if (args.auto_play) {
133
+ await controller.play();
134
+ return `${writeResult}. Playing.`;
135
+ }
136
+ return writeResult;
137
+ }
138
+ async function doAppend(args, ctx, sid) {
139
+ InputValidator.validateStringLength(args.code, 'code', 10000, true);
140
+ const current = await ctx.getCurrentPatternSafe(sid);
141
+ return await ctx.writePatternSafe(current + '\n' + args.code, sid);
142
+ }
143
+ async function doInsert(args, ctx, sid) {
144
+ InputValidator.validatePositiveInteger(args.position, 'position');
145
+ InputValidator.validateStringLength(args.code, 'code', 10000, true);
146
+ const lines = (await ctx.getCurrentPatternSafe(sid)).split('\n');
147
+ lines.splice(args.position, 0, args.code);
148
+ return await ctx.writePatternSafe(lines.join('\n'), sid);
149
+ }
150
+ async function doReplace(args, ctx, sid) {
151
+ InputValidator.validateStringLength(args.search, 'search', 1000, true);
152
+ InputValidator.validateStringLength(args.replace, 'replace', 10000, true);
153
+ const pattern = await ctx.getCurrentPatternSafe(sid);
154
+ // Escape $ in replacement to prevent special sequence injection ($&, $1, $', etc.)
155
+ const safeReplacement = args.replace.replace(/\$/g, '$$$$');
156
+ const replaced = pattern.replace(args.search, safeReplacement);
157
+ return await ctx.writePatternSafe(replaced, sid);
158
+ }
159
+ async function doClear(ctx, sid) {
160
+ return await ctx.writePatternSafe('', sid);
161
+ }
162
+ export async function execute(name, args, ctx) {
163
+ const sid = args?.session_id;
164
+ if (!sid && !ctx.isInitialized()) {
165
+ return 'Browser not initialized. Run init first.';
166
+ }
167
+ switch (name) {
168
+ case 'edit_pattern': {
169
+ const mode = args?.mode ?? 'write';
170
+ switch (mode) {
171
+ case 'write': return await doWrite(args, ctx, sid);
172
+ case 'append': return await doAppend(args, ctx, sid);
173
+ case 'insert': return await doInsert(args, ctx, sid);
174
+ case 'replace': return await doReplace(args, ctx, sid);
175
+ case 'clear': return await doClear(ctx, sid);
176
+ default:
177
+ throw new Error(`Invalid mode: ${mode}. Must be one of: write, append, insert, replace, clear`);
178
+ }
119
179
  }
120
- case 'clear':
121
- return await ctx.writePatternSafe('');
180
+ // Deprecated aliases — forward to consolidated handler.
181
+ case 'write': return await doWrite(args, ctx, sid);
182
+ case 'append': return await doAppend(args, ctx, sid);
183
+ case 'insert': return await doInsert(args, ctx, sid);
184
+ case 'replace': return await doReplace(args, ctx, sid);
185
+ case 'clear': return await doClear(ctx, sid);
122
186
  case 'get_pattern':
123
- return await ctx.getCurrentPatternSafe();
187
+ return await ctx.getCurrentPatternSafe(sid);
124
188
  default:
125
189
  throw new Error(`editor module does not handle tool: ${name}`);
126
190
  }
@@ -13,10 +13,16 @@
13
13
  * `generate_part(role)`, while scale+chord_prog become `music_theory`.
14
14
  */
15
15
  import { InputValidator } from '../../utils/InputValidator.js';
16
+ const SESSION_ID_PROP = {
17
+ session_id: {
18
+ type: 'string',
19
+ description: 'Optional session ID (#108). Omit to use default session.',
20
+ },
21
+ };
16
22
  export const tools = [
17
23
  {
18
24
  name: 'generate_pattern',
19
- description: 'Generate complete pattern from style with optional auto-play',
25
+ description: '[DEPRECATED — use compose instead, which auto-inits and returns richer metadata] Generate complete pattern from style with optional auto-play.',
20
26
  inputSchema: {
21
27
  type: 'object',
22
28
  properties: {
@@ -24,50 +30,99 @@ export const tools = [
24
30
  key: { type: 'string', description: 'Musical key' },
25
31
  bpm: { type: 'number', description: 'Tempo in BPM' },
26
32
  auto_play: { type: 'boolean', description: 'Start playback immediately (default: false)' },
33
+ ...SESSION_ID_PROP,
27
34
  },
28
35
  required: ['style'],
29
36
  },
30
37
  },
38
+ {
39
+ name: 'generate_part',
40
+ description: 'Generate a single instrumental layer and append it to the current session pattern. ' +
41
+ 'role=drums takes `style` (e.g. "techno"/"house") and optional `complexity` 0-1. ' +
42
+ 'role=bass takes `key` (e.g. "C") + `style`. ' +
43
+ 'role=melody takes `root`/`scale` (e.g. C/minor) and optional `length` (notes). ' +
44
+ 'role=fill takes `style` and optional `bars`. ' +
45
+ 'Example: generate_part({ role: "drums", style: "techno", complexity: 0.7 }). ' +
46
+ 'For full compositions use compose; for rhythmic patterns use generate_rhythm; for music-theory queries use music_theory.',
47
+ inputSchema: {
48
+ type: 'object',
49
+ properties: {
50
+ role: { type: 'string', enum: ['drums', 'bass', 'melody', 'fill'], description: 'Which part to generate' },
51
+ style: { type: 'string', description: 'role=drums/bass/fill: musical style' },
52
+ complexity: { type: 'number', description: 'role=drums: complexity 0-1 (default 0.5)' },
53
+ key: { type: 'string', description: 'role=bass: musical key' },
54
+ root: { type: 'string', description: 'role=melody: root note' },
55
+ scale: { type: 'string', description: 'role=melody: scale name' },
56
+ length: { type: 'number', description: 'role=melody: number of notes (default 8)' },
57
+ bars: { type: 'number', description: 'role=fill: number of bars (default 1)' },
58
+ ...SESSION_ID_PROP,
59
+ },
60
+ required: ['role'],
61
+ },
62
+ },
31
63
  {
32
64
  name: 'generate_drums',
33
- description: 'Generate drum pattern',
65
+ description: '[DEPRECATED — use generate_part({ role: "drums" }) instead] Generate drum pattern',
34
66
  inputSchema: {
35
67
  type: 'object',
36
68
  properties: {
37
69
  style: { type: 'string', description: 'Drum style' },
38
70
  complexity: { type: 'number', description: 'Complexity (0-1)' },
71
+ ...SESSION_ID_PROP,
39
72
  },
40
73
  required: ['style'],
41
74
  },
42
75
  },
43
76
  {
44
77
  name: 'generate_bassline',
45
- description: 'Generate bassline',
78
+ description: '[DEPRECATED — use generate_part({ role: "bass" }) instead] Generate bassline',
46
79
  inputSchema: {
47
80
  type: 'object',
48
81
  properties: {
49
82
  key: { type: 'string', description: 'Musical key' },
50
83
  style: { type: 'string', description: 'Bass style' },
84
+ ...SESSION_ID_PROP,
51
85
  },
52
86
  required: ['key', 'style'],
53
87
  },
54
88
  },
55
89
  {
56
90
  name: 'generate_melody',
57
- description: 'Generate melody from scale',
91
+ description: '[DEPRECATED — use generate_part({ role: "melody" }) instead] Generate melody from scale',
58
92
  inputSchema: {
59
93
  type: 'object',
60
94
  properties: {
61
95
  scale: { type: 'string', description: 'Scale name' },
62
96
  root: { type: 'string', description: 'Root note' },
63
97
  length: { type: 'number', description: 'Number of notes' },
98
+ ...SESSION_ID_PROP,
64
99
  },
65
100
  required: ['scale', 'root'],
66
101
  },
67
102
  },
103
+ {
104
+ name: 'music_theory',
105
+ description: 'Music-theory queries. ' +
106
+ 'query=scale returns the notes of a scale (e.g. "C major scale: C, D, E, F, G, A, B"). ' +
107
+ 'query=chord_progression returns a chord progression for the key/style AND writes the resulting chord pattern into the current session (matches the pre-consolidation behaviour of generate_chord_progression). ' +
108
+ 'Example: music_theory({ query: "scale", root: "C", scale: "major" }). ' +
109
+ 'For pattern generation (drums/bass/melody) use generate_part; for rhythmic patterns use generate_rhythm.',
110
+ inputSchema: {
111
+ type: 'object',
112
+ properties: {
113
+ query: { type: 'string', enum: ['scale', 'chord_progression'], description: 'Which theory query' },
114
+ root: { type: 'string', description: 'Root note (query=scale; also used as key for chord_progression)' },
115
+ scale: { type: 'string', description: 'Scale type (query=scale)' },
116
+ key: { type: 'string', description: 'Key (query=chord_progression)' },
117
+ style: { type: 'string', description: 'Style (query=chord_progression: pop/jazz/blues/etc)' },
118
+ ...SESSION_ID_PROP,
119
+ },
120
+ required: ['query'],
121
+ },
122
+ },
68
123
  {
69
124
  name: 'generate_scale',
70
- description: 'Generate scale notes',
125
+ description: '[DEPRECATED — use music_theory({ query: "scale" }) instead] Generate scale notes',
71
126
  inputSchema: {
72
127
  type: 'object',
73
128
  properties: {
@@ -79,61 +134,149 @@ export const tools = [
79
134
  },
80
135
  {
81
136
  name: 'generate_chord_progression',
82
- description: 'Generate chord progression',
137
+ description: '[DEPRECATED — use music_theory({ query: "chord_progression" }) instead] Generate chord progression',
83
138
  inputSchema: {
84
139
  type: 'object',
85
140
  properties: {
86
141
  key: { type: 'string', description: 'Key' },
87
142
  style: { type: 'string', description: 'Style (pop/jazz/blues/etc)' },
143
+ ...SESSION_ID_PROP,
88
144
  },
89
145
  required: ['key', 'style'],
90
146
  },
91
147
  },
148
+ {
149
+ name: 'generate_rhythm',
150
+ description: 'Generate a rhythmic pattern and append it to the current session. ' +
151
+ 'type=euclidean produces a Euclidean rhythm with `hits` evenly distributed across `steps` (optional `sound` param, default "bd"). ' +
152
+ 'type=polyrhythm overlays multiple sound layers with given pattern numbers. ' +
153
+ 'Example: generate_rhythm({ type: "euclidean", hits: 3, steps: 8, sound: "hh" }). ' +
154
+ 'For complete patterns (drums/bass/melody) use generate_part; for whole compositions use compose.',
155
+ inputSchema: {
156
+ type: 'object',
157
+ properties: {
158
+ type: { type: 'string', enum: ['euclidean', 'polyrhythm'], description: 'Rhythm type' },
159
+ hits: { type: 'number', description: 'type=euclidean: hits count' },
160
+ steps: { type: 'number', description: 'type=euclidean: total steps' },
161
+ sound: { type: 'string', description: 'type=euclidean: sound to use (default bd)' },
162
+ sounds: { type: 'array', items: { type: 'string' }, description: 'type=polyrhythm: sounds per layer' },
163
+ patterns: { type: 'array', items: { type: 'number' }, description: 'type=polyrhythm: pattern numbers per layer' },
164
+ ...SESSION_ID_PROP,
165
+ },
166
+ required: ['type'],
167
+ },
168
+ },
92
169
  {
93
170
  name: 'generate_euclidean',
94
- description: 'Generate Euclidean rhythm',
171
+ description: '[DEPRECATED — use generate_rhythm({ type: "euclidean" }) instead] Generate Euclidean rhythm',
95
172
  inputSchema: {
96
173
  type: 'object',
97
174
  properties: {
98
175
  hits: { type: 'number', description: 'Number of hits' },
99
176
  steps: { type: 'number', description: 'Total steps' },
100
177
  sound: { type: 'string', description: 'Sound to use' },
178
+ ...SESSION_ID_PROP,
101
179
  },
102
180
  required: ['hits', 'steps'],
103
181
  },
104
182
  },
105
183
  {
106
184
  name: 'generate_polyrhythm',
107
- description: 'Generate polyrhythm',
185
+ description: '[DEPRECATED — use generate_rhythm({ type: "polyrhythm" }) instead] Generate polyrhythm',
108
186
  inputSchema: {
109
187
  type: 'object',
110
188
  properties: {
111
189
  sounds: { type: 'array', items: { type: 'string' }, description: 'Sounds to use' },
112
190
  patterns: { type: 'array', items: { type: 'number' }, description: 'Pattern numbers' },
191
+ ...SESSION_ID_PROP,
113
192
  },
114
193
  required: ['sounds', 'patterns'],
115
194
  },
116
195
  },
117
196
  {
118
197
  name: 'generate_fill',
119
- description: 'Generate drum fill',
198
+ description: '[DEPRECATED — use generate_part({ role: "fill" }) instead] Generate drum fill',
120
199
  inputSchema: {
121
200
  type: 'object',
122
201
  properties: {
123
202
  style: { type: 'string', description: 'Fill style' },
124
203
  bars: { type: 'number', description: 'Number of bars' },
204
+ ...SESSION_ID_PROP,
125
205
  },
126
206
  required: ['style'],
127
207
  },
128
208
  },
129
209
  ];
130
210
  export const toolNames = new Set(tools.map(t => t.name));
131
- async function appendOrSet(generated, ctx) {
132
- const current = await ctx.getCurrentPatternSafe();
211
+ async function appendOrSet(generated, ctx, sessionId) {
212
+ const current = await ctx.getCurrentPatternSafe(sessionId);
133
213
  const combined = current ? current + '\n' + generated : generated;
134
- await ctx.writePatternSafe(combined);
214
+ await ctx.writePatternSafe(combined, sessionId);
215
+ }
216
+ function doScale(args, ctx) {
217
+ InputValidator.validateRootNote(args.root);
218
+ InputValidator.validateScaleName(args.scale);
219
+ const notes = ctx.theory.generateScale(args.root, args.scale);
220
+ return `${args.root} ${args.scale} scale: ${notes.join(', ')}`;
221
+ }
222
+ async function doChordProgression(args, ctx, sid) {
223
+ InputValidator.validateRootNote(args.key);
224
+ InputValidator.validateChordStyle(args.style);
225
+ const progression = ctx.theory.generateChordProgression(args.key, args.style);
226
+ const chordPattern = ctx.generator.generateChords(progression);
227
+ await appendOrSet(chordPattern, ctx, sid);
228
+ return `Generated ${args.style} progression in ${args.key}: ${progression}`;
229
+ }
230
+ async function doEuclidean(args, ctx, sid) {
231
+ InputValidator.validateEuclidean(args.hits, args.steps);
232
+ if (args.sound)
233
+ InputValidator.validateStringLength(args.sound, 'sound', 100, false);
234
+ const euclidean = ctx.generator.generateEuclideanPattern(args.hits, args.steps, args.sound || 'bd');
235
+ await appendOrSet(euclidean, ctx, sid);
236
+ return `Generated Euclidean rhythm (${args.hits}/${args.steps})`;
237
+ }
238
+ async function doPolyrhythm(args, ctx, sid) {
239
+ args.sounds.forEach((s) => InputValidator.validateStringLength(s, 'sound', 100, false));
240
+ args.patterns.forEach((p) => InputValidator.validatePositiveInteger(p, 'pattern'));
241
+ const poly = ctx.generator.generatePolyrhythm(args.sounds, args.patterns);
242
+ await appendOrSet(poly, ctx, sid);
243
+ return 'Generated polyrhythm';
244
+ }
245
+ async function doDrums(args, ctx, sid) {
246
+ InputValidator.validateStringLength(args.style, 'style', 100, false);
247
+ if (args.complexity !== undefined)
248
+ InputValidator.validateNormalizedValue(args.complexity, 'complexity');
249
+ const drums = ctx.generator.generateDrumPattern(args.style, args.complexity || 0.5);
250
+ await appendOrSet(drums, ctx, sid);
251
+ return `Generated ${args.style} drums`;
252
+ }
253
+ async function doBassline(args, ctx, sid) {
254
+ InputValidator.validateRootNote(args.key);
255
+ InputValidator.validateStringLength(args.style, 'style', 100, false);
256
+ const bass = ctx.generator.generateBassline(args.key, args.style);
257
+ await appendOrSet(bass, ctx, sid);
258
+ return `Generated ${args.style} bassline in ${args.key}`;
259
+ }
260
+ async function doMelody(args, ctx, sid) {
261
+ InputValidator.validateRootNote(args.root);
262
+ InputValidator.validateScaleName(args.scale);
263
+ if (args.length !== undefined)
264
+ InputValidator.validatePositiveInteger(args.length, 'length');
265
+ const scale = ctx.theory.generateScale(args.root, args.scale);
266
+ const melody = ctx.generator.generateMelody(scale, args.length || 8);
267
+ await appendOrSet(melody, ctx, sid);
268
+ return `Generated melody in ${args.root} ${args.scale}`;
269
+ }
270
+ async function doFill(args, ctx, sid) {
271
+ InputValidator.validateStringLength(args.style, 'style', 100, false);
272
+ if (args.bars !== undefined)
273
+ InputValidator.validatePositiveInteger(args.bars, 'bars');
274
+ const fill = ctx.generator.generateFill(args.style, args.bars || 1);
275
+ await appendOrSet(fill, ctx, sid);
276
+ return `Generated ${args.bars || 1} bar fill`;
135
277
  }
136
278
  export async function execute(name, args, ctx) {
279
+ const sid = args?.session_id;
137
280
  switch (name) {
138
281
  case 'generate_pattern': {
139
282
  InputValidator.validateStringLength(args.style, 'style', 100, false);
@@ -142,75 +285,48 @@ export async function execute(name, args, ctx) {
142
285
  if (args.bpm !== undefined)
143
286
  InputValidator.validateBPM(args.bpm);
144
287
  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();
288
+ await ctx.writePatternSafe(generated, sid);
289
+ if (args.auto_play && (sid || ctx.isInitialized())) {
290
+ await ctx.getController(sid).play();
148
291
  return `Generated ${args.style} pattern. Playing.`;
149
292
  }
150
293
  return `Generated ${args.style} pattern`;
151
294
  }
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})`;
295
+ case 'generate_part': {
296
+ const role = args?.role;
297
+ switch (role) {
298
+ case 'drums': return await doDrums(args, ctx, sid);
299
+ case 'bass': return await doBassline(args, ctx, sid);
300
+ case 'melody': return await doMelody(args, ctx, sid);
301
+ case 'fill': return await doFill(args, ctx, sid);
302
+ default:
303
+ throw new Error(`Invalid role: ${role}. Must be one of: drums, bass, melody, fill`);
304
+ }
198
305
  }
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';
306
+ case 'generate_drums': return await doDrums(args, ctx, sid);
307
+ case 'generate_bassline': return await doBassline(args, ctx, sid);
308
+ case 'generate_melody': return await doMelody(args, ctx, sid);
309
+ case 'music_theory': {
310
+ const q = args?.query;
311
+ if (q !== 'scale' && q !== 'chord_progression') {
312
+ throw new Error(`Invalid query: ${q}. Must be one of: scale, chord_progression`);
313
+ }
314
+ return q === 'scale' ? doScale(args, ctx) : await doChordProgression(args, ctx, sid);
205
315
  }
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`;
316
+ case 'generate_scale': return doScale(args, ctx);
317
+ case 'generate_chord_progression': return await doChordProgression(args, ctx, sid);
318
+ case 'generate_rhythm': {
319
+ const t = args?.type;
320
+ if (t !== 'euclidean' && t !== 'polyrhythm') {
321
+ throw new Error(`Invalid type: ${t}. Must be one of: euclidean, polyrhythm`);
322
+ }
323
+ return t === 'euclidean'
324
+ ? await doEuclidean(args, ctx, sid)
325
+ : await doPolyrhythm(args, ctx, sid);
213
326
  }
327
+ case 'generate_euclidean': return await doEuclidean(args, ctx, sid);
328
+ case 'generate_polyrhythm': return await doPolyrhythm(args, ctx, sid);
329
+ case 'generate_fill': return await doFill(args, ctx, sid);
214
330
  default:
215
331
  throw new Error(`generate module does not handle tool: ${name}`);
216
332
  }