@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.
@@ -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 from Strudel output. Audio must be playing for capture to work.',
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 the recorded audio as base64-encoded data.',
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: 'Capture a fixed-duration audio sample from Strudel output. Audio must be playing.',
75
+ description: '[DEPRECATED — use audio_capture({ action: "sample" }) instead] Capture a fixed-duration audio sample.',
47
76
  inputSchema: {
48
77
  type: 'object',
49
- properties: { duration: { type: 'number', description: 'Duration in milliseconds (default: 5000)' } },
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.controller.page, { format });
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.controller.page);
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.controller.page, durationMs);
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 — performance, memory, status, errors, screenshots.
2
+ * diagnostics domain — observability tools (browser status, errors,
3
+ * server metrics).
3
4
  *
4
- * Owns (6 tools today): performance_report, memory_usage, screenshot,
5
- * status, diagnostics, show_errors.
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
- * Post-consolidation target (per #110 audit): one `diagnostics` tool with
8
- * a `level` enum. For now we keep the individual verbs and only move them
9
- * out of server.ts; consolidation happens later under #120.
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
- if (!ctx.isInitialized()) {
70
- return {
71
- initialized: false,
72
- message: 'Browser not initialized. Run init first for full diagnostics.',
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
- case 'show_errors': {
78
- const errors = ctx.controller.getConsoleErrors();
79
- const warnings = ctx.controller.getConsoleWarnings();
80
- if (errors.length === 0 && warnings.length === 0) {
81
- return 'No errors or warnings captured.';
82
- }
83
- let result = '';
84
- if (errors.length > 0) {
85
- result += `❌ Errors (${errors.length}):\n${errors.map(e => ` • ${e}`).join('\n')}\n`;
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 result.trim();
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}`);