@williamzujkowski/live-coding-music-mcp 2.0.0 → 4.0.0

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