@williamzujkowski/live-coding-music-mcp 2.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.
@@ -50,73 +50,131 @@ function transposePattern(pattern, semitones) {
50
50
  return noteNames[newNoteValue] + newOctave;
51
51
  });
52
52
  }
53
+ const SESSION_ID_PROP = {
54
+ session_id: {
55
+ type: 'string',
56
+ description: 'Optional session ID (#108). Omit to use default session.',
57
+ },
58
+ };
53
59
  export const tools = [
60
+ {
61
+ name: 'transform',
62
+ description: 'Apply a single transform op to the current session pattern. ' +
63
+ 'op=transpose shifts notes by `semitones`. ' +
64
+ 'op=reverse appends `.rev` to the pattern. ' +
65
+ 'op=stretch slows by `factor` (>1 slower, <1 faster). ' +
66
+ 'op=quantize snaps to the `grid` (e.g. "1/16"). ' +
67
+ 'op=humanize adds rand-nudge timing of `amount` (0-1). ' +
68
+ 'op=swing applies `.swing(amount)` (alias for add_swing). ' +
69
+ 'op=scale applies a `root`/`scale` filter to notes (alias for apply_scale). ' +
70
+ 'op=vary returns a variation of `type` (subtle/moderate/extreme/glitch/evolving — note: this is what generate_variation does today; the name is misleading because it transforms rather than generates). ' +
71
+ 'Example: transform({ op: "transpose", semitones: 7 }). ' +
72
+ 'For effects (add/remove) use effect; for mood/energy/refine use shape; for tempo use set_tempo.',
73
+ inputSchema: {
74
+ type: 'object',
75
+ properties: {
76
+ op: {
77
+ type: 'string',
78
+ enum: ['transpose', 'reverse', 'stretch', 'quantize', 'humanize', 'swing', 'scale', 'vary'],
79
+ description: 'Which transform to apply',
80
+ },
81
+ semitones: { type: 'number', description: 'op=transpose: integer semitones to shift' },
82
+ factor: { type: 'number', description: 'op=stretch: stretch factor' },
83
+ grid: { type: 'string', description: 'op=quantize: grid size (e.g. "1/16")' },
84
+ amount: { type: 'number', description: 'op=humanize/swing: amount 0-1' },
85
+ root: { type: 'string', description: 'op=scale: root note (e.g. "C")' },
86
+ scale: { type: 'string', description: 'op=scale: scale name (e.g. "minor")' },
87
+ type: { type: 'string', description: 'op=vary: variation type (subtle/moderate/extreme/glitch/evolving)' },
88
+ ...SESSION_ID_PROP,
89
+ },
90
+ required: ['op'],
91
+ },
92
+ },
54
93
  {
55
94
  name: 'transpose',
56
- description: 'Transpose notes by semitones',
95
+ description: '[DEPRECATED — use transform({ op: "transpose" }) instead] Transpose notes by semitones',
57
96
  inputSchema: {
58
97
  type: 'object',
59
- properties: { semitones: { type: 'number', description: 'Semitones to transpose' } },
98
+ properties: { semitones: { type: 'number', description: 'Semitones to transpose' }, ...SESSION_ID_PROP },
60
99
  required: ['semitones'],
61
100
  },
62
101
  },
63
102
  {
64
103
  name: 'reverse',
65
- description: 'Reverse pattern',
66
- inputSchema: { type: 'object', properties: {} },
104
+ description: '[DEPRECATED — use transform({ op: "reverse" }) instead] Reverse pattern',
105
+ inputSchema: { type: 'object', properties: { ...SESSION_ID_PROP } },
67
106
  },
68
107
  {
69
108
  name: 'stretch',
70
- description: 'Time stretch pattern',
109
+ description: '[DEPRECATED — use transform({ op: "stretch" }) instead] Time stretch pattern',
71
110
  inputSchema: {
72
111
  type: 'object',
73
- properties: { factor: { type: 'number', description: 'Stretch factor' } },
112
+ properties: { factor: { type: 'number', description: 'Stretch factor' }, ...SESSION_ID_PROP },
74
113
  required: ['factor'],
75
114
  },
76
115
  },
77
116
  {
78
117
  name: 'quantize',
79
- description: 'Quantize to grid',
118
+ description: '[DEPRECATED — use transform({ op: "quantize" }) instead] Quantize to grid',
80
119
  inputSchema: {
81
120
  type: 'object',
82
- properties: { grid: { type: 'string', description: 'Grid size (e.g., "1/16")' } },
121
+ properties: { grid: { type: 'string', description: 'Grid size (e.g., "1/16")' }, ...SESSION_ID_PROP },
83
122
  required: ['grid'],
84
123
  },
85
124
  },
86
125
  {
87
126
  name: 'humanize',
88
- description: 'Add human timing variation',
127
+ description: '[DEPRECATED — use transform({ op: "humanize" }) instead] Add human timing variation',
89
128
  inputSchema: {
90
129
  type: 'object',
91
- properties: { amount: { type: 'number', description: 'Humanization amount (0-1)' } },
130
+ properties: { amount: { type: 'number', description: 'Humanization amount (0-1)' }, ...SESSION_ID_PROP },
92
131
  },
93
132
  },
94
133
  {
95
134
  name: 'generate_variation',
96
- description: 'Create pattern variations',
135
+ description: '[DEPRECATED — use transform({ op: "vary" }) instead] Create pattern variations (mis-named today; it transforms, not generates)',
97
136
  inputSchema: {
98
137
  type: 'object',
99
- properties: { type: { type: 'string', description: 'Variation type (subtle/moderate/extreme/glitch/evolving)' } },
138
+ properties: { type: { type: 'string', description: 'Variation type (subtle/moderate/extreme/glitch/evolving)' }, ...SESSION_ID_PROP },
139
+ },
140
+ },
141
+ {
142
+ name: 'effect',
143
+ description: 'Add or remove a Strudel effect on the current session pattern. ' +
144
+ 'action=add appends `.<effect>(<params>)`. ' +
145
+ 'action=remove strips the last `.<effect>(...)` call from the pattern. ' +
146
+ 'Example: effect({ action: "add", effect: "lpf", params: "1000" }). ' +
147
+ 'For higher-level effect bundles (mood/energy/refine) use shape; for raw transforms use transform.',
148
+ inputSchema: {
149
+ type: 'object',
150
+ properties: {
151
+ action: { type: 'string', enum: ['add', 'remove'], description: 'add or remove the effect' },
152
+ effect: { type: 'string', description: 'Effect name (e.g. lpf, room, delay)' },
153
+ params: { type: 'string', description: 'Effect parameters (action=add only)' },
154
+ ...SESSION_ID_PROP,
155
+ },
156
+ required: ['action', 'effect'],
100
157
  },
101
158
  },
102
159
  {
103
160
  name: 'add_effect',
104
- description: 'Add effect to pattern',
161
+ description: '[DEPRECATED — use effect({ action: "add" }) instead] Add effect to pattern',
105
162
  inputSchema: {
106
163
  type: 'object',
107
164
  properties: {
108
165
  effect: { type: 'string', description: 'Effect name' },
109
166
  params: { type: 'string', description: 'Effect parameters' },
167
+ ...SESSION_ID_PROP,
110
168
  },
111
169
  required: ['effect'],
112
170
  },
113
171
  },
114
172
  {
115
173
  name: 'remove_effect',
116
- description: 'Remove effect',
174
+ description: '[DEPRECATED — use effect({ action: "remove" }) instead] Remove effect',
117
175
  inputSchema: {
118
176
  type: 'object',
119
- properties: { effect: { type: 'string', description: 'Effect to remove' } },
177
+ properties: { effect: { type: 'string', description: 'Effect to remove' }, ...SESSION_ID_PROP },
120
178
  required: ['effect'],
121
179
  },
122
180
  },
@@ -125,34 +183,62 @@ export const tools = [
125
183
  description: 'Set BPM',
126
184
  inputSchema: {
127
185
  type: 'object',
128
- properties: { bpm: { type: 'number', description: 'Tempo in BPM' } },
186
+ properties: { bpm: { type: 'number', description: 'Tempo in BPM' }, ...SESSION_ID_PROP },
129
187
  required: ['bpm'],
130
188
  },
131
189
  },
132
190
  {
133
191
  name: 'add_swing',
134
- description: 'Add swing to pattern',
192
+ description: '[DEPRECATED — use transform({ op: "swing" }) instead] Add swing to pattern',
135
193
  inputSchema: {
136
194
  type: 'object',
137
- properties: { amount: { type: 'number', description: 'Swing amount (0-1)' } },
195
+ properties: { amount: { type: 'number', description: 'Swing amount (0-1)' }, ...SESSION_ID_PROP },
138
196
  required: ['amount'],
139
197
  },
140
198
  },
141
199
  {
142
200
  name: 'apply_scale',
143
- description: 'Apply scale to notes',
201
+ description: '[DEPRECATED — use transform({ op: "scale" }) instead] Apply scale to notes',
144
202
  inputSchema: {
145
203
  type: 'object',
146
204
  properties: {
147
205
  scale: { type: 'string', description: 'Scale name' },
148
206
  root: { type: 'string', description: 'Root note' },
207
+ ...SESSION_ID_PROP,
149
208
  },
150
209
  required: ['scale', 'root'],
151
210
  },
152
211
  },
212
+ {
213
+ name: 'shape',
214
+ description: 'Shape the current pattern along one of three high-level dimensions. ' +
215
+ 'dimension=mood applies a mood profile (dark/euphoric/melancholic/aggressive/dreamy/peaceful/energetic) with optional intensity 0-1. ' +
216
+ 'dimension=energy applies an energy level (integer 0-10). ' +
217
+ 'dimension=refine applies a directional refinement: faster/slower/louder/quieter/brighter/darker/"more reverb"/drier. ' +
218
+ 'All three auto-play by default. ' +
219
+ 'Example: shape({ dimension: "mood", target_mood: "dark", intensity: 0.8 }). ' +
220
+ 'For raw transform ops use transform; for explicit effects use effect.',
221
+ inputSchema: {
222
+ type: 'object',
223
+ properties: {
224
+ dimension: { type: 'string', enum: ['mood', 'energy', 'refine'], description: 'Which dimension to shape along' },
225
+ target_mood: {
226
+ type: 'string',
227
+ enum: ['dark', 'euphoric', 'melancholic', 'aggressive', 'dreamy', 'peaceful', 'energetic'],
228
+ description: 'dimension=mood: target mood',
229
+ },
230
+ intensity: { type: 'number', minimum: 0, maximum: 1, description: 'dimension=mood: intensity 0-1 (default 0.5)' },
231
+ level: { type: 'number', description: 'dimension=energy: integer level 0-10' },
232
+ direction: { type: 'string', description: 'dimension=refine: faster, slower, louder, quieter, brighter, darker, "more reverb", drier' },
233
+ auto_play: { type: 'boolean', description: 'Start playback after shape (default true)' },
234
+ ...SESSION_ID_PROP,
235
+ },
236
+ required: ['dimension'],
237
+ },
238
+ },
153
239
  {
154
240
  name: 'shift_mood',
155
- description: 'Transform current pattern to match a different emotional mood by adjusting tempo, effects, and note choices. Moods: dark, euphoric, melancholic, aggressive, dreamy, peaceful, energetic.',
241
+ description: '[DEPRECATED — use shape({ dimension: "mood" }) instead] Transform current pattern to match a different emotional mood. Moods: dark, euphoric, melancholic, aggressive, dreamy, peaceful, energetic.',
156
242
  inputSchema: {
157
243
  type: 'object',
158
244
  properties: {
@@ -163,122 +249,170 @@ export const tools = [
163
249
  },
164
250
  intensity: { type: 'number', minimum: 0, maximum: 1, description: 'How strongly to apply the mood transformation (0-1, default: 0.5)' },
165
251
  auto_play: { type: 'boolean', description: 'Start playback after transformation (default: true)' },
252
+ ...SESSION_ID_PROP,
166
253
  },
167
254
  required: ['target_mood'],
168
255
  },
169
256
  },
170
257
  {
171
258
  name: 'set_energy',
172
- description: 'Adjust the overall energy level of the current pattern on a 0-10 scale. 0: minimal/ambient, 1-2: sparse, 3-4: light/relaxed, 5-6: normal/moderate, 7-8: driving/intense, 9-10: maximum. Auto-plays after applying energy level.',
259
+ description: '[DEPRECATED — use shape({ dimension: "energy" }) instead] Adjust energy level 0-10. Auto-plays after applying.',
173
260
  inputSchema: {
174
261
  type: 'object',
175
- properties: { level: { type: 'number', description: 'Energy level from 0 (minimal) to 10 (maximum)' } },
262
+ properties: { level: { type: 'number', description: 'Energy level from 0 (minimal) to 10 (maximum)' }, ...SESSION_ID_PROP },
176
263
  required: ['level'],
177
264
  },
178
265
  },
179
266
  {
180
267
  name: 'refine',
181
- description: 'Incrementally refine the current pattern with simple directional commands. Supports: faster/slower (tempo), louder/quieter (gain), brighter/darker (filter cutoff), "more reverb"/drier (reverb). Auto-plays after applying refinement.',
268
+ description: '[DEPRECATED — use shape({ dimension: "refine" }) instead] Incrementally refine current pattern: faster/slower/louder/quieter/brighter/darker/"more reverb"/drier.',
182
269
  inputSchema: {
183
270
  type: 'object',
184
- properties: { direction: { type: 'string', description: 'Refinement direction: faster, slower, louder, quieter, brighter, darker, "more reverb", or drier' } },
271
+ properties: { direction: { type: 'string', description: 'Refinement direction: faster, slower, louder, quieter, brighter, darker, "more reverb", or drier' }, ...SESSION_ID_PROP },
185
272
  required: ['direction'],
186
273
  },
187
274
  },
188
275
  ];
