@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.
@@ -14,21 +14,63 @@
14
14
  */
15
15
  import { Logger } from '../../utils/Logger.js';
16
16
  const logger = new Logger();
17
+ const SESSION_ID_PROP = {
18
+ session_id: {
19
+ type: 'string',
20
+ description: 'Optional session ID (#108). Omit to use default session.',
21
+ },
22
+ };
17
23
  export const tools = [
24
+ {
25
+ name: 'ai_assist',
26
+ description: 'Gemini-backed pattern assistance. ' +
27
+ 'task=feedback returns creative critique on the current pattern (optionally with audio analysis). ' +
28
+ 'task=suggest analyzes the currently playing audio and suggests a complementary Strudel pattern as text (not auto-executed). ' +
29
+ 'task=jam generates a fresh layer (drums/bass/melody/pad/texture) and merges it into the current pattern, then auto-plays. ' +
30
+ 'All three share Gemini auth + rate limiting. ' +
31
+ 'Example: ai_assist({ task: "jam", layer: "bass" }). ' +
32
+ 'Requires GEMINI_API_KEY env var. For non-AI pattern generation use generate_part; for full compositions use compose.',
33
+ inputSchema: {
34
+ type: 'object',
35
+ properties: {
36
+ task: { type: 'string', enum: ['feedback', 'suggest', 'jam'], description: 'Which AI task' },
37
+ // feedback args
38
+ includeAudio: { type: 'boolean', description: 'task=feedback: include audio analysis (default false)' },
39
+ style: { type: 'string', description: 'task=feedback/suggest: style hint' },
40
+ // suggest args
41
+ role: {
42
+ type: 'string',
43
+ enum: ['complement', 'bassline', 'melody', 'percussion'],
44
+ description: 'task=suggest: role the suggested pattern fills (default complement)',
45
+ },
46
+ // jam args
47
+ layer: {
48
+ type: 'string',
49
+ enum: ['drums', 'bass', 'melody', 'pad', 'texture'],
50
+ description: 'task=jam: layer type to generate',
51
+ },
52
+ style_hint: { type: 'string', description: 'task=jam: style guidance' },
53
+ auto_play: { type: 'boolean', description: 'task=jam: start playback after merge (default true)' },
54
+ ...SESSION_ID_PROP,
55
+ },
56
+ required: ['task'],
57
+ },
58
+ },
18
59
  {
19
60
  name: 'get_pattern_feedback',
20
- description: 'Get AI-powered creative feedback on the current pattern using Google Gemini. Analyzes pattern structure and optionally audio.',
61
+ description: '[DEPRECATED — use ai_assist({ task: "feedback" }) instead] Get AI-powered creative feedback on the current pattern using Google Gemini.',
21
62
  inputSchema: {
22
63
  type: 'object',
23
64
  properties: {
24
65
  includeAudio: { type: 'boolean', description: 'Include audio analysis (plays pattern briefly). Default: false' },
25
66
  style: { type: 'string', description: 'Optional style hint for context (e.g., "techno", "ambient")' },
67
+ ...SESSION_ID_PROP,
26
68
  },
27
69
  },
28
70
  },
29
71
  {
30
72
  name: 'suggest_pattern_from_audio',
31
- description: 'Analyze the currently playing audio and suggest a complementary Strudel pattern using Gemini AI. Extracts tempo, key, and spectral features locally, then uses AI to generate a matching pattern. Returns pattern text (not auto-executed).',
73
+ description: '[DEPRECATED — use ai_assist({ task: "suggest" }) instead] Analyze playing audio and suggest a complementary Strudel pattern.',
32
74
  inputSchema: {
33
75
  type: 'object',
34
76
  properties: {
@@ -38,12 +80,13 @@ export const tools = [
38
80
  enum: ['complement', 'bassline', 'melody', 'percussion'],
39
81
  description: 'What role the suggested pattern should fill. Default: complement',
40
82
  },
83
+ ...SESSION_ID_PROP,
41
84
  },
42
85
  },
43
86
  },
44
87
  {
45
88
  name: 'jam_with',
46
- description: 'AI generates a complementary layer to jam with your pattern. Analyzes current pattern to detect tempo, key, and existing layers, then generates a matching layer that fits musically.',
89
+ description: '[DEPRECATED — use ai_assist({ task: "jam" }) instead] AI generates a complementary layer to jam with your pattern.',
47
90
  inputSchema: {
48
91
  type: 'object',
49
92
  properties: {
@@ -54,6 +97,7 @@ export const tools = [
54
97
  },
55
98
  style_hint: { type: 'string', description: 'Optional style guidance (e.g., "funky", "minimal", "atmospheric")' },
56
99
  auto_play: { type: 'boolean', description: 'Start playback after adding layer (default: true)' },
100
+ ...SESSION_ID_PROP,
57
101
  },
58
102
  required: ['layer'],
59
103
  },
@@ -61,13 +105,24 @@ export const tools = [
61
105
  ];
62
106
  export const toolNames = new Set(tools.map(t => t.name));
63
107
  export async function execute(name, args, ctx) {
108
+ const sid = args?.session_id;
64
109
  switch (name) {
110
+ case 'ai_assist': {
111
+ const t = args?.task;
112
+ switch (t) {
113
+ case 'feedback': return await getPatternFeedback(args?.includeAudio || false, args?.style, ctx, sid);
114
+ case 'suggest': return await suggestPatternFromAudio(args?.style, args?.role || 'complement', ctx, sid);
115
+ case 'jam': return await jamWith(args.layer, args.style_hint, args.auto_play, ctx, sid);
116
+ default:
117
+ throw new Error(`Invalid task: ${t}. Must be one of: feedback, suggest, jam`);
118
+ }
119
+ }
65
120
  case 'get_pattern_feedback':
66
- return await getPatternFeedback(args?.includeAudio || false, args?.style, ctx);
121
+ return await getPatternFeedback(args?.includeAudio || false, args?.style, ctx, sid);
67
122
  case 'suggest_pattern_from_audio':
68
- return await suggestPatternFromAudio(args?.style, args?.role || 'complement', ctx);
123
+ return await suggestPatternFromAudio(args?.style, args?.role || 'complement', ctx, sid);
69
124
  case 'jam_with':
70
- return await jamWith(args.layer, args.style_hint, args.auto_play, ctx);
125
+ return await jamWith(args.layer, args.style_hint, args.auto_play, ctx, sid);
71
126
  default:
72
127
  throw new Error(`ai module does not handle tool: ${name}`);
73
128
  }
@@ -75,14 +130,14 @@ export async function execute(name, args, ctx) {
75
130
  // ---------------------------------------------------------------------------
76
131
  // get_pattern_feedback
77
132
  // ---------------------------------------------------------------------------
78
- async function getPatternFeedback(includeAudio, style, ctx) {
133
+ async function getPatternFeedback(includeAudio, style, ctx, sid) {
79
134
  if (!ctx.geminiService.isAvailable()) {
80
135
  return {
81
136
  gemini_available: false,
82
137
  error: 'Gemini API not configured. Set GEMINI_API_KEY environment variable to enable AI feedback.',
83
138
  };
84
139
  }
85
- const pattern = await ctx.getCurrentPatternSafe();
140
+ const pattern = await ctx.getCurrentPatternSafe(sid);
86
141
  if (!pattern || pattern.trim().length === 0) {
87
142
  return { gemini_available: true, error: 'No pattern to analyze. Write a pattern first.' };
88
143
  }
@@ -98,9 +153,9 @@ async function getPatternFeedback(includeAudio, style, ctx) {
98
153
  }
99
154
  result.error = `Pattern analysis failed: ${message}`;
100
155
  }
101
- if (includeAudio && ctx.isInitialized()) {
156
+ if (includeAudio && (sid || ctx.isInitialized())) {
102
157
  try {
103
- const audioBlob = await captureAudioSampleForFeedback(ctx);
158
+ const audioBlob = await captureAudioSampleForFeedback(ctx, sid);
104
159
  if (audioBlob) {
105
160
  result.audio_analysis = await ctx.geminiService.analyzeAudio(audioBlob, { style, duration: 5 });
106
161
  }
@@ -128,8 +183,8 @@ async function getPatternFeedback(includeAudio, style, ctx) {
128
183
  * analyzer's AudioContext and records 5 seconds without disturbing
129
184
  * the full AudioCaptureService lifecycle.
130
185
  */
131
- async function captureAudioSampleForFeedback(ctx) {
132
- const page = ctx.controller.page;
186
+ async function captureAudioSampleForFeedback(ctx, sid) {
187
+ const page = ctx.getController(sid).page;
133
188
  if (!page) {
134
189
  logger.warn('Cannot capture audio: controller page not available');
135
190
  return null;
@@ -154,7 +209,7 @@ async function captureAudioSampleForFeedback(ctx) {
154
209
  try {
155
210
  analyzer.analyser.disconnect(destination);
156
211
  }
157
- catch { }
212
+ catch { /* already disconnected */ }
158
213
  if (chunks.length === 0) {
159
214
  resolve(null);
160
215
  return;
@@ -194,22 +249,23 @@ async function captureAudioSampleForFeedback(ctx) {
194
249
  // ---------------------------------------------------------------------------
195
250
  // suggest_pattern_from_audio
196
251
  // ---------------------------------------------------------------------------
197
- async function suggestPatternFromAudio(style, role, ctx) {
198
- if (!ctx.isInitialized()) {
252
+ async function suggestPatternFromAudio(style, role, ctx, sid) {
253
+ if (!sid && !ctx.isInitialized()) {
199
254
  return { error: 'Browser not initialized. Run init and play a pattern first.' };
200
255
  }
201
256
  if (!ctx.geminiService.isAvailable()) {
202
257
  return { error: 'Gemini API not configured. Set GEMINI_API_KEY to enable AI features.' };
203
258
  }
259
+ const controller = ctx.getController(sid);
204
260
  let bpm = 0, key = 'C', scale = 'major';
205
261
  try {
206
- const tempoResult = await ctx.controller.detectTempo();
262
+ const tempoResult = await controller.detectTempo();
207
263
  if (tempoResult && tempoResult.bpm > 0)
208
264
  bpm = tempoResult.bpm;
209
265
  }
210
266
  catch { /* best effort */ }
211
267
  try {
212
- const keyResult = await ctx.controller.detectKey();
268
+ const keyResult = await controller.detectKey();
213
269
  if (keyResult && keyResult.confidence > 0.1) {
214
270
  key = keyResult.key;
215
271
  scale = keyResult.scale;
@@ -263,7 +319,7 @@ Example patterns:
263
319
  // ---------------------------------------------------------------------------
264
320
  // jam_with
265
321
  // ---------------------------------------------------------------------------
266
- async function jamWith(layer, styleHint, autoPlay = true, ctx) {
322
+ async function jamWith(layer, styleHint, autoPlay = true, ctx, sid) {
267
323
  const validLayers = ['drums', 'bass', 'melody', 'pad', 'texture'];
268
324
  if (!validLayers.includes(layer)) {
269
325
  return {
@@ -274,7 +330,7 @@ async function jamWith(layer, styleHint, autoPlay = true, ctx) {
274
330
  newLayer: '',
275
331
  };
276
332
  }
277
- const currentPattern = await ctx.getCurrentPatternSafe();
333
+ const currentPattern = await ctx.getCurrentPatternSafe(sid);
278
334
  if (!currentPattern || currentPattern.trim().length === 0) {
279
335
  return {
280
336
  success: false,
@@ -308,9 +364,9 @@ async function jamWith(layer, styleHint, autoPlay = true, ctx) {
308
364
  }
309
365
  const mergedPattern = mergeLayerIntoPattern(currentPattern, newLayer, layer);
310
366
  try {
311
- await ctx.writePatternSafe(mergedPattern);
312
- if (autoPlay && ctx.isInitialized()) {
313
- await ctx.controller.play();
367
+ await ctx.writePatternSafe(mergedPattern, sid);
368
+ if (autoPlay && (sid || ctx.isInitialized())) {
369
+ await ctx.getController(sid).play();
314
370
  }
315
371
  return {
316
372
  success: true,
@@ -1,40 +1,71 @@
1
1
  /**
2
- * analysis domain — audio analysis + pattern runtime validation.
2
+ * analysis domain — audio analysis + pattern validation (browser + local).
3
3
  *
4
- * Owns (6 tools): analyze, analyze_spectrum, analyze_rhythm,
5
- * detect_tempo, detect_key, validate_pattern_runtime.
4
+ * Owns (10 tools):
5
+ * browser-required: analyze, analyze_spectrum, analyze_rhythm,
6
+ * detect_tempo, detect_key, validate_pattern_runtime
7
+ * browser-free: validate_pattern_local, analyze_pattern_local,
8
+ * query_pattern_events, transpile_pattern
9
+ *
10
+ * The browser-free set runs against the in-process StrudelEngine and
11
+ * pairs naturally with validate_pattern_runtime (which monitors the
12
+ * actual Strudel console). They were orphan handlers — implementations
13
+ * present but not registered in tools/list — until #124.
6
14
  *
7
15
  * Post-consolidation (per #110 audit): a single `analyze` tool with
8
- * an `include[]` filter absorbs the 4 detection-specific tools.
9
- * `validate_pattern_runtime` stays separate — it's console/syntax
10
- * scraping, not audio DSP.
16
+ * an `include[]` filter absorbs the 4 audio detection tools.
17
+ * Local-engine tools stay distinct — different subsystem.
11
18
  */
12
19
  import { InputValidator } from '../../utils/InputValidator.js';
20
+ const SESSION_ID_PROP = {
21
+ session_id: {
22
+ type: 'string',
23
+ description: 'Optional session ID (#108). Omit to use default session.',
24
+ },
25
+ };
13
26
  export const tools = [
14
27
  {
15
28
  name: 'analyze',
16
- description: 'Complete audio analysis',
17
- inputSchema: { type: 'object', properties: {} },
29
+ description: 'Audio analysis on the currently-playing pattern. ' +
30
+ 'include=["all"] (default) returns the full spectrum + features object, matching the pre-consolidation behaviour. ' +
31
+ 'include=["tempo"] returns only BPM with confidence. ' +
32
+ 'include=["key"] returns detected key + scale + confidence. ' +
33
+ 'include=["spectrum"] returns FFT features (bass, mid, treble, brightness, etc.). ' +
34
+ 'include=["rhythm"] returns rhythm-only analysis (complexity, density, syncopation). ' +
35
+ 'Combining values returns an object keyed by category, e.g. analyze({ include: ["tempo", "key"] }) → { tempo: {...}, key: {...} }. ' +
36
+ 'Example: analyze({ include: ["tempo"] }) to cheaply re-check BPM during a session. ' +
37
+ 'For static pattern analysis (no browser) use analyze_pattern_local; for runtime validation use validate_pattern_runtime.',
38
+ inputSchema: {
39
+ type: 'object',
40
+ properties: {
41
+ include: {
42
+ type: 'array',
43
+ items: { type: 'string', enum: ['all', 'spectrum', 'rhythm', 'tempo', 'key'] },
44
+ description: 'Which analyses to return. Default ["all"] preserves pre-consolidation behaviour.',
45
+ },
46
+ ...SESSION_ID_PROP,
47
+ },
48
+ },
18
49
  },
19
50
  {
20
51
  name: 'analyze_spectrum',
21
- description: 'FFT spectrum analysis',
22
- inputSchema: { type: 'object', properties: {} },
52
+ description: '[DEPRECATED — use analyze({ include: ["spectrum"] }) instead] FFT spectrum analysis',
53
+ inputSchema: { type: 'object', properties: { ...SESSION_ID_PROP } },
23
54
  },
24
55
  {
25
56
  name: 'analyze_rhythm',
26
- description: 'Rhythm analysis',
27
- inputSchema: { type: 'object', properties: {} },
57
+ description: '[DEPRECATED — use analyze({ include: ["rhythm"] }) instead] Rhythm analysis',
58
+ inputSchema: { type: 'object', properties: { ...SESSION_ID_PROP } },
28
59
  },
29
60
  {
30
61
  name: 'detect_tempo',
31
- description: 'BPM detection',
32
- inputSchema: { type: 'object', properties: {} },
62
+ description: '[DEPRECATED — use analyze({ include: ["tempo"] }) instead] BPM detection',
63
+ inputSchema: { type: 'object', properties: { ...SESSION_ID_PROP } },
33
64
  },
34
65
  {
35
66
  name: 'detect_key',
36
- description: 'Key detection',
37
- inputSchema: { type: 'object', properties: {} },
67
+ description: '[DEPRECATED — use analyze({ include: ["key"] }) instead] Key detection',
68
+ inputSchema: { type: 'object', properties: { ...SESSION_ID_PROP } },
38
69
  },
39
70
  {
40
71
  name: 'validate_pattern_runtime',
@@ -44,86 +75,248 @@ export const tools = [
44
75
  properties: {
45
76
  pattern: { type: 'string', description: 'Pattern code to validate' },
46
77
  waitMs: { type: 'number', description: 'How long to wait for errors (default 500ms)' },
78
+ ...SESSION_ID_PROP,
79
+ },
80
+ required: ['pattern'],
81
+ },
82
+ },
83
+ {
84
+ name: 'validate_pattern_local',
85
+ description: 'Validate pattern syntax against the in-process StrudelEngine (no browser required)',
86
+ inputSchema: {
87
+ type: 'object',
88
+ properties: {
89
+ pattern: { type: 'string', description: 'Pattern code to validate' },
90
+ },
91
+ required: ['pattern'],
92
+ },
93
+ },
94
+ {
95
+ name: 'analyze_pattern_local',
96
+ description: 'Static analysis (events/cycle, complexity, optional BPM) without browser playback',
97
+ inputSchema: {
98
+ type: 'object',
99
+ properties: {
100
+ pattern: { type: 'string', description: 'Pattern code to analyze' },
101
+ },
102
+ required: ['pattern'],
103
+ },
104
+ },
105
+ {
106
+ name: 'query_pattern_events',
107
+ description: 'Enumerate events the pattern would emit between two cycle indices (max 16 cycles)',
108
+ inputSchema: {
109
+ type: 'object',
110
+ properties: {
111
+ pattern: { type: 'string', description: 'Pattern code to query' },
112
+ start: { type: 'number', description: 'Start cycle (default 0)' },
113
+ end: { type: 'number', description: 'End cycle (default 1)' },
114
+ },
115
+ required: ['pattern'],
116
+ },
117
+ },
118
+ {
119
+ name: 'transpile_pattern',
120
+ description: 'Transpile pattern source via StrudelEngine; returns transpiled code or syntax error',
121
+ inputSchema: {
122
+ type: 'object',
123
+ properties: {
124
+ pattern: { type: 'string', description: 'Pattern code to transpile' },
47
125
  },
48
126
  required: ['pattern'],
49
127
  },
50
128
  },
51
129
  ];
52
130
  export const toolNames = new Set(tools.map(t => t.name));
131
+ // Tools in this module that do NOT require a browser session.
132
+ const LOCAL_ENGINE_TOOLS = new Set([
133
+ 'validate_pattern_local',
134
+ 'analyze_pattern_local',
135
+ 'query_pattern_events',
136
+ 'transpile_pattern',
137
+ ]);
138
+ async function getSpectrum(controller) {
139
+ const spectrum = await controller.analyzeAudio();
140
+ return spectrum.features || spectrum;
141
+ }
142
+ async function getRhythm(controller) {
143
+ return await controller.analyzeRhythm();
144
+ }
145
+ async function getTempo(controller) {
146
+ try {
147
+ const tempoAnalysis = await controller.detectTempo();
148
+ if (!tempoAnalysis || tempoAnalysis.bpm === 0) {
149
+ return {
150
+ bpm: 0,
151
+ confidence: 0,
152
+ message: 'No tempo detected. Ensure audio is playing and has a clear rhythmic pattern.',
153
+ };
154
+ }
155
+ return {
156
+ bpm: tempoAnalysis.bpm,
157
+ confidence: Math.round(tempoAnalysis.confidence * 100) / 100,
158
+ method: tempoAnalysis.method,
159
+ message: `Detected ${tempoAnalysis.bpm} BPM with ${Math.round(tempoAnalysis.confidence * 100)}% confidence`,
160
+ };
161
+ }
162
+ catch (error) {
163
+ const message = error instanceof Error ? error.message : String(error);
164
+ return { bpm: 0, confidence: 0, error: message || 'Tempo detection failed' };
165
+ }
166
+ }
167
+ async function getKey(controller) {
168
+ try {
169
+ const keyAnalysis = await controller.detectKey();
170
+ if (!keyAnalysis || keyAnalysis.confidence < 0.1) {
171
+ return {
172
+ key: 'Unknown',
173
+ scale: 'unknown',
174
+ confidence: 0,
175
+ message: 'No clear key detected. Ensure audio is playing and has tonal content.',
176
+ };
177
+ }
178
+ const result = {
179
+ key: keyAnalysis.key,
180
+ scale: keyAnalysis.scale,
181
+ confidence: Math.round(keyAnalysis.confidence * 100) / 100,
182
+ message: `Detected ${keyAnalysis.key} ${keyAnalysis.scale} with ${Math.round(keyAnalysis.confidence * 100)}% confidence`,
183
+ };
184
+ if (keyAnalysis.alternatives && keyAnalysis.alternatives.length > 0) {
185
+ result.alternatives = keyAnalysis.alternatives.map((alt) => ({
186
+ key: alt.key,
187
+ scale: alt.scale,
188
+ confidence: Math.round(alt.confidence * 100) / 100,
189
+ }));
190
+ }
191
+ return result;
192
+ }
193
+ catch (error) {
194
+ const message = error instanceof Error ? error.message : String(error);
195
+ return { key: 'Unknown', scale: 'unknown', confidence: 0, error: message || 'Key detection failed' };
196
+ }
197
+ }
198
+ async function doAnalyze(args, controller) {
199
+ const includeRaw = args?.include;
200
+ const include = Array.isArray(includeRaw) && includeRaw.length > 0
201
+ ? includeRaw
202
+ : ['all'];
203
+ for (const v of include) {
204
+ if (!['all', 'spectrum', 'rhythm', 'tempo', 'key'].includes(v)) {
205
+ throw new Error(`Invalid include value: ${v}. Must be one of: all, spectrum, rhythm, tempo, key`);
206
+ }
207
+ }
208
+ // include=['all'] is the most-common path and returns the unwrapped
209
+ // analyzeAudio result (no schema surprise vs old `analyze`).
210
+ if (include.includes('all')) {
211
+ return await controller.analyzeAudio();
212
+ }
213
+ // Otherwise return an object keyed by category, computed in parallel.
214
+ const tasks = {};
215
+ if (include.includes('spectrum'))
216
+ tasks.spectrum = getSpectrum(controller);
217
+ if (include.includes('rhythm'))
218
+ tasks.rhythm = getRhythm(controller);
219
+ if (include.includes('tempo'))
220
+ tasks.tempo = getTempo(controller);
221
+ if (include.includes('key'))
222
+ tasks.key = getKey(controller);
223
+ const entries = await Promise.all(Object.entries(tasks).map(async ([k, p]) => [k, await p]));
224
+ return Object.fromEntries(entries);
225
+ }
53
226
  export async function execute(name, args, ctx) {
54
- if (!ctx.isInitialized()) {
227
+ const sid = args?.session_id;
228
+ if (!LOCAL_ENGINE_TOOLS.has(name) && !sid && !ctx.isInitialized()) {
55
229
  return 'Browser not initialized. Run init first.';
56
230
  }
231
+ const controller = LOCAL_ENGINE_TOOLS.has(name) ? null : ctx.getController(sid);
57
232
  switch (name) {
58
233
  case 'analyze':
59
- return await ctx.controller.analyzeAudio();
60
- case 'analyze_spectrum': {
61
- const spectrum = await ctx.controller.analyzeAudio();
62
- return spectrum.features || spectrum;
234
+ return await doAnalyze(args, controller);
235
+ // Deprecated aliases — forward to consolidated handler.
236
+ case 'analyze_spectrum': return await getSpectrum(controller);
237
+ case 'analyze_rhythm': return await getRhythm(controller);
238
+ case 'detect_tempo': return await getTempo(controller);
239
+ case 'detect_key': return await getKey(controller);
240
+ case 'validate_pattern_runtime': {
241
+ InputValidator.validateStringLength(args.pattern, 'pattern', 10000, false);
242
+ const validation = await controller.validatePatternRuntime(args.pattern, args.waitMs || 500);
243
+ if (validation.valid) {
244
+ return '✅ Pattern valid - no runtime errors detected';
245
+ }
246
+ return `❌ Pattern has runtime errors:\n${validation.errors.join('\n')}\n` +
247
+ (validation.warnings.length > 0 ? `\nWarnings:\n${validation.warnings.join('\n')}` : '');
63
248
  }
64
- case 'analyze_rhythm':
65
- return await ctx.controller.analyzeRhythm();
66
- case 'detect_tempo': {
67
- try {
68
- const tempoAnalysis = await ctx.controller.detectTempo();
69
- if (!tempoAnalysis || tempoAnalysis.bpm === 0) {
70
- return {
71
- bpm: 0,
72
- confidence: 0,
73
- message: 'No tempo detected. Ensure audio is playing and has a clear rhythmic pattern.',
74
- };
75
- }
76
- return {
77
- bpm: tempoAnalysis.bpm,
78
- confidence: Math.round(tempoAnalysis.confidence * 100) / 100,
79
- method: tempoAnalysis.method,
80
- message: `Detected ${tempoAnalysis.bpm} BPM with ${Math.round(tempoAnalysis.confidence * 100)}% confidence`,
81
- };
249
+ case 'validate_pattern_local': {
250
+ InputValidator.validateStringLength(args.pattern, 'pattern', 10000, false);
251
+ const localValidation = ctx.strudelEngine.validate(args.pattern);
252
+ return {
253
+ valid: localValidation.valid,
254
+ errors: localValidation.errors,
255
+ warnings: localValidation.warnings,
256
+ suggestions: localValidation.suggestions,
257
+ errorLocation: localValidation.errorLocation,
258
+ message: localValidation.valid
259
+ ? '✅ Pattern is valid'
260
+ : `❌ Pattern has ${localValidation.errors.length} error(s)`,
261
+ };
262
+ }
263
+ case 'analyze_pattern_local': {
264
+ InputValidator.validateStringLength(args.pattern, 'pattern', 10000, false);
265
+ const patternMetadata = ctx.strudelEngine.analyzePattern(args.pattern);
266
+ return {
267
+ ...patternMetadata,
268
+ message: `Pattern analysis: ${patternMetadata.eventsPerCycle} events/cycle, ` +
269
+ `complexity ${(patternMetadata.complexity * 100).toFixed(0)}%` +
270
+ (patternMetadata.bpm ? `, ${patternMetadata.bpm} BPM` : ''),
271
+ };
272
+ }
273
+ case 'query_pattern_events': {
274
+ InputValidator.validateStringLength(args.pattern, 'pattern', 10000, false);
275
+ const startCycle = args.start ?? 0;
276
+ const endCycle = args.end ?? 1;
277
+ if (startCycle >= endCycle) {
278
+ return { error: 'Start must be less than end' };
82
279
  }
83
- catch (error) {
84
- const message = error instanceof Error ? error.message : String(error);
85
- return { bpm: 0, confidence: 0, error: message || 'Tempo detection failed' };
280
+ if (endCycle - startCycle > 16) {
281
+ return { error: 'Maximum range is 16 cycles to prevent excessive output' };
86
282
  }
87
- }
88
- case 'detect_key': {
89
283
  try {
90
- const keyAnalysis = await ctx.controller.detectKey();
91
- if (!keyAnalysis || keyAnalysis.confidence < 0.1) {
92
- return {
93
- key: 'Unknown',
94
- scale: 'unknown',
95
- confidence: 0,
96
- message: 'No clear key detected. Ensure audio is playing and has tonal content.',
97
- };
98
- }
99
- const result = {
100
- key: keyAnalysis.key,
101
- scale: keyAnalysis.scale,
102
- confidence: Math.round(keyAnalysis.confidence * 100) / 100,
103
- message: `Detected ${keyAnalysis.key} ${keyAnalysis.scale} with ${Math.round(keyAnalysis.confidence * 100)}% confidence`,
284
+ const events = ctx.strudelEngine.queryEvents(args.pattern, startCycle, endCycle);
285
+ return {
286
+ count: events.length,
287
+ range: { start: startCycle, end: endCycle },
288
+ events: events.map((e) => ({
289
+ value: e.value,
290
+ start: e.start,
291
+ end: e.end,
292
+ duration: e.end - e.start,
293
+ })),
104
294
  };
105
- if (keyAnalysis.alternatives && keyAnalysis.alternatives.length > 0) {
106
- result.alternatives = keyAnalysis.alternatives.map((alt) => ({
107
- key: alt.key,
108
- scale: alt.scale,
109
- confidence: Math.round(alt.confidence * 100) / 100,
110
- }));
111
- }
112
- return result;
113
295
  }
114
296
  catch (error) {
115
297
  const message = error instanceof Error ? error.message : String(error);
116
- return { key: 'Unknown', scale: 'unknown', confidence: 0, error: message || 'Key detection failed' };
298
+ return {
299
+ error: message,
300
+ suggestion: 'Check pattern syntax with validate_pattern_local first',
301
+ };
117
302
  }
118
303
  }
119
- case 'validate_pattern_runtime': {
304
+ case 'transpile_pattern': {
120
305
  InputValidator.validateStringLength(args.pattern, 'pattern', 10000, false);
121
- const validation = await ctx.controller.validatePatternRuntime(args.pattern, args.waitMs || 500);
122
- if (validation.valid) {
123
- return '✅ Pattern valid - no runtime errors detected';
306
+ const transpileResult = ctx.strudelEngine.transpile(args.pattern);
307
+ if (transpileResult.success) {
308
+ return {
309
+ success: true,
310
+ transpiledCode: transpileResult.transpiledCode,
311
+ message: 'Pattern transpiled successfully',
312
+ };
124
313
  }
125
- return `❌ Pattern has runtime errors:\n${validation.errors.join('\n')}\n` +
126
- (validation.warnings.length > 0 ? `\nWarnings:\n${validation.warnings.join('\n')}` : '');
314
+ return {
315
+ success: false,
316
+ error: transpileResult.error,
317
+ errorLocation: transpileResult.errorLocation,
318
+ message: 'Transpilation failed',
319
+ };
127
320
  }
128
321
  default:
129
322
  throw new Error(`analysis module does not handle tool: ${name}`);