@williamzujkowski/live-coding-music-mcp 3.0.0 → 4.1.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 (53) hide show
  1. package/README.md +212 -780
  2. package/dist/AudioAnalyzer.js +1294 -91
  3. package/dist/PatternStore.js +276 -23
  4. package/dist/StrudelController.js +388 -56
  5. package/dist/server/resources.js +27 -20
  6. package/dist/server/server.js +391 -83
  7. package/dist/server/tools/ai.js +129 -152
  8. package/dist/server/tools/analysis.js +170 -91
  9. package/dist/server/tools/capture.js +128 -55
  10. package/dist/server/tools/compose.js +28 -42
  11. package/dist/server/tools/diagnostics.js +23 -53
  12. package/dist/server/tools/editor.js +63 -72
  13. package/dist/server/tools/generate.js +45 -167
  14. package/dist/server/tools/history.js +35 -66
  15. package/dist/server/tools/playback.js +7 -22
  16. package/dist/server/tools/session.js +41 -47
  17. package/dist/server/tools/storage.js +173 -50
  18. package/dist/server/tools/transform.js +136 -199
  19. package/dist/server/tools/types.js +209 -14
  20. package/dist/services/AudioCaptureService.js +302 -33
  21. package/dist/services/AudioExportService.js +318 -0
  22. package/dist/services/BrowserOnlyFunctions.js +149 -0
  23. package/dist/services/EventDensityProbe.js +324 -0
  24. package/dist/services/GeminiService.js +221 -36
  25. package/dist/services/IsolatedEngineRunner.js +308 -0
  26. package/dist/services/IsolatedStrudelEngine.js +135 -0
  27. package/dist/services/LocalPatternEngine.js +16 -0
  28. package/dist/services/MIDIExportService.js +765 -71
  29. package/dist/services/MIDIImportService.js +474 -0
  30. package/dist/services/MusicTheory.js +85 -13
  31. package/dist/services/PatternGenerator.js +168 -85
  32. package/dist/services/PatternSandbox.js +359 -0
  33. package/dist/services/SessionManager.js +338 -65
  34. package/dist/services/StrudelEngine.js +218 -12
  35. package/dist/services/StrudelEngineHelpers.js +24 -8
  36. package/dist/services/StyleRegistry.js +204 -0
  37. package/dist/services/Transferable.js +114 -0
  38. package/dist/services/ai/AiTransport.js +56 -0
  39. package/dist/services/ai/AudioMeasurements.js +155 -0
  40. package/dist/services/ai/CliTransport.js +256 -0
  41. package/dist/services/engineChild.js +83 -0
  42. package/dist/services/engineChildPath.js +28 -0
  43. package/dist/utils/CategorisedError.js +59 -0
  44. package/dist/utils/ErrorRecovery.js +130 -110
  45. package/dist/utils/InputValidator.js +55 -7
  46. package/dist/utils/PatternValidator.js +136 -17
  47. package/dist/utils/SafePath.js +112 -0
  48. package/dist/utils/ServerConfig.js +248 -0
  49. package/dist/utils/TableLookup.js +41 -0
  50. package/dist/utils/Tempo.js +143 -0
  51. package/package.json +17 -5
  52. package/dist/services/__mocks__/StrudelEngine.js +0 -157
  53. package/dist/types/index.js +0 -1
@@ -1,14 +1,13 @@
1
1
  /**
2
2
  * editor domain — pattern editing in the CodeMirror editor.
3
3
  *
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.
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).
10
7
  */
8
+ import { empty, ok, withStashField } from './types.js';
11
9
  import { InputValidator } from '../../utils/InputValidator.js';
