@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.
- package/README.md +175 -249
- 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 +227 -349
- package/dist/server/tools/ai.js +59 -59
- package/dist/server/tools/analysis.js +245 -82
- package/dist/server/tools/capture.js +51 -42
- package/dist/server/tools/compose.js +145 -0
- package/dist/server/tools/diagnostics.js +77 -72
- package/dist/server/tools/editor.js +100 -99
- package/dist/server/tools/generate.js +146 -175
- package/dist/server/tools/history.js +132 -124
- package/dist/server/tools/playback.js +41 -26
- package/dist/server/tools/session.js +93 -93
- package/dist/server/tools/storage.js +61 -53
- package/dist/server/tools/transform.js +193 -206
- 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
|
@@ -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
|
|
@@ -206,7 +206,7 @@ export class MIDIExportService {
|
|
|
206
206
|
}
|
|
207
207
|
// Handle sub-patterns in brackets [c4 e4]
|
|
208
208
|
if (part.startsWith('[')) {
|
|
209
|
-
const subContent = part.replace(/[
|
|
209
|
+
const subContent = part.replace(/[[\]]/g, '');
|
|
210
210
|
const subParts = subContent.split(/[\s,]+/);
|
|
211
211
|
subParts.forEach(subPart => {
|
|
212
212
|
const midi = asMidiNumbers
|
|
@@ -271,7 +271,7 @@ export class MIDIExportService {
|
|
|
271
271
|
parseInlineNotes(pattern) {
|
|
272
272
|
const notes = [];
|
|
273
273
|
// Find quoted strings that look like note sequences
|
|
274
|
-
const quotedRegex = /["'`]([a-g][#b]?\d[\s,a-g#b0-9
|
|
274
|
+
const quotedRegex = /["'`]([a-g][#b]?\d[\s,a-g#b0-9~\-[\]]+)["'`]/gi;
|
|
275
275
|
let match;
|
|
276
276
|
while ((match = quotedRegex.exec(pattern)) !== null) {
|
|
277
277
|
const noteSequence = match[1];
|
|
@@ -300,8 +300,6 @@ export class MIDIExportService {
|
|
|
300
300
|
// Create track
|
|
301
301
|
const track = midi.addTrack();
|
|
302
302
|
track.name = trackName;
|
|
303
|
-
// Calculate PPQ (pulses per quarter note) from the header
|
|
304
|
-
const ppq = midi.header.ppq;
|
|
305
303
|
// Add notes to track
|
|
306
304
|
notes.forEach(noteEvent => {
|
|
307
305
|
// Only add notes within the specified bar range
|
|
@@ -47,11 +47,13 @@ export class SessionManager {
|
|
|
47
47
|
/**
|
|
48
48
|
* Creates a new isolated Strudel session
|
|
49
49
|
* @param id - Unique session identifier
|
|
50
|
-
* @param
|
|
50
|
+
* @param _headless - Override headless mode for this session (currently a
|
|
51
|
+
* no-op; sessions share the constructor's browser instance). Kept in the
|
|
52
|
+
* signature for API stability until multi-session work in #108 lands.
|
|
51
53
|
* @returns Initialized StrudelController for the session
|
|
52
54
|
* @throws {Error} When max sessions limit reached or session ID already exists
|
|
53
55
|
*/
|
|
54
|
-
async createSession(id,
|
|
56
|
+
async createSession(id, _headless) {
|
|
55
57
|
// Validate session ID
|
|
56
58
|
if (!id || typeof id !== 'string') {
|
|
57
59
|
throw new Error('Session ID must be a non-empty string');
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import * as strudelCore from '@strudel/core';
|
|
16
16
|
import { mini } from '@strudel/mini';
|
|
17
17
|
import { transpiler } from '@strudel/transpiler';
|
|
18
|
+
import { calculateComplexity, checkCommonIssues, extractBpm, extractFunctionsUsed, getSuggestionsForError, parseErrorLocation, } from './StrudelEngineHelpers.js';
|
|
18
19
|
/**
|
|
19
20
|
* Local Strudel pattern engine - executes patterns without browser.
|
|
20
21
|
* Replaces fragile browser automation for pattern validation and analysis.
|
|
@@ -76,8 +77,7 @@ export class StrudelEngine {
|
|
|
76
77
|
};
|
|
77
78
|
}
|
|
78
79
|
catch (error) {
|
|
79
|
-
|
|
80
|
-
const location = this.parseErrorLocation(error);
|
|
80
|
+
const location = parseErrorLocation(error);
|
|
81
81
|
return {
|
|
82
82
|
success: false,
|
|
83
83
|
error: error.message || 'Transpilation failed',
|
|
@@ -112,24 +112,21 @@ export class StrudelEngine {
|
|
|
112
112
|
suggestions: ['Add a pattern like: s("bd hh sd hh")'],
|
|
113
113
|
};
|
|
114
114
|
}
|
|
115
|
-
// Try transpilation first - catches syntax errors
|
|
116
115
|
const transpileResult = this.transpile(code);
|
|
117
116
|
if (!transpileResult.success) {
|
|
118
117
|
return {
|
|
119
118
|
valid: false,
|
|
120
119
|
errors: [transpileResult.error || 'Syntax error'],
|
|
121
120
|
warnings,
|
|
122
|
-
suggestions:
|
|
121
|
+
suggestions: getSuggestionsForError(transpileResult.error || ''),
|
|
123
122
|
errorLocation: transpileResult.errorLocation
|
|
124
123
|
? { line: transpileResult.errorLocation.line, column: transpileResult.errorLocation.column }
|
|
125
124
|
: undefined,
|
|
126
125
|
};
|
|
127
126
|
}
|
|
128
|
-
// Try to evaluate the pattern to catch runtime errors
|
|
129
127
|
try {
|
|
130
128
|
const fn = new Function(...Object.keys(this.context), transpileResult.transpiledCode);
|
|
131
129
|
const pattern = fn(...Object.values(this.context));
|
|
132
|
-
// Verify it's a valid pattern
|
|
133
130
|
if (!pattern || typeof pattern.queryArc !== 'function') {
|
|
134
131
|
errors.push('Code did not produce a valid pattern');
|
|
135
132
|
suggestions.push('Ensure your code returns a pattern (e.g., s("bd"), note("c3"), stack(...))');
|
|
@@ -137,10 +134,9 @@ export class StrudelEngine {
|
|
|
137
134
|
}
|
|
138
135
|
catch (error) {
|
|
139
136
|
errors.push(`Runtime error: ${error.message}`);
|
|
140
|
-
suggestions.push(...
|
|
137
|
+
suggestions.push(...getSuggestionsForError(error.message));
|
|
141
138
|
}
|
|
142
|
-
|
|
143
|
-
this.checkCommonIssues(code, warnings, suggestions);
|
|
139
|
+
checkCommonIssues(code, warnings, suggestions);
|
|
144
140
|
return {
|
|
145
141
|
valid: errors.length === 0,
|
|
146
142
|
errors,
|
|
@@ -221,36 +217,22 @@ export class StrudelEngine {
|
|
|
221
217
|
* ```
|
|
222
218
|
*/
|
|
223
219
|
analyzePattern(code) {
|
|
224
|
-
// Default metadata
|
|
225
220
|
const metadata = {
|
|
226
221
|
eventsPerCycle: 0,
|
|
227
222
|
uniqueValues: [],
|
|
228
|
-
usesSound:
|
|
229
|
-
usesNote:
|
|
230
|
-
isStack:
|
|
231
|
-
functionsUsed:
|
|
223
|
+
usesSound: /\bs\s*\(/.test(code) || /\bsound\s*\(/.test(code),
|
|
224
|
+
usesNote: /\bnote\s*\(/.test(code),
|
|
225
|
+
isStack: /\bstack\s*\(/.test(code),
|
|
226
|
+
functionsUsed: extractFunctionsUsed(code),
|
|
232
227
|
complexity: 0,
|
|
233
228
|
};
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
metadata.bpm = parseFloat(bpmMatch[1]);
|
|
229
|
+
const bpm = extractBpm(code);
|
|
230
|
+
if (bpm !== undefined) {
|
|
231
|
+
metadata.bpm = bpm;
|
|
238
232
|
}
|
|
239
|
-
// Analyze code structure
|
|
240
|
-
metadata.usesSound = /\bs\s*\(/.test(code) || /\bsound\s*\(/.test(code);
|
|
241
|
-
metadata.usesNote = /\bnote\s*\(/.test(code);
|
|
242
|
-
metadata.isStack = /\bstack\s*\(/.test(code);
|
|
243
|
-
// Extract function names used
|
|
244
|
-
const funcMatches = code.match(/\b([a-z][a-zA-Z0-9_]*)\s*\(/g);
|
|
245
|
-
if (funcMatches) {
|
|
246
|
-
const funcNames = funcMatches.map(m => m.replace(/\s*\($/, ''));
|
|
247
|
-
metadata.functionsUsed = [...new Set(funcNames)];
|
|
248
|
-
}
|
|
249
|
-
// Try to query events for detailed analysis
|
|
250
233
|
try {
|
|
251
234
|
const events = this.queryEvents(code, 0, 1);
|
|
252
235
|
metadata.eventsPerCycle = events.length;
|
|
253
|
-
// Extract unique values
|
|
254
236
|
const values = events.map(e => {
|
|
255
237
|
if (typeof e.value === 'object' && e.value !== null) {
|
|
256
238
|
return e.value.s || e.value.note || e.value.n || JSON.stringify(e.value);
|
|
@@ -258,21 +240,20 @@ export class StrudelEngine {
|
|
|
258
240
|
return String(e.value);
|
|
259
241
|
});
|
|
260
242
|
metadata.uniqueValues = [...new Set(values)];
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
(code.length / 500) * 0.1, // Code length
|
|
268
|
-
];
|
|
269
|
-
metadata.complexity = Math.min(complexityFactors.reduce((a, b) => a + b, 0), 1);
|
|
243
|
+
metadata.complexity = calculateComplexity({
|
|
244
|
+
uniqueValues: metadata.uniqueValues,
|
|
245
|
+
functionsUsed: metadata.functionsUsed,
|
|
246
|
+
isStack: metadata.isStack,
|
|
247
|
+
codeLength: code.length,
|
|
248
|
+
}, events.length);
|
|
270
249
|
}
|
|
271
250
|
catch {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
251
|
+
metadata.complexity = calculateComplexity({
|
|
252
|
+
uniqueValues: metadata.uniqueValues,
|
|
253
|
+
functionsUsed: metadata.functionsUsed,
|
|
254
|
+
isStack: metadata.isStack,
|
|
255
|
+
codeLength: code.length,
|
|
256
|
+
});
|
|
276
257
|
}
|
|
277
258
|
return metadata;
|
|
278
259
|
}
|
|
@@ -288,87 +269,4 @@ export class StrudelEngine {
|
|
|
288
269
|
context: hap.context,
|
|
289
270
|
};
|
|
290
271
|
}
|
|
291
|
-
/**
|
|
292
|
-
* Parse error location from various error formats
|
|
293
|
-
*/
|
|
294
|
-
parseErrorLocation(error) {
|
|
295
|
-
// Acorn-style location
|
|
296
|
-
if (error.loc) {
|
|
297
|
-
return {
|
|
298
|
-
line: error.loc.line,
|
|
299
|
-
column: error.loc.column,
|
|
300
|
-
offset: error.pos || 0,
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
// Strudel mini SyntaxError
|
|
304
|
-
if (error.location) {
|
|
305
|
-
return {
|
|
306
|
-
line: error.location.start.line,
|
|
307
|
-
column: error.location.start.column,
|
|
308
|
-
offset: error.location.start.offset,
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
// Try to parse from message
|
|
312
|
-
const lineColMatch = error.message?.match(/line\s*(\d+).*column\s*(\d+)/i);
|
|
313
|
-
if (lineColMatch) {
|
|
314
|
-
return {
|
|
315
|
-
line: parseInt(lineColMatch[1]),
|
|
316
|
-
column: parseInt(lineColMatch[2]),
|
|
317
|
-
offset: 0,
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
return undefined;
|
|
321
|
-
}
|
|
322
|
-
/**
|
|
323
|
-
* Get suggestions based on error message
|
|
324
|
-
*/
|
|
325
|
-
getSuggestionsForError(error) {
|
|
326
|
-
const suggestions = [];
|
|
327
|
-
const lowerError = error.toLowerCase();
|
|
328
|
-
if (lowerError.includes('unexpected token')) {
|
|
329
|
-
suggestions.push('Check for missing quotes, parentheses, or brackets');
|
|
330
|
-
suggestions.push('Ensure all function calls have matching ()');
|
|
331
|
-
}
|
|
332
|
-
if (lowerError.includes('is not defined')) {
|
|
333
|
-
const match = error.match(/(\w+) is not defined/);
|
|
334
|
-
if (match) {
|
|
335
|
-
suggestions.push(`"${match[1]}" is not a known Strudel function`);
|
|
336
|
-
suggestions.push('Check spelling or use a valid function like s(), note(), stack()');
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
if (lowerError.includes('not a function')) {
|
|
340
|
-
suggestions.push('Check that you are calling methods on a pattern object');
|
|
341
|
-
}
|
|
342
|
-
if (lowerError.includes('unexpected end')) {
|
|
343
|
-
suggestions.push('Pattern appears incomplete - check for missing closing brackets');
|
|
344
|
-
}
|
|
345
|
-
return suggestions;
|
|
346
|
-
}
|
|
347
|
-
/**
|
|
348
|
-
* Check for common issues in pattern code
|
|
349
|
-
*/
|
|
350
|
-
checkCommonIssues(code, warnings, suggestions) {
|
|
351
|
-
// Check for high gain values
|
|
352
|
-
const gainMatches = code.match(/\.gain\s*\(\s*(\d+(?:\.\d+)?)\s*\)/g);
|
|
353
|
-
if (gainMatches) {
|
|
354
|
-
for (const match of gainMatches) {
|
|
355
|
-
const value = parseFloat(match.match(/(\d+(?:\.\d+)?)/)?.[1] || '0');
|
|
356
|
-
if (value > 2) {
|
|
357
|
-
warnings.push(`High gain value (${value}) may cause distortion`);
|
|
358
|
-
}
|
|
359
|
-
if (value > 5) {
|
|
360
|
-
warnings.push(`Dangerous gain value (${value}) - consider reducing to 2 or less`);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
// Check for missing sound-producing functions
|
|
365
|
-
if (!/\b(s|sound|note|n)\s*\(/.test(code) && !/\bstack\s*\(/.test(code)) {
|
|
366
|
-
warnings.push('Pattern may not produce sound - no s(), note(), or stack() found');
|
|
367
|
-
suggestions.push('Add a sound source like s("bd") or note("c3")');
|
|
368
|
-
}
|
|
369
|
-
// Suggest tempo if not set
|
|
370
|
-
if (!/setcpm|setbpm|cpm|bpm/.test(code)) {
|
|
371
|
-
suggestions.push('Consider setting tempo with setcpm(120)');
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
272
|
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers extracted from StrudelEngine for direct unit testing.
|
|
3
|
+
*
|
|
4
|
+
* StrudelEngine.ts imports @strudel/* packages, which ship as ESM and
|
|
5
|
+
* fight Jest's CJS loader (see #107). Helpers that don't touch @strudel
|
|
6
|
+
* — string parsing, regex extraction, scoring — live here so they can
|
|
7
|
+
* be tested without the ESM workaround dance.
|
|
8
|
+
*
|
|
9
|
+
* @module services/StrudelEngineHelpers
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Parse an error location from the assortment of shapes the Strudel
|
|
13
|
+
* stack produces:
|
|
14
|
+
* - acorn: { loc: { line, column }, pos }
|
|
15
|
+
* - mini: { location: { start: { line, column, offset } } }
|
|
16
|
+
* - other: message string containing "line N ... column N"
|
|
17
|
+
* Returns undefined if no shape matches — callers should treat that
|
|
18
|
+
* as "location unknown" rather than an error.
|
|
19
|
+
*/
|
|
20
|
+
export function parseErrorLocation(error) {
|
|
21
|
+
if (error?.loc) {
|
|
22
|
+
return {
|
|
23
|
+
line: error.loc.line,
|
|
24
|
+
column: error.loc.column,
|
|
25
|
+
offset: error.pos ?? 0,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (error?.location?.start) {
|
|
29
|
+
return {
|
|
30
|
+
line: error.location.start.line,
|
|
31
|
+
column: error.location.start.column,
|
|
32
|
+
offset: error.location.start.offset,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const lineColMatch = error?.message?.match(/line\s*(\d+).*column\s*(\d+)/i);
|
|
36
|
+
if (lineColMatch) {
|
|
37
|
+
return {
|
|
38
|
+
line: parseInt(lineColMatch[1], 10),
|
|
39
|
+
column: parseInt(lineColMatch[2], 10),
|
|
40
|
+
offset: 0,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Produce actionable suggestions for an error message. Recognises four
|
|
47
|
+
* common error families: unexpected token, undefined name, not-a-fn,
|
|
48
|
+
* unexpected end of input. Returns an empty array for anything else
|
|
49
|
+
* (callers should still surface the raw error).
|
|
50
|
+
*/
|
|
51
|
+
export function getSuggestionsForError(error) {
|
|
52
|
+
const suggestions = [];
|
|
53
|
+
const lowerError = error.toLowerCase();
|
|
54
|
+
if (lowerError.includes('unexpected token')) {
|
|
55
|
+
suggestions.push('Check for missing quotes, parentheses, or brackets');
|
|
56
|
+
suggestions.push('Ensure all function calls have matching ()');
|
|
57
|
+
}
|
|
58
|
+
if (lowerError.includes('is not defined')) {
|
|
59
|
+
const match = error.match(/(\w+) is not defined/);
|
|
60
|
+
if (match) {
|
|
61
|
+
suggestions.push(`"${match[1]}" is not a known Strudel function`);
|
|
62
|
+
suggestions.push('Check spelling or use a valid function like s(), note(), stack()');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (lowerError.includes('not a function')) {
|
|
66
|
+
suggestions.push('Check that you are calling methods on a pattern object');
|
|
67
|
+
}
|
|
68
|
+
if (lowerError.includes('unexpected end')) {
|
|
69
|
+
suggestions.push('Pattern appears incomplete - check for missing closing brackets');
|
|
70
|
+
}
|
|
71
|
+
return suggestions;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Append warnings/suggestions for common pattern-code issues:
|
|
75
|
+
* - gain values above safe thresholds (2.0 warn, 5.0 strong warn)
|
|
76
|
+
* - patterns with no sound-producing call (s/sound/note/n/stack)
|
|
77
|
+
* - patterns with no tempo set (setcpm/setbpm/cpm/bpm)
|
|
78
|
+
*
|
|
79
|
+
* Mutates the passed arrays in-place to match the calling convention
|
|
80
|
+
* in StrudelEngine.validate(). Returns nothing.
|
|
81
|
+
*/
|
|
82
|
+
export function checkCommonIssues(code, warnings, suggestions) {
|
|
83
|
+
const gainMatches = code.match(/\.gain\s*\(\s*(\d+(?:\.\d+)?)\s*\)/g);
|
|
84
|
+
if (gainMatches) {
|
|
85
|
+
for (const match of gainMatches) {
|
|
86
|
+
const value = parseFloat(match.match(/(\d+(?:\.\d+)?)/)?.[1] || '0');
|
|
87
|
+
if (value > 2) {
|
|
88
|
+
warnings.push(`High gain value (${value}) may cause distortion`);
|
|
89
|
+
}
|
|
90
|
+
if (value > 5) {
|
|
91
|
+
warnings.push(`Dangerous gain value (${value}) - consider reducing to 2 or less`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (!/\b(s|sound|note|n)\s*\(/.test(code) && !/\bstack\s*\(/.test(code)) {
|
|
96
|
+
warnings.push('Pattern may not produce sound - no s(), note(), or stack() found');
|
|
97
|
+
suggestions.push('Add a sound source like s("bd") or note("c3")');
|
|
98
|
+
}
|
|
99
|
+
if (!/setcpm|setbpm|cpm|bpm/.test(code)) {
|
|
100
|
+
suggestions.push('Consider setting tempo with setcpm(120)');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Extract BPM from a `setcpm(<number>)` call. Returns undefined when
|
|
105
|
+
* absent — Strudel's default tempo is documented elsewhere; this helper
|
|
106
|
+
* stays narrow (no fallback) so callers can distinguish "not set" from
|
|
107
|
+
* "set to default".
|
|
108
|
+
*/
|
|
109
|
+
export function extractBpm(code) {
|
|
110
|
+
const match = code.match(/setcpm\s*\(\s*(\d+(?:\.\d+)?)\s*\)/);
|
|
111
|
+
return match ? parseFloat(match[1]) : undefined;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Extract unique function-call names from pattern code. Matches the
|
|
115
|
+
* lowercase-starting identifier immediately before `(` — same heuristic
|
|
116
|
+
* Strudel patterns follow (s, note, stack, fast, ...). Returns names in
|
|
117
|
+
* first-seen order, deduplicated.
|
|
118
|
+
*/
|
|
119
|
+
export function extractFunctionsUsed(code) {
|
|
120
|
+
const matches = code.match(/\b([a-z][a-zA-Z0-9_]*)\s*\(/g);
|
|
121
|
+
if (!matches)
|
|
122
|
+
return [];
|
|
123
|
+
const names = matches.map(m => m.replace(/\s*\($/, ''));
|
|
124
|
+
return [...new Set(names)];
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Score pattern complexity on a 0..1 scale. Two paths:
|
|
128
|
+
* - with events: weighted sum across event density, value variety,
|
|
129
|
+
* function count, stack bonus, code length
|
|
130
|
+
* - without events (e.g. unqueryable pattern): fall back to a simpler
|
|
131
|
+
* code-only estimate so we still return something useful
|
|
132
|
+
*
|
|
133
|
+
* Weights and caps are calibrated to keep typical patterns around 0.3–0.7
|
|
134
|
+
* with truly dense patterns reaching 1.0. Changing them shifts every
|
|
135
|
+
* downstream "complexity" report — bump cautiously.
|
|
136
|
+
*/
|
|
137
|
+
export function calculateComplexity(input, eventCount) {
|
|
138
|
+
if (eventCount === undefined) {
|
|
139
|
+
return Math.min((input.functionsUsed.length / 10) * 0.5 +
|
|
140
|
+
(input.codeLength / 500) * 0.3 +
|
|
141
|
+
(input.isStack ? 0.2 : 0), 1);
|
|
142
|
+
}
|
|
143
|
+
const factors = [
|
|
144
|
+
Math.min(eventCount / 16, 1) * 0.3,
|
|
145
|
+
Math.min(input.uniqueValues.length / 8, 1) * 0.2,
|
|
146
|
+
Math.min(input.functionsUsed.length / 10, 1) * 0.3,
|
|
147
|
+
input.isStack ? 0.1 : 0,
|
|
148
|
+
(input.codeLength / 500) * 0.1,
|
|
149
|
+
];
|
|
150
|
+
return Math.min(factors.reduce((a, b) => a + b, 0), 1);
|
|
151
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@williamzujkowski/live-coding-music-mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "MCP server (unofficial, fan project) for AI-assisted live-coding music via strudel.cc. Lets Claude drive pattern generation, audio analysis, and MIDI export through the Strudel browser REPL.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|