@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
|
@@ -24,29 +24,61 @@ function blobToBase64(arrayBuffer) {
|
|
|
24
24
|
}
|
|
25
25
|
return btoa(binary);
|
|
26
26
|
}
|
|
27
|
+
const SESSION_ID_PROP = {
|
|
28
|
+
session_id: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
description: 'Optional session ID (#108). Routes page reference to the named session. Note: AudioCaptureService is currently server-wide — concurrent captures across sessions will conflict.',
|
|
31
|
+
},
|
|
32
|
+
};
|
|
27
33
|
export const tools = [
|
|
34
|
+
{
|
|
35
|
+
name: 'audio_capture',
|
|
36
|
+
description: 'Record audio output from the live Strudel session. ' +
|
|
37
|
+
'action=start begins streaming capture (optional `format` webm/opus, default webm). ' +
|
|
38
|
+
'action=stop ends the stream and returns base64-encoded audio. ' +
|
|
39
|
+
'action=sample captures a fixed-`duration` window in one call (100-60000ms, default 5000ms). ' +
|
|
40
|
+
'Example: audio_capture({ action: "sample", duration: 3000 }). ' +
|
|
41
|
+
'Audio must be playing for capture to record meaningful data. ' +
|
|
42
|
+
'For MIDI export use export_midi; for runtime diagnostics use diagnostics. ' +
|
|
43
|
+
'Note: the AudioCaptureService is currently a server-wide singleton — concurrent captures across sessions will conflict.',
|
|
44
|
+
inputSchema: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
properties: {
|
|
47
|
+
action: { type: 'string', enum: ['start', 'stop', 'sample'], description: 'Capture action' },
|
|
48
|
+
format: { type: 'string', enum: ['webm', 'opus'], description: 'action=start: audio format (default webm)' },
|
|
49
|
+
maxDuration: { type: 'number', description: 'action=start: maximum capture duration ms' },
|
|
50
|
+
duration: { type: 'number', description: 'action=sample: duration ms (100-60000, default 5000)' },
|
|
51
|
+
...SESSION_ID_PROP,
|
|
52
|
+
},
|
|
53
|
+
required: ['action'],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
28
56
|
{
|
|
29
57
|
name: 'start_audio_capture',
|
|
30
|
-
description: 'Start capturing audio
|
|
58
|
+
description: '[DEPRECATED — use audio_capture({ action: "start" }) instead] Start capturing audio. Audio must be playing.',
|
|
31
59
|
inputSchema: {
|
|
32
60
|
type: 'object',
|
|
33
61
|
properties: {
|
|
34
62
|
format: { type: 'string', enum: ['webm', 'opus'], description: 'Audio format (default: webm)' },
|
|
35
63
|
maxDuration: { type: 'number', description: 'Maximum capture duration in milliseconds' },
|
|
64
|
+
...SESSION_ID_PROP,
|
|
36
65
|
},
|
|
37
66
|
},
|
|
38
67
|
},
|
|
39
68
|
{
|
|
40
69
|
name: 'stop_audio_capture',
|
|
41
|
-
description: 'Stop audio capture and return
|
|
42
|
-
inputSchema: { type: 'object', properties: {} },
|
|
70
|
+
description: '[DEPRECATED — use audio_capture({ action: "stop" }) instead] Stop audio capture and return base64-encoded audio.',
|
|
71
|
+
inputSchema: { type: 'object', properties: { ...SESSION_ID_PROP } },
|
|
43
72
|
},
|
|
44
73
|
{
|
|
45
74
|
name: 'capture_audio_sample',
|
|
46
|
-
description: '
|
|
75
|
+
description: '[DEPRECATED — use audio_capture({ action: "sample" }) instead] Capture a fixed-duration audio sample.',
|
|
47
76
|
inputSchema: {
|
|
48
77
|
type: 'object',
|
|
49
|
-
properties: {
|
|
78
|
+
properties: {
|
|
79
|
+
duration: { type: 'number', description: 'Duration in milliseconds (default: 5000)' },
|
|
80
|
+
...SESSION_ID_PROP,
|
|
81
|
+
},
|
|
50
82
|
},
|
|
51
83
|
},
|
|
52
84
|
{
|
|
@@ -59,32 +91,47 @@ export const tools = [
|
|
|
59
91
|
duration: { type: 'number', description: 'Export duration in bars (default: 4)' },
|
|
60
92
|
bpm: { type: 'number', description: 'Tempo in BPM (default: 120)' },
|
|
61
93
|
format: { type: 'string', enum: ['file', 'base64'], description: 'Output format: file or base64 (default: base64)' },
|
|
94
|
+
...SESSION_ID_PROP,
|
|
62
95
|
},
|
|
63
96
|
},
|
|
64
97
|
},
|
|
65
98
|
];
|
|
66
99
|
export const toolNames = new Set(tools.map(t => t.name));
|
|
67
100
|
export async function execute(name, args, ctx) {
|
|
101
|
+
const sid = args?.session_id;
|
|
102
|
+
// No outer init check: each capture handler has try/catch that
|
|
103
|
+
// produces a structured { success: false, message } from any error,
|
|
104
|
+
// including AudioCaptureService's "Browser not initialized" throw.
|
|
68
105
|
switch (name) {
|
|
106
|
+
case 'audio_capture': {
|
|
107
|
+
const a = args?.action;
|
|
108
|
+
switch (a) {
|
|
109
|
+
case 'start': return await startAudioCapture(args?.format, args?.maxDuration, ctx, sid);
|
|
110
|
+
case 'stop': return await stopAudioCapture(ctx, sid);
|
|
111
|
+
case 'sample': return await captureAudioSample(args?.duration, ctx, sid);
|
|
112
|
+
default:
|
|
113
|
+
throw new Error(`Invalid action: ${a}. Must be one of: start, stop, sample`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
69
116
|
case 'start_audio_capture':
|
|
70
|
-
return await startAudioCapture(args?.format, args?.maxDuration, ctx);
|
|
117
|
+
return await startAudioCapture(args?.format, args?.maxDuration, ctx, sid);
|
|
71
118
|
case 'stop_audio_capture':
|
|
72
|
-
return await stopAudioCapture(ctx);
|
|
119
|
+
return await stopAudioCapture(ctx, sid);
|
|
73
120
|
case 'capture_audio_sample':
|
|
74
|
-
return await captureAudioSample(args?.duration, ctx);
|
|
121
|
+
return await captureAudioSample(args?.duration, ctx, sid);
|
|
75
122
|
case 'export_midi':
|
|
76
|
-
return await exportMidi(args?.filename, args?.duration, args?.bpm, args?.format, ctx);
|
|
123
|
+
return await exportMidi(args?.filename, args?.duration, args?.bpm, args?.format, ctx, sid);
|
|
77
124
|
default:
|
|
78
125
|
throw new Error(`capture module does not handle tool: ${name}`);
|
|
79
126
|
}
|
|
80
127
|
}
|
|
81
|
-
async function startAudioCapture(format, _maxDuration, ctx) {
|
|
128
|
+
async function startAudioCapture(format, _maxDuration, ctx, sid) {
|
|
82
129
|
try {
|
|
83
|
-
const service = await ctx.getAudioCaptureService();
|
|
130
|
+
const service = await ctx.getAudioCaptureService(sid);
|
|
84
131
|
if (service.isCapturing()) {
|
|
85
132
|
return { success: false, message: 'Audio capture already in progress. Stop it first.' };
|
|
86
133
|
}
|
|
87
|
-
await service.startCapture(ctx.
|
|
134
|
+
await service.startCapture(ctx.getController(sid).page, { format });
|
|
88
135
|
return {
|
|
89
136
|
success: true,
|
|
90
137
|
message: 'Audio capture started. Use stop_audio_capture to get the recorded audio.',
|
|
@@ -96,13 +143,13 @@ async function startAudioCapture(format, _maxDuration, ctx) {
|
|
|
96
143
|
return { success: false, message: `Failed to start audio capture: ${message}` };
|
|
97
144
|
}
|
|
98
145
|
}
|
|
99
|
-
async function stopAudioCapture(ctx) {
|
|
146
|
+
async function stopAudioCapture(ctx, sid) {
|
|
100
147
|
try {
|
|
101
|
-
const service = await ctx.getAudioCaptureService();
|
|
148
|
+
const service = await ctx.getAudioCaptureService(sid);
|
|
102
149
|
if (!service.isCapturing()) {
|
|
103
150
|
return { success: false, message: 'No audio capture in progress. Start capture first.' };
|
|
104
151
|
}
|
|
105
|
-
const result = await service.stopCapture(ctx.
|
|
152
|
+
const result = await service.stopCapture(ctx.getController(sid).page);
|
|
106
153
|
const buf = await result.blob.arrayBuffer();
|
|
107
154
|
return {
|
|
108
155
|
success: true,
|
|
@@ -117,17 +164,17 @@ async function stopAudioCapture(ctx) {
|
|
|
117
164
|
return { success: false, message: `Failed to stop audio capture: ${message}` };
|
|
118
165
|
}
|
|
119
166
|
}
|
|
120
|
-
async function captureAudioSample(duration, ctx) {
|
|
167
|
+
async function captureAudioSample(duration, ctx, sid) {
|
|
121
168
|
const durationMs = duration || 5000;
|
|
122
169
|
if (durationMs < 100 || durationMs > 60000) {
|
|
123
170
|
return { success: false, message: 'Duration must be between 100ms and 60000ms (1 minute)' };
|
|
124
171
|
}
|
|
125
172
|
try {
|
|
126
|
-
const service = await ctx.getAudioCaptureService();
|
|
173
|
+
const service = await ctx.getAudioCaptureService(sid);
|
|
127
174
|
if (service.isCapturing()) {
|
|
128
175
|
return { success: false, message: 'Audio capture already in progress. Stop it first.' };
|
|
129
176
|
}
|
|
130
|
-
const result = await service.captureForDuration(ctx.
|
|
177
|
+
const result = await service.captureForDuration(ctx.getController(sid).page, durationMs);
|
|
131
178
|
const buf = await result.blob.arrayBuffer();
|
|
132
179
|
return {
|
|
133
180
|
success: true,
|
|
@@ -142,13 +189,13 @@ async function captureAudioSample(duration, ctx) {
|
|
|
142
189
|
return { success: false, message: `Failed to capture audio sample: ${message}` };
|
|
143
190
|
}
|
|
144
191
|
}
|
|
145
|
-
async function exportMidi(filename, bars, bpm, format, ctx) {
|
|
192
|
+
async function exportMidi(filename, bars, bpm, format, ctx, sid) {
|
|
146
193
|
if (bpm !== undefined)
|
|
147
194
|
InputValidator.validateBPM(bpm);
|
|
148
195
|
if (bars !== undefined && (bars < 1 || bars > 128)) {
|
|
149
196
|
return { success: false, message: 'Bars must be between 1 and 128' };
|
|
150
197
|
}
|
|
151
|
-
const pattern = await ctx.getCurrentPatternSafe();
|
|
198
|
+
const pattern = await ctx.getCurrentPatternSafe(sid);
|
|
152
199
|
if (!pattern || pattern.trim().length === 0) {
|
|
153
200
|
return { success: false, message: 'No pattern to export. Write a pattern first.' };
|
|
154
201
|
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* compose domain — high-level UX tools that aren't a thin verb on a
|
|
3
|
+
* subsystem: `compose` (one-shot generate+write+play) and `show_browser`
|
|
4
|
+
* (bring the Strudel window forward).
|
|
5
|
+
*
|
|
6
|
+
* Post-consolidation (per #110 audit):
|
|
7
|
+
* - `compose` absorbs `generate_pattern` after the alias period
|
|
8
|
+
* - `show_browser` merges with `screenshot` into `browser_window`
|
|
9
|
+
* These targets live in different epics (#120). Keeping the two tools
|
|
10
|
+
* together here matches today's "UX Tools" grouping; the file is small
|
|
11
|
+
* enough to split later without disruption.
|
|
12
|
+
*/
|
|
13
|
+
import { InputValidator } from '../../utils/InputValidator.js';
|
|
14
|
+
const SESSION_ID_PROP = {
|
|
15
|
+
session_id: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
description: 'Optional session ID (#108). Omit to use default session. compose auto-init only applies to default session — named sessions must already exist.',
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
export const tools = [
|
|
21
|
+
{
|
|
22
|
+
name: 'browser_window',
|
|
23
|
+
description: 'Interact with the visible Strudel browser window. ' +
|
|
24
|
+
'action=show brings the window to the foreground. ' +
|
|
25
|
+
'action=screenshot captures the current editor view to disk (optional `filename`). ' +
|
|
26
|
+
'Example: browser_window({ action: "screenshot", filename: "demo.png" }). ' +
|
|
27
|
+
'For diagnostics (status/errors/perf) use diagnostics; for the editor itself use edit_pattern.',
|
|
28
|
+
inputSchema: {
|
|
29
|
+
type: 'object',
|
|
30
|
+
properties: {
|
|
31
|
+
action: { type: 'string', enum: ['show', 'screenshot'], description: 'Window action' },
|
|
32
|
+
filename: { type: 'string', description: 'action=screenshot: optional output filename' },
|
|
33
|
+
...SESSION_ID_PROP,
|
|
34
|
+
},
|
|
35
|
+
required: ['action'],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'show_browser',
|
|
40
|
+
description: '[DEPRECATED — use browser_window({ action: "show" }) instead] Bring browser window to foreground',
|
|
41
|
+
inputSchema: { type: 'object', properties: { ...SESSION_ID_PROP } },
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'compose',
|
|
45
|
+
description: 'Generate, write, and play a complete pattern in one step. Auto-initializes default browser if needed.',
|
|
46
|
+
inputSchema: {
|
|
47
|
+
type: 'object',
|
|
48
|
+
properties: {
|
|
49
|
+
style: { type: 'string', description: 'Genre: techno, house, dnb, ambient, trap, jungle, jazz, experimental' },
|
|
50
|
+
tempo: { type: 'number', description: 'BPM (default: genre-appropriate)' },
|
|
51
|
+
key: { type: 'string', description: 'Musical key (default: C)' },
|
|
52
|
+
auto_play: { type: 'boolean', description: 'Start playback immediately (default: true)' },
|
|
53
|
+
get_feedback: { type: 'boolean', description: 'Get AI feedback on the generated pattern (default: false)' },
|
|
54
|
+
...SESSION_ID_PROP,
|
|
55
|
+
},
|
|
56
|
+
required: ['style'],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
export const toolNames = new Set(tools.map(t => t.name));
|
|
61
|
+
const TEMPO_BY_STYLE = {
|
|
62
|
+
techno: 130,
|
|
63
|
+
house: 125,
|
|
64
|
+
dnb: 174,
|
|
65
|
+
'drum and bass': 174,
|
|
66
|
+
ambient: 80,
|
|
67
|
+
trap: 140,
|
|
68
|
+
jungle: 160,
|
|
69
|
+
jazz: 110,
|
|
70
|
+
experimental: 120,
|
|
71
|
+
dubstep: 140,
|
|
72
|
+
trance: 138,
|
|
73
|
+
breakbeat: 130,
|
|
74
|
+
garage: 130,
|
|
75
|
+
electro: 128,
|
|
76
|
+
downtempo: 90,
|
|
77
|
+
idm: 115,
|
|
78
|
+
};
|
|
79
|
+
function defaultTempo(style) {
|
|
80
|
+
return TEMPO_BY_STYLE[style.toLowerCase()] ?? 120;
|
|
81
|
+
}
|
|
82
|
+
async function doShow(ctx, sid) {
|
|
83
|
+
if (!sid && !ctx.isInitialized()) {
|
|
84
|
+
return 'Browser not initialized. Run init first.';
|
|
85
|
+
}
|
|
86
|
+
return await ctx.getController(sid).showBrowser();
|
|
87
|
+
}
|
|
88
|
+
async function doScreenshot(args, ctx, sid) {
|
|
89
|
+
if (!sid && !ctx.isInitialized()) {
|
|
90
|
+
return 'Browser not initialized. Run init first.';
|
|
91
|
+
}
|
|
92
|
+
return await ctx.getController(sid).takeScreenshot(args?.filename);
|
|
93
|
+
}
|
|
94
|
+
export async function execute(name, args, ctx) {
|
|
95
|
+
const sid = args?.session_id;
|
|
96
|
+
switch (name) {
|
|
97
|
+
case 'browser_window': {
|
|
98
|
+
const a = args?.action;
|
|
99
|
+
if (a !== 'show' && a !== 'screenshot') {
|
|
100
|
+
throw new Error(`Invalid action: ${a}. Must be one of: show, screenshot`);
|
|
101
|
+
}
|
|
102
|
+
return a === 'show' ? await doShow(ctx, sid) : await doScreenshot(args, ctx, sid);
|
|
103
|
+
}
|
|
104
|
+
case 'show_browser':
|
|
105
|
+
return await doShow(ctx, sid);
|
|
106
|
+
case 'compose': {
|
|
107
|
+
InputValidator.validateStringLength(args.style, 'style', 100, false);
|
|
108
|
+
if (args.key) {
|
|
109
|
+
InputValidator.validateRootNote(args.key);
|
|
110
|
+
}
|
|
111
|
+
if (args.tempo !== undefined) {
|
|
112
|
+
InputValidator.validateBPM(args.tempo);
|
|
113
|
+
}
|
|
114
|
+
// Auto-init only the default session. Named sessions must be
|
|
115
|
+
// created via `create_session` first — ctx.getController(sid)
|
|
116
|
+
// throws if missing, which the dispatcher renders as err('business').
|
|
117
|
+
if (!sid) {
|
|
118
|
+
await ctx.ensureInitialized();
|
|
119
|
+
}
|
|
120
|
+
const controller = ctx.getController(sid);
|
|
121
|
+
const tempo = args.tempo || defaultTempo(args.style);
|
|
122
|
+
const key = args.key || 'C';
|
|
123
|
+
const pattern = ctx.generator.generateCompletePattern(args.style, key, tempo);
|
|
124
|
+
await controller.writePattern(pattern);
|
|
125
|
+
const shouldPlay = args.auto_play !== false;
|
|
126
|
+
if (shouldPlay) {
|
|
127
|
+
await controller.play();
|
|
128
|
+
}
|
|
129
|
+
const response = {
|
|
130
|
+
success: true,
|
|
131
|
+
pattern: pattern.substring(0, 200) + (pattern.length > 200 ? '...' : ''),
|
|
132
|
+
metadata: { style: args.style, bpm: tempo, key },
|
|
133
|
+
status: shouldPlay ? 'playing' : 'ready',
|
|
134
|
+
message: `Created ${args.style} pattern in ${key}${shouldPlay ? ' - now playing' : ''}`,
|
|
135
|
+
};
|
|
136
|
+
if (args.get_feedback) {
|
|
137
|
+
if (ctx.geminiService.isAvailable()) {
|
|
138
|
+
try {
|
|
139
|
+
const feedback = await ctx.geminiService.getCreativeFeedback(pattern);
|
|
140
|
+
response.feedback = feedback;
|
|
141
|
+
response.message += ` (AI feedback: ${feedback.complexity} complexity, estimated ${feedback.estimatedStyle})`;
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
145
|
+
ctx.logger.warn('Failed to get AI feedback for compose', { error: message });
|
|
146
|
+
response.message += ' (AI feedback unavailable)';
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
response.message += ' (AI feedback requires GEMINI_API_KEY)';
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return response;
|
|
154
|
+
}
|
|
155
|
+
default:
|
|
156
|
+
throw new Error(`compose module does not handle tool: ${name}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
export const composeModule = { tools, toolNames, execute };
|
|
@@ -1,93 +1,145 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* diagnostics domain —
|
|
2
|
+
* diagnostics domain — observability tools (browser status, errors,
|
|
3
|
+
* server metrics).
|
|
3
4
|
*
|
|
4
|
-
* Owns
|
|
5
|
-
* status
|
|
5
|
+
* Owns one consolidated tool (`diagnostics(level)`) plus four deprecated
|
|
6
|
+
* aliases (`status`, `show_errors`, `performance_report`, `memory_usage`)
|
|
7
|
+
* per #120 / #144.
|
|
6
8
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
9
|
+
* The `<15ms` SLA on `level='status'` is preserved — that branch only
|
|
10
|
+
* reads from the controller's cache, not the browser. Other levels can
|
|
11
|
+
* touch the browser (`level='full'`) or just inspect server state
|
|
12
|
+
* (`perf`, `memory`).
|
|
13
|
+
*
|
|
14
|
+
* `screenshot` stays separate; it folds into `browser_window` per
|
|
15
|
+
* Phase 3 (#156).
|
|
10
16
|
*/
|
|
17
|
+
const SESSION_ID_PROP = {
|
|
18
|
+
session_id: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
description: 'Optional session ID (#108). Applies to level=status, full, errors. Ignored for perf/memory (server-wide metrics).',
|
|
21
|
+
},
|
|
22
|
+
};
|
|
11
23
|
export const tools = [
|
|
24
|
+
{
|
|
25
|
+
name: 'diagnostics',
|
|
26
|
+
description: 'Inspect server and browser state. ' +
|
|
27
|
+
'level=status returns a quick state snapshot (cache read, <15ms SLA). ' +
|
|
28
|
+
'level=full returns detailed browser diagnostics including caches, errors, and performance. ' +
|
|
29
|
+
'level=perf returns server-side timing metrics + top bottlenecks. ' +
|
|
30
|
+
'level=memory returns process memory usage. ' +
|
|
31
|
+
'level=errors returns captured console errors and warnings from Strudel. ' +
|
|
32
|
+
'Default level=full preserves the pre-consolidation behaviour. ' +
|
|
33
|
+
'Example: diagnostics({ level: "status" }) — millisecond-cheap. ' +
|
|
34
|
+
'For screenshots use browser_window (Phase 3 / #156); for tool listings use the strudel://docs/tools resource.',
|
|
35
|
+
inputSchema: {
|
|
36
|
+
type: 'object',
|
|
37
|
+
properties: {
|
|
38
|
+
level: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
enum: ['status', 'full', 'perf', 'memory', 'errors'],
|
|
41
|
+
description: 'Which diagnostic surface to read (default: full)',
|
|
42
|
+
},
|
|
43
|
+
...SESSION_ID_PROP,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'status',
|
|
49
|
+
description: '[DEPRECATED — use diagnostics({ level: "status" }) instead] Get current browser and playback status (quick state check)',
|
|
50
|
+
inputSchema: { type: 'object', properties: { ...SESSION_ID_PROP } },
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'show_errors',
|
|
54
|
+
description: '[DEPRECATED — use diagnostics({ level: "errors" }) instead] Display captured console errors and warnings from Strudel',
|
|
55
|
+
inputSchema: { type: 'object', properties: { ...SESSION_ID_PROP } },
|
|
56
|
+
},
|
|
12
57
|
{
|
|
13
58
|
name: 'performance_report',
|
|
14
|
-
description: 'Get performance metrics and bottlenecks',
|
|
59
|
+
description: '[DEPRECATED — use diagnostics({ level: "perf" }) instead] Get performance metrics and bottlenecks',
|
|
15
60
|
inputSchema: { type: 'object', properties: {} },
|
|
16
61
|
},
|
|
17
62
|
{
|
|
18
63
|
name: 'memory_usage',
|
|
19
|
-
description: 'Get current memory usage statistics',
|
|
64
|
+
description: '[DEPRECATED — use diagnostics({ level: "memory" }) instead] Get current memory usage statistics',
|
|
20
65
|
inputSchema: { type: 'object', properties: {} },
|
|
21
66
|
},
|
|
22
67
|
{
|
|
23
68
|
name: 'screenshot',
|
|
24
|
-
description: 'Take a screenshot of the current Strudel editor state',
|
|
69
|
+
description: '[DEPRECATED — use browser_window({ action: "screenshot" }) instead] Take a screenshot of the current Strudel editor state',
|
|
25
70
|
inputSchema: {
|
|
26
71
|
type: 'object',
|
|
27
72
|
properties: {
|
|
28
73
|
filename: { type: 'string', description: 'Optional filename for screenshot' },
|
|
74
|
+
...SESSION_ID_PROP,
|
|
29
75
|
},
|
|
30
76
|
},
|
|
31
77
|
},
|
|
32
|
-
{
|
|
33
|
-
name: 'status',
|
|
34
|
-
description: 'Get current browser and playback status (quick state check)',
|
|
35
|
-
inputSchema: { type: 'object', properties: {} },
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
name: 'diagnostics',
|
|
39
|
-
description: 'Get detailed browser diagnostics including cache, errors, and performance',
|
|
40
|
-
inputSchema: { type: 'object', properties: {} },
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
name: 'show_errors',
|
|
44
|
-
description: 'Display captured console errors and warnings from Strudel',
|
|
45
|
-
inputSchema: { type: 'object', properties: {} },
|
|
46
|
-
},
|
|
47
78
|
];
|
|
48
79
|
export const toolNames = new Set(tools.map(t => t.name));
|
|
80
|
+
function getPerformanceReport(ctx) {
|
|
81
|
+
const report = ctx.perfMonitor.getReport();
|
|
82
|
+
const bottlenecks = ctx.perfMonitor.getBottlenecks(5);
|
|
83
|
+
return `${report}\n\nTop 5 Bottlenecks:\n${JSON.stringify(bottlenecks, null, 2)}`;
|
|
84
|
+
}
|
|
85
|
+
function getMemoryUsage(ctx) {
|
|
86
|
+
const memory = ctx.perfMonitor.getMemoryUsage();
|
|
87
|
+
return memory ? JSON.stringify(memory, null, 2) : 'Memory usage not available';
|
|
88
|
+
}
|
|
89
|
+
function getStatus(ctx, sid) {
|
|
90
|
+
return ctx.getController(sid).getStatus();
|
|
91
|
+
}
|
|
92
|
+
async function getFullDiagnostics(ctx, sid) {
|
|
93
|
+
if (!sid && !ctx.isInitialized()) {
|
|
94
|
+
return {
|
|
95
|
+
initialized: false,
|
|
96
|
+
message: 'Browser not initialized. Run init first for full diagnostics.',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return await ctx.getController(sid).getDiagnostics();
|
|
100
|
+
}
|
|
101
|
+
function getErrors(ctx, sid) {
|
|
102
|
+
const controller = ctx.getController(sid);
|
|
103
|
+
const errors = controller.getConsoleErrors();
|
|
104
|
+
const warnings = controller.getConsoleWarnings();
|
|
105
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
106
|
+
return 'No errors or warnings captured.';
|
|
107
|
+
}
|
|
108
|
+
let result = '';
|
|
109
|
+
if (errors.length > 0) {
|
|
110
|
+
result += `❌ Errors (${errors.length}):\n${errors.map(e => ` • ${e}`).join('\n')}\n`;
|
|
111
|
+
}
|
|
112
|
+
if (warnings.length > 0) {
|
|
113
|
+
result += `⚠️ Warnings (${warnings.length}):\n${warnings.map(w => ` • ${w}`).join('\n')}`;
|
|
114
|
+
}
|
|
115
|
+
return result.trim();
|
|
116
|
+
}
|
|
49
117
|
export async function execute(name, args, ctx) {
|
|
118
|
+
const sid = args?.session_id;
|
|
50
119
|
switch (name) {
|
|
51
|
-
case 'performance_report': {
|
|
52
|
-
const report = ctx.perfMonitor.getReport();
|
|
53
|
-
const bottlenecks = ctx.perfMonitor.getBottlenecks(5);
|
|
54
|
-
return `${report}\n\nTop 5 Bottlenecks:\n${JSON.stringify(bottlenecks, null, 2)}`;
|
|
55
|
-
}
|
|
56
|
-
case 'memory_usage': {
|
|
57
|
-
const memory = ctx.perfMonitor.getMemoryUsage();
|
|
58
|
-
return memory ? JSON.stringify(memory, null, 2) : 'Memory usage not available';
|
|
59
|
-
}
|
|
60
|
-
case 'screenshot': {
|
|
61
|
-
if (!ctx.isInitialized()) {
|
|
62
|
-
return 'Browser not initialized. Run init first.';
|
|
63
|
-
}
|
|
64
|
-
return await ctx.controller.takeScreenshot(args?.filename);
|
|
65
|
-
}
|
|
66
|
-
case 'status':
|
|
67
|
-
return ctx.controller.getStatus();
|
|
68
120
|
case 'diagnostics': {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
121
|
+
const level = args?.level ?? 'full';
|
|
122
|
+
switch (level) {
|
|
123
|
+
case 'status': return getStatus(ctx, sid);
|
|
124
|
+
case 'full': return await getFullDiagnostics(ctx, sid);
|
|
125
|
+
case 'perf': return getPerformanceReport(ctx);
|
|
126
|
+
case 'memory': return getMemoryUsage(ctx);
|
|
127
|
+
case 'errors': return getErrors(ctx, sid);
|
|
128
|
+
default:
|
|
129
|
+
throw new Error(`Invalid level: ${level}. Must be one of: status, full, perf, memory, errors`);
|
|
74
130
|
}
|
|
75
|
-
return await ctx.controller.getDiagnostics();
|
|
76
131
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
if (warnings.length > 0) {
|
|
88
|
-
result += `⚠️ Warnings (${warnings.length}):\n${warnings.map(w => ` • ${w}`).join('\n')}`;
|
|
132
|
+
// Deprecated aliases — forward to consolidated handler. Kept for ≥1
|
|
133
|
+
// release per #120 migration policy.
|
|
134
|
+
case 'status': return getStatus(ctx, sid);
|
|
135
|
+
case 'show_errors': return getErrors(ctx, sid);
|
|
136
|
+
case 'performance_report': return getPerformanceReport(ctx);
|
|
137
|
+
case 'memory_usage': return getMemoryUsage(ctx);
|
|
138
|
+
case 'screenshot': {
|
|
139
|
+
if (!sid && !ctx.isInitialized()) {
|
|
140
|
+
return 'Browser not initialized. Run init first.';
|
|
89
141
|
}
|
|
90
|
-
return
|
|
142
|
+
return await ctx.getController(sid).takeScreenshot(args?.filename);
|
|
91
143
|
}
|
|
92
144
|
default:
|
|
93
145
|
throw new Error(`diagnostics module does not handle tool: ${name}`);
|