10
+ import { ValidationError } from '../../utils/CategorisedError.js';
12
11
  const SESSION_ID_PROP = {
13
12
  session_id: {
14
13
  type: 'string',
@@ -22,7 +21,8 @@ export const tools = [
22
21
  'mode=write replaces the editor contents (default; mirrors the old write tool exactly, including optional pattern validation and auto_play). ' +
23
22
  'mode=append concatenates `code` after the current pattern with a newline. ' +
24
23
  'mode=insert places `code` at the given line `position`. ' +
25
- 'mode=replace runs a single string-replace from `search` to `replace`. ' +
24
+ 'mode=replace substitutes `search` with `replace` — the first occurrence only, unless replace_all is true. ' +
25
+ 'The response reports how many occurrences matched, were replaced, and remain. ' +
26
26
  'mode=clear empties the editor. ' +
27
27
  'Example: edit_pattern({ mode: "write", pattern: "s(\\"bd\\")", auto_play: true }). ' +
28
28
  'For reading the editor without mutating it use get_pattern; for the on-disk pattern catalog use pattern_store.',
@@ -36,9 +36,14 @@ export const tools = [
36
36
  },
37
37
  pattern: { type: 'string', description: 'Pattern code (mode=write)' },
38
38
  code: { type: 'string', description: 'Code to append/insert (mode=append/insert)' },
39
- position: { type: 'number', description: 'Line number (mode=insert)' },
39
+ position: {
40
+ type: 'number',
41
+ description: 'mode=insert: 0-indexed line to insert BEFORE. 0 puts the code at the top; '
42
+ + 'a value equal to the line count appends. Out of range is an error, not an append.',
43
+ },
40
44
  search: { type: 'string', description: 'Text to replace (mode=replace)' },
41
45
  replace: { type: 'string', description: 'Replacement text (mode=replace)' },
46
+ replace_all: { type: 'boolean', description: 'mode=replace: replace every occurrence instead of just the first (default false)' },
42
47
  auto_play: { type: 'boolean', description: 'Start playback after write (mode=write only, default: false)' },
43
48
  validate: { type: 'boolean', description: 'Validate pattern before write (mode=write only, default: true)' },
44
49
  ...SESSION_ID_PROP,
@@ -50,60 +55,6 @@ export const tools = [
50
55
  description: 'Get current pattern code',
51
56
  inputSchema: { type: 'object', properties: { ...SESSION_ID_PROP } },
52
57
  },
53
- {
54
- name: 'write',
55
- description: '[DEPRECATED — use edit_pattern({ mode: "write" }) instead] Write pattern to editor with optional auto-play and validation',
56
- inputSchema: {
57
- type: 'object',
58
- properties: {
59
- pattern: { type: 'string', description: 'Pattern code' },
60
- auto_play: { type: 'boolean', description: 'Start playback immediately after writing (default: false)' },
61
- validate: { type: 'boolean', description: 'Validate pattern before writing (default: true)' },
62
- ...SESSION_ID_PROP,
63
- },
64
- required: ['pattern'],
65
- },
66
- },
67
- {
68
- name: 'append',
69
- description: '[DEPRECATED — use edit_pattern({ mode: "append" }) instead] Append code to current pattern',
70
- inputSchema: {
71
- type: 'object',
72
- properties: { code: { type: 'string', description: 'Code to append' }, ...SESSION_ID_PROP },
73
- required: ['code'],
74
- },
75
- },
76
- {
77
- name: 'insert',
78
- description: '[DEPRECATED — use edit_pattern({ mode: "insert" }) instead] Insert code at specific line',
79
- inputSchema: {
80
- type: 'object',
81
- properties: {
82
- position: { type: 'number', description: 'Line number' },
83
- code: { type: 'string', description: 'Code to insert' },
84
- ...SESSION_ID_PROP,
85
- },
86
- required: ['position', 'code'],
87
- },
88
- },
89
- {
90
- name: 'replace',
91
- description: '[DEPRECATED — use edit_pattern({ mode: "replace" }) instead] Replace pattern section',
92
- inputSchema: {
93
- type: 'object',
94
- properties: {
95
- search: { type: 'string', description: 'Text to replace' },
96
- replace: { type: 'string', description: 'Replacement text' },
97
- ...SESSION_ID_PROP,
98
- },
99
- required: ['search', 'replace'],
100
- },
101
- },
102
- {
103
- name: 'clear',
104
- description: '[DEPRECATED — use edit_pattern({ mode: "clear" }) instead] Clear the editor',
105
- inputSchema: { type: 'object', properties: { ...SESSION_ID_PROP } },
106
- },
107
58
  ];
108
59
  export const toolNames = new Set(tools.map(t => t.name));
109
60
  async function doWrite(args, ctx, sid) {
@@ -141,20 +92,66 @@ async function doAppend(args, ctx, sid) {
141
92
  return await ctx.writePatternSafe(current + '\n' + args.code, sid);
142
93
  }
143
94
  async function doInsert(args, ctx, sid) {
144
- InputValidator.validatePositiveInteger(args.position, 'position');
145
95
  InputValidator.validateStringLength(args.code, 'code', 10000, true);
146
96
  const lines = (await ctx.getCurrentPatternSafe(sid)).split('\n');
147
- lines.splice(args.position, 0, args.code);
97
+ // 0-indexed, and 0 is a legal position.
98
+ //
99
+ // `validatePositiveInteger` rejected 0 while the `splice` below is
100
+ // 0-indexed, so inserting at the TOP of a pattern was impossible
101
+ // through this tool — and `StrudelController.insertAtLine`, which does
102
+ // the same job, documents the parameter as 0-indexed and permits 0.
103
+ //
104
+ // The other end was unguarded: `position: 9999` on a three-line
105
+ // pattern passed validation and `splice` silently appended, where
106
+ // `insertAtLine` throws. Now it matches that contract exactly
107
+ // (#442), so the two ways of doing this cannot disagree.
108
+ const position = args.position;
109
+ if (!Number.isInteger(position) || position < 0 || position > lines.length) {
110
+ throw new ValidationError(`Invalid position: ${String(position)}. Must be an integer between 0 and ${String(lines.length)} ` +
111
+ `(0 inserts before the first line, ${String(lines.length)} appends).`);
112
+ }
113
+ lines.splice(position, 0, args.code);
148
114
  return await ctx.writePatternSafe(lines.join('\n'), sid);
149
115
  }
150
116
  async function doReplace(args, ctx, sid) {
151
117
  InputValidator.validateStringLength(args.search, 'search', 1000, true);
152
118
  InputValidator.validateStringLength(args.replace, 'replace', 10000, true);
119
+ const search = args.search ?? '';
120
+ if (search.length === 0) {
121
+ throw new Error('Invalid search: must be a non-empty string.');
122
+ }
153
123
  const pattern = await ctx.getCurrentPatternSafe(sid);
124
+ // Literal string operations throughout — never a RegExp built from
125
+ // `search`. Interpolating caller text into a pattern is how #236
126
+ // happened: `(a+)+Z` against a crafted subject blocked the event loop
127
+ // for 25 seconds.
128
+ const matches = pattern.split(search).length - 1;
154
129
  // Escape $ in replacement to prevent special sequence injection ($&, $1, $', etc.)
155
130
  const safeReplacement = args.replace.replace(/\$/g, '$$$$');
156
- const replaced = pattern.replace(args.search, safeReplacement);
157
- return await ctx.writePatternSafe(replaced, sid);
131
+ const replaceAll = args.replace_all === true;
132
+ const updated = replaceAll
133
+ ? pattern.replaceAll(search, safeReplacement)
134
+ : pattern.replace(search, safeReplacement);
135
+ const replaced = replaceAll ? matches : Math.min(matches, 1);
136
+ const remaining = matches - replaced;
137
+ const written = await ctx.writePatternSafe(updated, sid);
138
+ // Counts are reported under BOTH settings. Previously a caller had no
139
+ // way to learn that other occurrences survived — and these callers are
140
+ // LLM agents, which do not reliably re-read the pattern to check (#243).
141
+ // Zero matches is a valid-empty: the search ran and found nothing.
142
+ // Wrapped in a plain ok() it was indistinguishable from a replace
143
+ // that actually changed the pattern (#288).
144
+ const wrap = matches === 0 ? empty : ok;
145
+ return wrap(withStashField({
146
+ success: true,
147
+ message: matches === 0
148
+ ? `No occurrences of ${JSON.stringify(search)} found`
149
+ : `Replaced ${String(replaced)} of ${String(matches)} occurrence(s)` +
150
+ (remaining > 0 ? `; ${String(remaining)} remain. Pass replace_all: true to replace them all.` : ''),
151
+ replaced,
152
+ remaining,
153
+ matches,
154
+ }, written));
158
155
  }
159
156
  async function doClear(ctx, sid) {
160
157
  return await ctx.writePatternSafe('', sid);
@@ -177,12 +174,6 @@ export async function execute(name, args, ctx) {
177
174
  throw new Error(`Invalid mode: ${mode}. Must be one of: write, append, insert, replace, clear`);
178
175
  }
179
176
  }
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);
186
177
  case 'get_pattern':
187
178
  return await ctx.getCurrentPatternSafe(sid);
188
179
  default:
@@ -1,18 +1,17 @@
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
  */
12
+ import { withStashNotice } from './types.js';
15
13
  import { InputValidator } from '../../utils/InputValidator.js';
14
+ import { BASS_STYLES, DRUM_STYLES, resolveBassStyle, resolveDrumStyle } from '../../services/StyleRegistry.js';
16
15
  const SESSION_ID_PROP = {
17
16
  session_id: {
18
17
  type: 'string',
@@ -20,21 +19,6 @@ const SESSION_ID_PROP = {
20
19
  },
21
20
  };
22
21
  export const tools = [
23
- {
24
- name: 'generate_pattern',
25
- description: '[DEPRECATED — use compose instead, which auto-inits and returns richer metadata] Generate complete pattern from style with optional auto-play.',
26
- inputSchema: {
27
- type: 'object',
28
- properties: {
29
- style: { type: 'string', description: 'Music style (techno/house/dnb/ambient/etc)' },
30
- key: { type: 'string', description: 'Musical key' },
31
- bpm: { type: 'number', description: 'Tempo in BPM' },
32
- auto_play: { type: 'boolean', description: 'Start playback immediately (default: false)' },
33
- ...SESSION_ID_PROP,
34
- },
35
- required: ['style'],
36
- },
37
- },
38
22
  {
39
23
  name: 'generate_part',
40
24
  description: 'Generate a single instrumental layer and append it to the current session pattern. ' +
@@ -60,51 +44,11 @@ export const tools = [
60
44
  required: ['role'],
61
45
  },
62
46
  },
63
- {
64
- name: 'generate_drums',
65
- description: '[DEPRECATED — use generate_part({ role: "drums" }) instead] Generate drum pattern',
66
- inputSchema: {
67
- type: 'object',
68
- properties: {
69
- style: { type: 'string', description: 'Drum style' },
70
- complexity: { type: 'number', description: 'Complexity (0-1)' },
71
- ...SESSION_ID_PROP,
72
- },
73
- required: ['style'],
74
- },
75
- },
76
- {
77
- name: 'generate_bassline',
78
- description: '[DEPRECATED — use generate_part({ role: "bass" }) instead] Generate bassline',
79
- inputSchema: {
80
- type: 'object',
81
- properties: {
82
- key: { type: 'string', description: 'Musical key' },
83
- style: { type: 'string', description: 'Bass style' },
84
- ...SESSION_ID_PROP,
85
- },
86
- required: ['key', 'style'],
87
- },
88
- },
89
- {
90
- name: 'generate_melody',
91
- description: '[DEPRECATED — use generate_part({ role: "melody" }) instead] Generate melody from scale',
92
- inputSchema: {
93
- type: 'object',
94
- properties: {
95
- scale: { type: 'string', description: 'Scale name' },
96
- root: { type: 'string', description: 'Root note' },
97
- length: { type: 'number', description: 'Number of notes' },
98
- ...SESSION_ID_PROP,
99
- },
100
- required: ['scale', 'root'],
101
- },
102
- },
103
47
  {
104
48
  name: 'music_theory',
105
49
  description: 'Music-theory queries. ' +
106
50
  '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). ' +
51
+ 'query=chord_progression returns a chord progression for the key/style AND writes the resulting chord pattern into the current session. ' +
108
52
  'Example: music_theory({ query: "scale", root: "C", scale: "major" }). ' +
109
53
  'For pattern generation (drums/bass/melody) use generate_part; for rhythmic patterns use generate_rhythm.',
110
54
  inputSchema: {
@@ -120,31 +64,6 @@ export const tools = [
120
64
  required: ['query'],
121
65
  },
122
66
  },
123
- {
124
- name: 'generate_scale',
125
- description: '[DEPRECATED — use music_theory({ query: "scale" }) instead] Generate scale notes',
126
- inputSchema: {
127
- type: 'object',
128
- properties: {
129
- root: { type: 'string', description: 'Root note' },
130
- scale: { type: 'string', description: 'Scale type' },
131
- },
132
- required: ['root', 'scale'],
133
- },
134
- },
135
- {
136
- name: 'generate_chord_progression',
137
- description: '[DEPRECATED — use music_theory({ query: "chord_progression" }) instead] Generate chord progression',
138
- inputSchema: {
139
- type: 'object',
140
- properties: {
141
- key: { type: 'string', description: 'Key' },
142
- style: { type: 'string', description: 'Style (pop/jazz/blues/etc)' },
143
- ...SESSION_ID_PROP,
144
- },
145
- required: ['key', 'style'],
146
- },
147
- },
148
67
  {
149
68
  name: 'generate_rhythm',
150
69
  description: 'Generate a rhythmic pattern and append it to the current session. ' +
@@ -166,52 +85,19 @@ export const tools = [
166
85
  required: ['type'],
167
86
  },
168
87
  },
169
- {
170
- name: 'generate_euclidean',
171
- description: '[DEPRECATED — use generate_rhythm({ type: "euclidean" }) instead] Generate Euclidean rhythm',
172
- inputSchema: {
173
- type: 'object',
174
- properties: {
175
- hits: { type: 'number', description: 'Number of hits' },
176
- steps: { type: 'number', description: 'Total steps' },
177
- sound: { type: 'string', description: 'Sound to use' },
178
- ...SESSION_ID_PROP,
179
- },
180
- required: ['hits', 'steps'],
181
- },
182
- },
183
- {
184
- name: 'generate_polyrhythm',
185
- description: '[DEPRECATED — use generate_rhythm({ type: "polyrhythm" }) instead] Generate polyrhythm',
186
- inputSchema: {
187
- type: 'object',
188
- properties: {
189
- sounds: { type: 'array', items: { type: 'string' }, description: 'Sounds to use' },
190
- patterns: { type: 'array', items: { type: 'number' }, description: 'Pattern numbers' },
191
- ...SESSION_ID_PROP,
192
- },
193
- required: ['sounds', 'patterns'],
194
- },
195
- },
196
- {
197
- name: 'generate_fill',
198
- description: '[DEPRECATED — use generate_part({ role: "fill" }) instead] Generate drum fill',
199
- inputSchema: {
200
- type: 'object',
201
- properties: {
202
- style: { type: 'string', description: 'Fill style' },
203
- bars: { type: 'number', description: 'Number of bars' },
204
- ...SESSION_ID_PROP,
205
- },
206
- required: ['style'],
207
- },
208
- },
209
88
  ];
210
89
  export const toolNames = new Set(tools.map(t => t.name));
90
+ /**
91
+ * Appends generated material to the current pattern and writes it back.
92
+ *
93
+ * @returns Whatever `writePatternSafe` returned — callers must pass it
94
+ * to `withStashNotice` rather than discard it, because a write with
95
+ * no browser up is a stash, not a write (#285).
96
+ */
211
97
  async function appendOrSet(generated, ctx, sessionId) {
212
98
  const current = await ctx.getCurrentPatternSafe(sessionId);
213
99
  const combined = current ? current + '\n' + generated : generated;
214
- await ctx.writePatternSafe(combined, sessionId);
100
+ return await ctx.writePatternSafe(combined, sessionId);
215
101
  }
216
102
  function doScale(args, ctx) {
217
103
  InputValidator.validateRootNote(args.root);
@@ -224,38 +110,52 @@ async function doChordProgression(args, ctx, sid) {
224
110
  InputValidator.validateChordStyle(args.style);
225
111
  const progression = ctx.theory.generateChordProgression(args.key, args.style);
226
112
  const chordPattern = ctx.generator.generateChords(progression);
227
- await appendOrSet(chordPattern, ctx, sid);
228
- return `Generated ${args.style} progression in ${args.key}: ${progression}`;
113
+ const written = await appendOrSet(chordPattern, ctx, sid);
114
+ return withStashNotice(`Generated ${args.style} progression in ${args.key}: ${progression}`, written);
229
115
  }
230
116
  async function doEuclidean(args, ctx, sid) {
231
117
  InputValidator.validateEuclidean(args.hits, args.steps);
232
118
  if (args.sound)
233
119
  InputValidator.validateStringLength(args.sound, 'sound', 100, false);
234
120
  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})`;
121
+ const written = await appendOrSet(euclidean, ctx, sid);
122
+ return withStashNotice(`Generated Euclidean rhythm (${args.hits}/${args.steps})`, written);
237
123
  }
238
124
  async function doPolyrhythm(args, ctx, sid) {
239
125
  args.sounds.forEach((s) => InputValidator.validateStringLength(s, 'sound', 100, false));
240
126
  args.patterns.forEach((p) => InputValidator.validatePositiveInteger(p, 'pattern'));
241
127
  const poly = ctx.generator.generatePolyrhythm(args.sounds, args.patterns);
242
- await appendOrSet(poly, ctx, sid);
243
- return 'Generated polyrhythm';
128
+ const written = await appendOrSet(poly, ctx, sid);
129
+ return withStashNotice('Generated polyrhythm', written);
244
130
  }
245
131
  async function doDrums(args, ctx, sid) {
246
132
  InputValidator.validateStringLength(args.style, 'style', 100, false);
247
133
  if (args.complexity !== undefined)
248
134
  InputValidator.validateNormalizedValue(args.complexity, 'complexity');
135
+ // Say which style actually produced the notes. An unknown genre falls
136
+ // back to techno, and echoing the request back made that invisible (#279).
137
+ const resolution = resolveDrumStyle(args.style);
249
138
  const drums = ctx.generator.generateDrumPattern(args.style, args.complexity || 0.5);
250
- await appendOrSet(drums, ctx, sid);
251
- return `Generated ${args.style} drums`;
139
+ const written = await appendOrSet(drums, ctx, sid);
140
+ return withStashNotice(resolution.supported
141
+ ? `Generated ${resolution.resolved} drums`
142
+ : `No drum pattern for style "${args.style}" — generated ${resolution.resolved} drums instead. ` +
143
+ `Styles with their own drums: ${DRUM_STYLES.join(', ')}.`, written);
252
144
  }
253
145
  async function doBassline(args, ctx, sid) {
254
146
  InputValidator.validateRootNote(args.key);
255
147
  InputValidator.validateStringLength(args.style, 'style', 100, false);
148
+ // role=bass echoed args.style back while the generator had silently
149
+ // fallen back to techno — #279 in the same file, eight lines below
150
+ // the sibling it fixed (#294).
151
+ const resolution = resolveBassStyle(args.style);
256
152
  const bass = ctx.generator.generateBassline(args.key, args.style);
257
- await appendOrSet(bass, ctx, sid);
258
- return `Generated ${args.style} bassline in ${args.key}`;
153
+ const written = await appendOrSet(bass, ctx, sid);
154
+ return withStashNotice(resolution.supported
155
+ ? `Generated ${resolution.resolved} bassline in ${args.key}`
156
+ : `No bassline for style "${args.style}" — generated ${resolution.resolved} ` +
157
+ `bassline in ${args.key} instead. ` +
158
+ `Styles with their own bassline: ${BASS_STYLES.join(', ')}.`, written);
259
159
  }
260
160
  async function doMelody(args, ctx, sid) {
261
161
  InputValidator.validateRootNote(args.root);
@@ -264,34 +164,20 @@ async function doMelody(args, ctx, sid) {
264
164
  InputValidator.validatePositiveInteger(args.length, 'length');
265
165
  const scale = ctx.theory.generateScale(args.root, args.scale);
266
166
  const melody = ctx.generator.generateMelody(scale, args.length || 8);
267
- await appendOrSet(melody, ctx, sid);
268
- return `Generated melody in ${args.root} ${args.scale}`;
167
+ const written = await appendOrSet(melody, ctx, sid);
168
+ return withStashNotice(`Generated melody in ${args.root} ${args.scale}`, written);
269
169
  }
270
170
  async function doFill(args, ctx, sid) {
271
171
  InputValidator.validateStringLength(args.style, 'style', 100, false);
272
172
  if (args.bars !== undefined)
273
173
  InputValidator.validatePositiveInteger(args.bars, 'bars');
274
174
  const fill = ctx.generator.generateFill(args.style, args.bars || 1);
275
- await appendOrSet(fill, ctx, sid);
276
- return `Generated ${args.bars || 1} bar fill`;
175
+ const written = await appendOrSet(fill, ctx, sid);
176
+ return withStashNotice(`Generated ${args.bars || 1} bar fill`, written);
277
177
  }
278
178
  export async function execute(name, args, ctx) {
279
179
  const sid = args?.session_id;
280
180
  switch (name) {
281
- case 'generate_pattern': {
282
- InputValidator.validateStringLength(args.style, 'style', 100, false);
283
- if (args.key)
284
- InputValidator.validateRootNote(args.key);
285
- if (args.bpm !== undefined)
286
- InputValidator.validateBPM(args.bpm);
287
- const generated = ctx.generator.generateCompletePattern(args.style, args.key || 'C', args.bpm || 120);
288
- await ctx.writePatternSafe(generated, sid);
289
- if (args.auto_play && (sid || ctx.isInitialized())) {
290
- await ctx.getController(sid).play();
291
- return `Generated ${args.style} pattern. Playing.`;
292
- }
293
- return `Generated ${args.style} pattern`;
294
- }
295
181
  case 'generate_part': {
296
182
  const role = args?.role;
297
183
  switch (role) {
@@ -303,9 +189,6 @@ export async function execute(name, args, ctx) {
303
189
  throw new Error(`Invalid role: ${role}. Must be one of: drums, bass, melody, fill`);
304
190
  }
305
191
  }
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
192
  case 'music_theory': {
310
193
  const q = args?.query;
311
194
  if (q !== 'scale' && q !== 'chord_progression') {
@@ -313,8 +196,6 @@ export async function execute(name, args, ctx) {
313
196
  }
314
197
  return q === 'scale' ? doScale(args, ctx) : await doChordProgression(args, ctx, sid);
315
198
  }
316
- case 'generate_scale': return doScale(args, ctx);
317
- case 'generate_chord_progression': return await doChordProgression(args, ctx, sid);
318
199
  case 'generate_rhythm': {
319
200
  const t = args?.type;
320
201
  if (t !== 'euclidean' && t !== 'polyrhythm') {
@@ -324,9 +205,6 @@ export async function execute(name, args, ctx) {
324
205
  ? await doEuclidean(args, ctx, sid)
325
206
  : await doPolyrhythm(args, ctx, sid);
326
207
  }
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);
330
208
  default:
331
209
  throw new Error(`generate module does not handle tool: ${name}`);
332
210
  }