@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.
- package/README.md +222 -231
- package/dist/AudioAnalyzer.js +0 -1
- package/dist/PatternStore.js +0 -1
- package/dist/index.js +0 -0
- package/dist/server/resources.js +175 -0
- package/dist/server/server.js +226 -348
- package/dist/server/tools/ai.js +78 -22
- package/dist/server/tools/analysis.js +266 -73
- package/dist/server/tools/capture.js +67 -20
- package/dist/server/tools/compose.js +159 -0
- package/dist/server/tools/diagnostics.js +112 -60
- package/dist/server/tools/editor.js +130 -66
- package/dist/server/tools/generate.js +190 -74
- package/dist/server/tools/history.js +149 -88
- package/dist/server/tools/playback.js +49 -18
- package/dist/server/tools/session.js +108 -70
- package/dist/server/tools/storage.js +86 -30
- package/dist/server/tools/transform.js +250 -116
- package/dist/server/tools/types.js +66 -1
- package/dist/services/AudioCaptureService.js +4 -3
- package/dist/services/GeminiService.js +1 -1
- package/dist/services/MIDIExportService.js +2 -4
- package/dist/services/SessionManager.js +4 -2
- package/dist/services/StrudelEngine.js +24 -126
- package/dist/services/StrudelEngineHelpers.js +151 -0
- package/package.json +1 -1
- package/dist/__tests__/MusicTheory.test.js +0 -245
- package/dist/__tests__/PatternGenerator.test.js +0 -337
- package/dist/__tests__/PatternStore.test.js +0 -153
- package/dist/__tests__/utils/MockPlaywright.js +0 -130
- package/dist/server/EnhancedMCPServer.js +0 -654
- package/dist/server/EnhancedMCPServerFixed.js +0 -2506
|
@@ -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
|
|
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: '
|
|
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: '
|
|
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 '
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
case '
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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 '
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
273
|
-
case '
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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 = (
|
|
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
|
|
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,
|
|
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
|
-
|
|
552
|
+
const prompt = `You are an expert in Strudel.cc live coding music.
|
|
553
553
|
|
|
554
554
|
Given this pattern:
|
|
555
555
|
\`\`\`javascript
|