189
276
  export const toolNames = new Set(tools.map(t => t.name));
277
+ // ---- per-op helpers (shared by transform({op}) and the deprecated aliases)
278
+ async function opTranspose(args, ctx, sid) {
279
+ if (typeof args.semitones !== 'number' || !Number.isInteger(args.semitones)) {
280
+ throw new Error('Semitones must be an integer');
281
+ }
282
+ const p = await ctx.getCurrentPatternSafe(sid);
283
+ await ctx.writePatternSafe(transposePattern(p, args.semitones), sid);
284
+ return `Transposed ${args.semitones} semitones`;
285
+ }
286
+ async function opReverse(ctx, sid) {
287
+ const p = await ctx.getCurrentPatternSafe(sid);
288
+ await ctx.writePatternSafe(p + '.rev', sid);
289
+ return 'Pattern reversed';
290
+ }
291
+ async function opStretch(args, ctx, sid) {
292
+ InputValidator.validateGain(args.factor);
293
+ const p = await ctx.getCurrentPatternSafe(sid);
294
+ await ctx.writePatternSafe(p + `.slow(${args.factor})`, sid);
295
+ return `Stretched by factor of ${args.factor}`;
296
+ }
297
+ async function opQuantize(args, ctx, sid) {
298
+ InputValidator.validateStringLength(args.grid, 'grid', 50, false);
299
+ const p = await ctx.getCurrentPatternSafe(sid);
300
+ await ctx.writePatternSafe(p + `.struct("${args.grid}")`, sid);
301
+ return `Quantized to ${args.grid} grid`;
302
+ }
303
+ async function opHumanize(args, ctx, sid) {
304
+ if (args.amount !== undefined)
305
+ InputValidator.validateNormalizedValue(args.amount, 'amount');
306
+ const p = await ctx.getCurrentPatternSafe(sid);
307
+ const amt = args.amount || 0.01;
308
+ await ctx.writePatternSafe(p + `.nudge(rand.range(-${amt}, ${amt}))`, sid);
309
+ return 'Added human timing';
310
+ }
311
+ async function opVary(args, ctx, sid) {
312
+ const p = await ctx.getCurrentPatternSafe(sid);
313
+ const varied = ctx.generator.generateVariation(p, args.type || 'subtle');
314
+ await ctx.writePatternSafe(varied, sid);
315
+ return `Added ${args.type || 'subtle'} variation`;
316
+ }
317
+ async function opSwing(args, ctx, sid) {
318
+ InputValidator.validateNormalizedValue(args.amount, 'amount');
319
+ const p = await ctx.getCurrentPatternSafe(sid);
320
+ await ctx.writePatternSafe(p + `.swing(${args.amount})`, sid);
321
+ return `Added swing: ${args.amount}`;
322
+ }
323
+ async function opScale(args, ctx, sid) {
324
+ InputValidator.validateStringLength(args.scale, 'scale', 50, false);
325
+ InputValidator.validateRootNote(args.root);
326
+ const p = await ctx.getCurrentPatternSafe(sid);
327
+ await ctx.writePatternSafe(p + `.scale("${args.root}:${args.scale}")`, sid);
328
+ return `Applied ${args.root} ${args.scale} scale`;
329
+ }
330
+ async function opEffectAdd(args, ctx, sid) {
331
+ InputValidator.validateStringLength(args.effect, 'effect', 100, false);
332
+ if (args.params)
333
+ InputValidator.validateStringLength(args.params, 'params', 1000, true);
334
+ const p = await ctx.getCurrentPatternSafe(sid);
335
+ const withEffect = args.params
336
+ ? p + `.${args.effect}(${args.params})`
337
+ : p + `.${args.effect}()`;
338
+ await ctx.writePatternSafe(withEffect, sid);
339
+ return `Added ${args.effect} effect`;
340
+ }
341
+ async function opEffectRemove(args, ctx, sid) {
342
+ InputValidator.validateStringLength(args.effect, 'effect', 100, false);
343
+ const p = await ctx.getCurrentPatternSafe(sid);
344
+ const regex = new RegExp(`\\.${args.effect}\\([^)]*\\)`, 'g');
345
+ const stripped = p.replace(regex, '');
346
+ if (stripped === p)
347
+ return `No ${args.effect} effect found to remove`;
348
+ await ctx.writePatternSafe(stripped, sid);
349
+ return `Removed ${args.effect} effect`;
350
+ }
190
351
  export async function execute(name, args, ctx) {
352
+ const sid = args?.session_id;
353
+ if (!sid && !ctx.isInitialized()) {
354
+ return 'Browser not initialized. Run init first.';
355
+ }
191
356
  switch (name) {
192
- case 'transpose': {
193
- if (typeof args.semitones !== 'number' || !Number.isInteger(args.semitones)) {
194
- throw new Error('Semitones must be an integer');
357
+ case 'transform': {
358
+ const op = args?.op;
359
+ switch (op) {
360
+ case 'transpose': return await opTranspose(args, ctx, sid);
361
+ case 'reverse': return await opReverse(ctx, sid);
362
+ case 'stretch': return await opStretch(args, ctx, sid);
363
+ case 'quantize': return await opQuantize(args, ctx, sid);
364
+ case 'humanize': return await opHumanize(args, ctx, sid);
365
+ case 'swing': return await opSwing(args, ctx, sid);
366
+ case 'scale': return await opScale(args, ctx, sid);
367
+ case 'vary': return await opVary(args, ctx, sid);
368
+ default:
369
+ throw new Error(`Invalid op: ${op}. Must be one of: transpose, reverse, stretch, quantize, humanize, swing, scale, vary`);
195
370
  }
196
- const p = await ctx.getCurrentPatternSafe();
197
- await ctx.writePatternSafe(transposePattern(p, args.semitones));
198
- return `Transposed ${args.semitones} semitones`;
199
- }
200
- case 'reverse': {
201
- const p = await ctx.getCurrentPatternSafe();
202
- await ctx.writePatternSafe(p + '.rev');
203
- return 'Pattern reversed';
204
- }
205
- case 'stretch': {
206
- InputValidator.validateGain(args.factor);
207
- const p = await ctx.getCurrentPatternSafe();
208
- await ctx.writePatternSafe(p + `.slow(${args.factor})`);
209
- return `Stretched by factor of ${args.factor}`;
210
- }
211
- case 'quantize': {
212
- InputValidator.validateStringLength(args.grid, 'grid', 50, false);
213
- const p = await ctx.getCurrentPatternSafe();
214
- await ctx.writePatternSafe(p + `.struct("${args.grid}")`);
215
- return `Quantized to ${args.grid} grid`;
216
371
  }
217
- case 'humanize': {
218
- if (args.amount !== undefined)
219
- InputValidator.validateNormalizedValue(args.amount, 'amount');
220
- const p = await ctx.getCurrentPatternSafe();
221
- const amt = args.amount || 0.01;
222
- await ctx.writePatternSafe(p + `.nudge(rand.range(-${amt}, ${amt}))`);
223
- return 'Added human timing';
224
- }
225
- case 'generate_variation': {
226
- const p = await ctx.getCurrentPatternSafe();
227
- const varied = ctx.generator.generateVariation(p, args.type || 'subtle');
228
- await ctx.writePatternSafe(varied);
229
- return `Added ${args.type || 'subtle'} variation`;
230
- }
231
- case 'add_effect': {
232
- InputValidator.validateStringLength(args.effect, 'effect', 100, false);
233
- if (args.params)
234
- InputValidator.validateStringLength(args.params, 'params', 1000, true);
235
- const p = await ctx.getCurrentPatternSafe();
236
- const withEffect = args.params
237
- ? p + `.${args.effect}(${args.params})`
238
- : p + `.${args.effect}()`;
239
- await ctx.writePatternSafe(withEffect);
240
- return `Added ${args.effect} effect`;
241
- }
242
- case 'remove_effect': {
243
- InputValidator.validateStringLength(args.effect, 'effect', 100, false);
244
- const p = await ctx.getCurrentPatternSafe();
245
- const regex = new RegExp(`\\.${args.effect}\\([^)]*\\)`, 'g');
246
- const stripped = p.replace(regex, '');
247
- if (stripped === p)
248
- return `No ${args.effect} effect found to remove`;
249
- await ctx.writePatternSafe(stripped);
250
- return `Removed ${args.effect} effect`;
372
+ // Deprecated aliases — forward to per-op helpers.
373
+ case 'transpose': return await opTranspose(args, ctx, sid);
374
+ case 'reverse': return await opReverse(ctx, sid);
375
+ case 'stretch': return await opStretch(args, ctx, sid);
376
+ case 'quantize': return await opQuantize(args, ctx, sid);
377
+ case 'humanize': return await opHumanize(args, ctx, sid);
378
+ case 'generate_variation': return await opVary(args, ctx, sid);
379
+ case 'add_swing': return await opSwing(args, ctx, sid);
380
+ case 'apply_scale': return await opScale(args, ctx, sid);
381
+ case 'effect': {
382
+ const a = args?.action;
383
+ if (a !== 'add' && a !== 'remove') {
384
+ throw new Error(`Invalid action: ${a}. Must be one of: add, remove`);
385
+ }
386
+ return a === 'add'
387
+ ? await opEffectAdd(args, ctx, sid)
388
+ : await opEffectRemove(args, ctx, sid);
251
389
  }
390
+ case 'add_effect': return await opEffectAdd(args, ctx, sid);
391
+ case 'remove_effect': return await opEffectRemove(args, ctx, sid);
252
392
  case 'set_tempo': {
253
393
  InputValidator.validateBPM(args.bpm);
254
- const p = await ctx.getCurrentPatternSafe();
255
- await ctx.writePatternSafe(`setcpm(${args.bpm})\n${p}`);
394
+ const p = await ctx.getCurrentPatternSafe(sid);
395
+ await ctx.writePatternSafe(`setcpm(${args.bpm})\n${p}`, sid);
256
396
  return `Set tempo to ${args.bpm} BPM`;
257
397
  }
258
- case 'add_swing': {
259
- InputValidator.validateNormalizedValue(args.amount, 'amount');
260
- const p = await ctx.getCurrentPatternSafe();
261
- await ctx.writePatternSafe(p + `.swing(${args.amount})`);
262
- return `Added swing: ${args.amount}`;
263
- }
264
- case 'apply_scale': {
265
- InputValidator.validateStringLength(args.scale, 'scale', 50, false);
266
- InputValidator.validateRootNote(args.root);
267
- const p = await ctx.getCurrentPatternSafe();
268
- await ctx.writePatternSafe(p + `.scale("${args.root}:${args.scale}")`);
269
- return `Applied ${args.root} ${args.scale} scale`;
398
+ case 'shape': {
399
+ const dim = args?.dimension;
400
+ switch (dim) {
401
+ case 'mood': return await shiftMood(args, ctx, sid);
402
+ case 'energy': return await setEnergyLevel(args.level, ctx, sid);
403
+ case 'refine': return await refinePattern(args.direction, ctx, sid);
404
+ default:
405
+ throw new Error(`Invalid dimension: ${dim}. Must be one of: mood, energy, refine`);
406
+ }
270
407
  }
271
- case 'shift_mood':
272
- return await shiftMood(args, ctx);
273
- case 'set_energy':
274
- return await setEnergyLevel(args.level, ctx);
275
- case 'refine':
276
- return await refinePattern(args.direction, ctx);
408
+ case 'shift_mood': return await shiftMood(args, ctx, sid);
409
+ case 'set_energy': return await setEnergyLevel(args.level, ctx, sid);
410
+ case 'refine': return await refinePattern(args.direction, ctx, sid);
277
411
  default:
278
412
  throw new Error(`transform module does not handle tool: ${name}`);
279
413
  }
280
414
  }
281
- async function shiftMood(args, ctx) {
415
+ async function shiftMood(args, ctx, sid) {
282
416
  const mood = args.target_mood?.toLowerCase()?.trim();
283
417
  const profile = MOOD_PROFILES[mood];
284
418
  if (!profile) {
@@ -287,7 +421,7 @@ async function shiftMood(args, ctx) {
287
421
  error: `Unknown mood: ${args.target_mood}. Valid moods: ${Object.keys(MOOD_PROFILES).join(', ')}.`,
288
422
  };
289
423
  }
290
- const pattern = await ctx.getCurrentPatternSafe();
424
+ const pattern = await ctx.getCurrentPatternSafe(sid);
291
425
  if (!pattern || pattern.trim().length === 0) {
292
426
  return { success: false, error: 'No pattern to transform. Write a pattern first.' };
293
427
  }
@@ -329,17 +463,17 @@ async function shiftMood(args, ctx) {
329
463
  result += `.gain(${g.toFixed(2)})`;
330
464
  applied.push(`gain ${profile.gainMod > 0 ? '+' : ''}${Math.round(profile.gainMod * 100 * intensity)}%`);
331
465
  }
332
- await ctx.writePatternSafe(result);
333
- if (args.auto_play !== false && ctx.isInitialized()) {
334
- await ctx.controller.play();
466
+ await ctx.writePatternSafe(result, sid);
467
+ if (args.auto_play !== false && (sid || ctx.isInitialized())) {
468
+ await ctx.getController(sid).play();
335
469
  }
336
470
  return { success: true, target_mood: mood, intensity, applied_effects: applied };
337
471
  }
338
- async function setEnergyLevel(level, ctx) {
472
+ async function setEnergyLevel(level, ctx, sid) {
339
473
  if (level < 0 || level > 10 || !Number.isInteger(level)) {
340
474
  return { success: false, error: 'Energy level must be an integer from 0 to 10.' };
341
475
  }
342
- const pattern = await ctx.getCurrentPatternSafe();
476
+ const pattern = await ctx.getCurrentPatternSafe(sid);
343
477
  if (!pattern || pattern.trim().length === 0) {
344
478
  return { success: false, error: 'No pattern to adjust. Write a pattern first.' };
345
479
  }
@@ -348,13 +482,13 @@ async function setEnergyLevel(level, ctx) {
348
482
  if (config.densityAdjust)
349
483
  result += config.densityAdjust;
350
484
  result += `.room(${config.roomAmount})`;
351
- await ctx.writePatternSafe(result);
352
- if (ctx.isInitialized())
353
- await ctx.controller.play();
485
+ await ctx.writePatternSafe(result, sid);
486
+ if (sid || ctx.isInitialized())
487
+ await ctx.getController(sid).play();
354
488
  return { success: true, level, description: config.description };
355
489
  }
356
- async function refinePattern(direction, ctx) {
357
- const pattern = await ctx.getCurrentPatternSafe();
490
+ async function refinePattern(direction, ctx, sid) {
491
+ const pattern = await ctx.getCurrentPatternSafe(sid);
358
492
  if (!pattern || pattern.trim().length === 0) {
359
493
  return { success: false, error: 'No pattern to refine. Write a pattern first.' };
360
494
  }
@@ -391,9 +525,9 @@ async function refinePattern(direction, ctx) {
391
525
  error: `Unknown refinement direction: ${direction}. Supported: faster, slower, louder, quieter, brighter, darker, "more reverb", drier.`,
392
526
  };
393
527
  }
394
- await ctx.writePatternSafe(pattern + modification);
395
- if (ctx.isInitialized())
396
- await ctx.controller.play();
528
+ await ctx.writePatternSafe(pattern + modification, sid);
529
+ if (sid || ctx.isInitialized())
530
+ await ctx.getController(sid).play();
397
531
  return { success: true, direction: dir, applied: modification };
398
532
  }
399
533
  export const transformModule = { tools, toolNames, execute };
@@ -6,4 +6,69 @@
6
6
  * dispatcher. The server keeps the MCP protocol layer thin and delegates
7
7
  * to these modules.
8
8
  */
9
- export {};
9
+ /** Type guard for tools and dispatchers. */
10
+ export function isEnvelope(v) {
11
+ return (typeof v === 'object' &&
12
+ v !== null &&
13
+ 'ok' in v &&
14
+ typeof v.ok === 'boolean');
15
+ }
16
+ /** Construct a success envelope. */
17
+ export function ok(data) {
18
+ return { ok: true, data };
19
+ }
20
+ /**
21
+ * Construct a success envelope that represents a valid-empty result —
22
+ * the call worked but produced nothing (no patterns found, empty
23
+ * history, etc.). Distinct from `err` so agents can choose between
24
+ * "report nothing" and "retry/setup".
25
+ */
26
+ export function empty(data) {
27
+ return { ok: true, data, empty: true };
28
+ }
29
+ /** Construct an error envelope. */
30
+ export function err(category, message, opts) {
31
+ return {
32
+ ok: false,
33
+ errorCategory: category,
34
+ // Default retryability follows category: transient retries, others don't.
35
+ isRetryable: opts?.isRetryable ?? category === 'transient',
36
+ message,
37
+ ...(opts?.partialResult !== undefined ? { partialResult: opts.partialResult } : {}),
38
+ };
39
+ }
40
+ /**
41
+ * Best-effort error categorization for raw thrown errors. Used by the
42
+ * dispatcher when a tool throws without wrapping. Tools that know better
43
+ * should construct `err(category, message)` directly.
44
+ */
45
+ export function categorizeError(error) {
46
+ const message = error instanceof Error ? error.message : String(error);
47
+ const lower = message.toLowerCase();
48
+ if (lower.includes('not initialized') ||
49
+ lower.includes("run 'init' first") ||
50
+ lower.includes('run init first') ||
51
+ lower.includes('not found') ||
52
+ lower.includes('already exists')) {
53
+ return 'business';
54
+ }
55
+ if (lower.includes('invalid') ||
56
+ lower.includes('must be') ||
57
+ lower.includes('required') ||
58
+ lower.includes('out of range')) {
59
+ return 'validation';
60
+ }
61
+ if (lower.includes('gemini') ||
62
+ lower.includes('api key') ||
63
+ lower.includes('unauthorized') ||
64
+ lower.includes('forbidden')) {
65
+ return 'permission';
66
+ }
67
+ if (lower.includes('timeout') ||
68
+ lower.includes('econnrefused') ||
69
+ lower.includes('network') ||
70
+ lower.includes('fetch failed')) {
71
+ return 'transient';
72
+ }
73
+ return 'internal';
74
+ }
@@ -89,7 +89,7 @@ export class AudioCaptureService {
89
89
  capture.chunks.push(event.data);
90
90
  }
91
91
  };
92
- capture.recorder.onerror = (event) => {
92
+ capture.recorder.onerror = (_event) => {
93
93
  capture.error = 'MediaRecorder error occurred';
94
94
  capture.isCapturing = false;
95
95
  };
@@ -157,10 +157,11 @@ export class AudioCaptureService {
157
157
  * Starts audio capture on the page.
158
158
  *
159
159
  * @param page - Playwright page instance
160
- * @param config - Optional capture configuration
160
+ * @param _config - Optional capture configuration (currently unused;
161
+ * recorder config is fixed at injectRecorder time)
161
162
  * @throws {Error} When capture fails to start
162
163
  */
163
- async startCapture(page, config) {
164
+ async startCapture(page, _config) {
164
165
  const result = await page.evaluate(/* istanbul ignore next */ () => {
165
166
  const capture = window.strudelAudioCapture;
166
167
  if (!capture) {
@@ -549,7 +549,7 @@ Focus on:
549
549
  return prompt;
550
550
  }
551
551
  buildVariationPrompt(pattern, style) {
552
- let prompt = `You are an expert in Strudel.cc live coding music.
552
+ const prompt = `You are an expert in Strudel.cc live coding music.
553
553
 
554
554
  Given this pattern:
555
555
  \`\`\`javascript