@williamzujkowski/live-coding-music-mcp 3.0.0 → 4.1.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 +212 -780
- package/dist/AudioAnalyzer.js +1294 -91
- package/dist/PatternStore.js +276 -23
- package/dist/StrudelController.js +388 -56
- package/dist/server/resources.js +27 -20
- package/dist/server/server.js +391 -83
- package/dist/server/tools/ai.js +129 -152
- package/dist/server/tools/analysis.js +170 -91
- package/dist/server/tools/capture.js +128 -55
- package/dist/server/tools/compose.js +28 -42
- package/dist/server/tools/diagnostics.js +23 -53
- package/dist/server/tools/editor.js +63 -72
- package/dist/server/tools/generate.js +45 -167
- package/dist/server/tools/history.js +35 -66
- package/dist/server/tools/playback.js +7 -22
- package/dist/server/tools/session.js +41 -47
- package/dist/server/tools/storage.js +173 -50
- package/dist/server/tools/transform.js +136 -199
- package/dist/server/tools/types.js +209 -14
- package/dist/services/AudioCaptureService.js +302 -33
- package/dist/services/AudioExportService.js +318 -0
- package/dist/services/BrowserOnlyFunctions.js +149 -0
- package/dist/services/EventDensityProbe.js +324 -0
- package/dist/services/GeminiService.js +221 -36
- package/dist/services/IsolatedEngineRunner.js +308 -0
- package/dist/services/IsolatedStrudelEngine.js +135 -0
- package/dist/services/LocalPatternEngine.js +16 -0
- package/dist/services/MIDIExportService.js +765 -71
- package/dist/services/MIDIImportService.js +474 -0
- package/dist/services/MusicTheory.js +85 -13
- package/dist/services/PatternGenerator.js +168 -85
- package/dist/services/PatternSandbox.js +359 -0
- package/dist/services/SessionManager.js +338 -65
- package/dist/services/StrudelEngine.js +218 -12
- package/dist/services/StrudelEngineHelpers.js +24 -8
- package/dist/services/StyleRegistry.js +204 -0
- package/dist/services/Transferable.js +114 -0
- package/dist/services/ai/AiTransport.js +56 -0
- package/dist/services/ai/AudioMeasurements.js +155 -0
- package/dist/services/ai/CliTransport.js +256 -0
- package/dist/services/engineChild.js +83 -0
- package/dist/services/engineChildPath.js +28 -0
- package/dist/utils/CategorisedError.js +59 -0
- package/dist/utils/ErrorRecovery.js +130 -110
- package/dist/utils/InputValidator.js +55 -7
- package/dist/utils/PatternValidator.js +136 -17
- package/dist/utils/SafePath.js +112 -0
- package/dist/utils/ServerConfig.js +248 -0
- package/dist/utils/TableLookup.js +41 -0
- package/dist/utils/Tempo.js +143 -0
- package/package.json +17 -5
- package/dist/services/__mocks__/StrudelEngine.js +0 -157
- package/dist/types/index.js +0 -1
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* analysis domain — audio analysis + pattern validation (browser + local).
|
|
3
3
|
*
|
|
4
|
-
* Owns (
|
|
5
|
-
* browser-required: analyze
|
|
6
|
-
* detect_tempo, detect_key, validate_pattern_runtime
|
|
4
|
+
* Owns (6 tools):
|
|
5
|
+
* browser-required: analyze (include[] filter), validate_pattern_runtime
|
|
7
6
|
* browser-free: validate_pattern_local, analyze_pattern_local,
|
|
8
7
|
* query_pattern_events, transpile_pattern
|
|
9
8
|
*
|
|
10
|
-
* The browser-free set runs against the
|
|
9
|
+
* The browser-free set runs against the local engine — since #307, a
|
|
10
|
+
* forked child with a heap cap and a deadline, not this process — and
|
|
11
11
|
* pairs naturally with validate_pattern_runtime (which monitors the
|
|
12
12
|
* actual Strudel console). They were orphan handlers — implementations
|
|
13
13
|
* present but not registered in tools/list — until #124.
|
|
14
|
-
*
|
|
15
|
-
* Post-consolidation (per #110 audit): a single `analyze` tool with
|
|
16
|
-
* an `include[]` filter absorbs the 4 audio detection tools.
|
|
17
|
-
* Local-engine tools stay distinct — different subsystem.
|
|
18
14
|
*/
|
|
15
|
+
import { empty, err, ok } from './types.js';
|
|
19
16
|
import { InputValidator } from '../../utils/InputValidator.js';
|
|
17
|
+
import { IsolatedRunnerError } from '../../services/IsolatedEngineRunner.js';
|
|
18
|
+
import { ValidationError } from '../../utils/CategorisedError.js';
|
|
20
19
|
const SESSION_ID_PROP = {
|
|
21
20
|
session_id: {
|
|
22
21
|
type: 'string',
|
|
@@ -47,26 +46,6 @@ export const tools = [
|
|
|
47
46
|
},
|
|
48
47
|
},
|
|
49
48
|
},
|
|
50
|
-
{
|
|
51
|
-
name: 'analyze_spectrum',
|
|
52
|
-
description: '[DEPRECATED — use analyze({ include: ["spectrum"] }) instead] FFT spectrum analysis',
|
|
53
|
-
inputSchema: { type: 'object', properties: { ...SESSION_ID_PROP } },
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
name: 'analyze_rhythm',
|
|
57
|
-
description: '[DEPRECATED — use analyze({ include: ["rhythm"] }) instead] Rhythm analysis',
|
|
58
|
-
inputSchema: { type: 'object', properties: { ...SESSION_ID_PROP } },
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
name: 'detect_tempo',
|
|
62
|
-
description: '[DEPRECATED — use analyze({ include: ["tempo"] }) instead] BPM detection',
|
|
63
|
-
inputSchema: { type: 'object', properties: { ...SESSION_ID_PROP } },
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
name: 'detect_key',
|
|
67
|
-
description: '[DEPRECATED — use analyze({ include: ["key"] }) instead] Key detection',
|
|
68
|
-
inputSchema: { type: 'object', properties: { ...SESSION_ID_PROP } },
|
|
69
|
-
},
|
|
70
49
|
{
|
|
71
50
|
name: 'validate_pattern_runtime',
|
|
72
51
|
description: 'Validate pattern with runtime error checking (monitors Strudel console for errors)',
|
|
@@ -82,7 +61,7 @@ export const tools = [
|
|
|
82
61
|
},
|
|
83
62
|
{
|
|
84
63
|
name: 'validate_pattern_local',
|
|
85
|
-
description: 'Validate pattern syntax against the in
|
|
64
|
+
description: 'Validate pattern syntax against the local StrudelEngine, which runs in a sandboxed child process (no browser required)',
|
|
86
65
|
inputSchema: {
|
|
87
66
|
type: 'object',
|
|
88
67
|
properties: {
|
|
@@ -143,57 +122,80 @@ async function getRhythm(controller) {
|
|
|
143
122
|
return await controller.analyzeRhythm();
|
|
144
123
|
}
|
|
145
124
|
async function getTempo(controller) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
125
|
+
const tempoAnalysis = await controller.detectTempo();
|
|
126
|
+
if (!tempoAnalysis || tempoAnalysis.bpm === 0) {
|
|
127
|
+
// The measurement ran and detected nothing. `bpm: 0` read as a
|
|
128
|
+
// measured value — zero beats per minute — so null plus an
|
|
129
|
+
// explicit flag (#288). No envelope here: getTempo is a sub-result
|
|
130
|
+
// composed into analyze's object, and an envelope nested inside a
|
|
131
|
+
// data payload is not the contract.
|
|
132
|
+
//
|
|
133
|
+
// Two different states arrive here. A reading that is merely
|
|
134
|
+
// settling has a candidate already and needs another poll; telling
|
|
135
|
+
// its caller to check whether audio is playing, while audio IS
|
|
136
|
+
// playing, sends it to fix the wrong thing (#374).
|
|
155
137
|
return {
|
|
156
|
-
bpm:
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
message:
|
|
138
|
+
bpm: null,
|
|
139
|
+
detected: false,
|
|
140
|
+
confidence: 0,
|
|
141
|
+
message: tempoAnalysis?.settling === true
|
|
142
|
+
? 'Tempo is still settling — a reading was taken but not yet confirmed by a second '
|
|
143
|
+
+ 'window. Call again in a second or two.'
|
|
144
|
+
: 'No tempo detected. Ensure audio is playing and has a clear rhythmic pattern.',
|
|
160
145
|
};
|
|
161
146
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
147
|
+
return {
|
|
148
|
+
bpm: tempoAnalysis.bpm,
|
|
149
|
+
confidence: Math.round(tempoAnalysis.confidence * 100) / 100,
|
|
150
|
+
method: tempoAnalysis.method,
|
|
151
|
+
message: `Detected ${tempoAnalysis.bpm} BPM with ${Math.round(tempoAnalysis.confidence * 100)}% confidence`,
|
|
152
|
+
};
|
|
153
|
+
// No catch here on purpose.
|
|
154
|
+
//
|
|
155
|
+
// It used to return `{bpm: null, detected: false, error: message}`,
|
|
156
|
+
// which analyze nests under the `tempo` key — where `isFailureShaped`
|
|
157
|
+
// cannot see it, since that inspects only the top level. So a dead
|
|
158
|
+
// browser reached the client as:
|
|
159
|
+
//
|
|
160
|
+
// include:['tempo'] -> {ok:true, data:{tempo:{bpm:null,
|
|
161
|
+
// error:'Target page ... has been closed'}}}
|
|
162
|
+
// include:['all'] -> a failure envelope
|
|
163
|
+
//
|
|
164
|
+
// One tool, one fault, opposite verdicts (#453). Letting it propagate
|
|
165
|
+
// makes both agree, and keeps the ORIGINAL error so `categorizeError`
|
|
166
|
+
// reads its type rather than a rebuilt message (#382).
|
|
167
|
+
//
|
|
168
|
+
// The no-detection branch above is untouched: that is a measurement
|
|
169
|
+
// that ran and found nothing, which is data, not a failure (#288).
|
|
166
170
|
}
|
|
167
171
|
async function getKey(controller) {
|
|
168
|
-
try
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
key: keyAnalysis.key,
|
|
180
|
-
scale: keyAnalysis.scale,
|
|
181
|
-
confidence: Math.round(keyAnalysis.confidence * 100) / 100,
|
|
182
|
-
message: `Detected ${keyAnalysis.key} ${keyAnalysis.scale} with ${Math.round(keyAnalysis.confidence * 100)}% confidence`,
|
|
172
|
+
// No try/catch, for the reason given on getTempo: an error nested
|
|
173
|
+
// inside a data payload is invisible to `isFailureShaped` and reached
|
|
174
|
+
// the client as ok:true (#453).
|
|
175
|
+
const keyAnalysis = await controller.detectKey();
|
|
176
|
+
if (!keyAnalysis || keyAnalysis.confidence < 0.1) {
|
|
177
|
+
return {
|
|
178
|
+
key: null,
|
|
179
|
+
scale: null,
|
|
180
|
+
detected: false,
|
|
181
|
+
confidence: 0,
|
|
182
|
+
message: 'No clear key detected. Ensure audio is playing and has tonal content.',
|
|
183
183
|
};
|
|
184
|
-
if (keyAnalysis.alternatives && keyAnalysis.alternatives.length > 0) {
|
|
185
|
-
result.alternatives = keyAnalysis.alternatives.map((alt) => ({
|
|
186
|
-
key: alt.key,
|
|
187
|
-
scale: alt.scale,
|
|
188
|
-
confidence: Math.round(alt.confidence * 100) / 100,
|
|
189
|
-
}));
|
|
190
|
-
}
|
|
191
|
-
return result;
|
|
192
184
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
185
|
+
const result = {
|
|
186
|
+
key: keyAnalysis.key,
|
|
187
|
+
scale: keyAnalysis.scale,
|
|
188
|
+
confidence: Math.round(keyAnalysis.confidence * 100) / 100,
|
|
189
|
+
message: `Detected ${keyAnalysis.key} ${keyAnalysis.scale} with ${Math.round(keyAnalysis.confidence * 100)}% confidence`,
|
|
190
|
+
};
|
|
191
|
+
if (keyAnalysis.alternatives && keyAnalysis.alternatives.length > 0) {
|
|
192
|
+
result.alternatives = keyAnalysis.alternatives.map((alt) => ({
|
|
193
|
+
key: alt.key,
|
|
194
|
+
scale: alt.scale,
|
|
195
|
+
confidence: Math.round(alt.confidence * 100) / 100,
|
|
196
|
+
}));
|
|
196
197
|
}
|
|
198
|
+
return result;
|
|
197
199
|
}
|
|
198
200
|
async function doAnalyze(args, controller) {
|
|
199
201
|
const includeRaw = args?.include;
|
|
@@ -223,7 +225,44 @@ async function doAnalyze(args, controller) {
|
|
|
223
225
|
const entries = await Promise.all(Object.entries(tasks).map(async ([k, p]) => [k, await p]));
|
|
224
226
|
return Object.fromEntries(entries);
|
|
225
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* Turns a death of the isolated engine child into an envelope (#307).
|
|
230
|
+
*
|
|
231
|
+
* All four local-engine tools funnel through here, which is the point:
|
|
232
|
+
* one isolation path, one place that decides what a caller is told.
|
|
233
|
+
*
|
|
234
|
+
* The distinction the categories carry is whether retrying could possibly
|
|
235
|
+
* help. It cannot for an OOM — `new Array(5e7).fill(7)` will exhaust the
|
|
236
|
+
* cap every time it is sent — so that is the caller's input to fix, not a
|
|
237
|
+
* transient condition to wait out. A hang or an unexplained crash might
|
|
238
|
+
* well be transient. Recording this because the vote on #307 said "one
|
|
239
|
+
* retryable envelope" for all of them, and a blanket `isRetryable: true`
|
|
240
|
+
* would invite an agent into a loop it cannot win.
|
|
241
|
+
*/
|
|
242
|
+
function isolationFailure(error) {
|
|
243
|
+
// Not retryable, and for two different reasons. An OOM is the caller's
|
|
244
|
+
// input: `s("[bd*99999]*99999")` exhausts the cap every time it is
|
|
245
|
+
// sent. A spawn failure is the deployment: a missing or unbuilt child
|
|
246
|
+
// is missing on the next attempt too. Both would put an agent in a loop
|
|
247
|
+
// it cannot win, which is why the vote's "one retryable envelope for
|
|
248
|
+
// everything" is not what shipped.
|
|
249
|
+
if (error.kind === 'oom')
|
|
250
|
+
return err('validation', error.message);
|
|
251
|
+
if (error.kind === 'spawn')
|
|
252
|
+
return err('internal', error.message);
|
|
253
|
+
return err('transient', error.message, { isRetryable: true });
|
|
254
|
+
}
|
|
226
255
|
export async function execute(name, args, ctx) {
|
|
256
|
+
try {
|
|
257
|
+
return await run(name, args, ctx);
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
if (error instanceof IsolatedRunnerError)
|
|
261
|
+
return isolationFailure(error);
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async function run(name, args, ctx) {
|
|
227
266
|
const sid = args?.session_id;
|
|
228
267
|
if (!LOCAL_ENGINE_TOOLS.has(name) && !sid && !ctx.isInitialized()) {
|
|
229
268
|
return 'Browser not initialized. Run init first.';
|
|
@@ -232,11 +271,6 @@ export async function execute(name, args, ctx) {
|
|
|
232
271
|
switch (name) {
|
|
233
272
|
case 'analyze':
|
|
234
273
|
return await doAnalyze(args, controller);
|
|
235
|
-
// Deprecated aliases — forward to consolidated handler.
|
|
236
|
-
case 'analyze_spectrum': return await getSpectrum(controller);
|
|
237
|
-
case 'analyze_rhythm': return await getRhythm(controller);
|
|
238
|
-
case 'detect_tempo': return await getTempo(controller);
|
|
239
|
-
case 'detect_key': return await getKey(controller);
|
|
240
274
|
case 'validate_pattern_runtime': {
|
|
241
275
|
InputValidator.validateStringLength(args.pattern, 'pattern', 10000, false);
|
|
242
276
|
const validation = await controller.validatePatternRuntime(args.pattern, args.waitMs || 500);
|
|
@@ -248,7 +282,7 @@ export async function execute(name, args, ctx) {
|
|
|
248
282
|
}
|
|
249
283
|
case 'validate_pattern_local': {
|
|
250
284
|
InputValidator.validateStringLength(args.pattern, 'pattern', 10000, false);
|
|
251
|
-
const localValidation = ctx.strudelEngine.validate(args.pattern);
|
|
285
|
+
const localValidation = await ctx.strudelEngine.validate(args.pattern);
|
|
252
286
|
return {
|
|
253
287
|
valid: localValidation.valid,
|
|
254
288
|
errors: localValidation.errors,
|
|
@@ -262,7 +296,29 @@ export async function execute(name, args, ctx) {
|
|
|
262
296
|
}
|
|
263
297
|
case 'analyze_pattern_local': {
|
|
264
298
|
InputValidator.validateStringLength(args.pattern, 'pattern', 10000, false);
|
|
265
|
-
const patternMetadata = ctx.strudelEngine.analyzePattern(args.pattern);
|
|
299
|
+
const patternMetadata = await ctx.strudelEngine.analyzePattern(args.pattern);
|
|
300
|
+
// Do not report event counts the engine never measured. When
|
|
301
|
+
// evaluation failed, `eventsPerCycle: 0` is a default, not a
|
|
302
|
+
// finding — and it is indistinguishable from a genuinely silent
|
|
303
|
+
// pattern, so an agent was told working code produced nothing
|
|
304
|
+
// (#276). Say what happened instead.
|
|
305
|
+
if (!patternMetadata.evaluated) {
|
|
306
|
+
// Build explicitly rather than destructuring the unwanted fields
|
|
307
|
+
// out: naming them only to discard them trips no-unused-vars, and
|
|
308
|
+
// listing what IS reported is clearer about the contract anyway.
|
|
309
|
+
return {
|
|
310
|
+
usesSound: patternMetadata.usesSound,
|
|
311
|
+
usesNote: patternMetadata.usesNote,
|
|
312
|
+
isStack: patternMetadata.isStack,
|
|
313
|
+
functionsUsed: patternMetadata.functionsUsed,
|
|
314
|
+
complexity: patternMetadata.complexity,
|
|
315
|
+
...(patternMetadata.bpm !== undefined ? { bpm: patternMetadata.bpm } : {}),
|
|
316
|
+
evaluated: false,
|
|
317
|
+
message: 'Pattern could not be evaluated, so event counts are unavailable. ' +
|
|
318
|
+
'The static analysis below is still valid. ' +
|
|
319
|
+
(patternMetadata.evaluationError ?? ''),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
266
322
|
return {
|
|
267
323
|
...patternMetadata,
|
|
268
324
|
message: `Pattern analysis: ${patternMetadata.eventsPerCycle} events/cycle, ` +
|
|
@@ -275,14 +331,22 @@ export async function execute(name, args, ctx) {
|
|
|
275
331
|
const startCycle = args.start ?? 0;
|
|
276
332
|
const endCycle = args.end ?? 1;
|
|
277
333
|
if (startCycle >= endCycle) {
|
|
278
|
-
|
|
334
|
+
// Categorised explicitly rather than by whether the message
|
|
335
|
+
// happens to contain "must be" — which is the only reason this
|
|
336
|
+
// one landed in `validation` while the range check below landed
|
|
337
|
+
// in `internal` (#453).
|
|
338
|
+
return err('validation', 'Start must be less than end');
|
|
279
339
|
}
|
|
280
340
|
if (endCycle - startCycle > 16) {
|
|
281
|
-
return
|
|
341
|
+
return err('validation', 'Maximum range is 16 cycles to prevent excessive output');
|
|
282
342
|
}
|
|
283
343
|
try {
|
|
284
|
-
const events = ctx.strudelEngine.queryEvents(args.pattern, startCycle, endCycle);
|
|
285
|
-
|
|
344
|
+
const events = await ctx.strudelEngine.queryEvents(args.pattern, startCycle, endCycle);
|
|
345
|
+
// A silent pattern is a valid-empty query, not a failed one —
|
|
346
|
+
// and not the same as a pattern with events, which is what a
|
|
347
|
+
// plain ok() made it look like (#288).
|
|
348
|
+
const wrap = events.length === 0 ? empty : ok;
|
|
349
|
+
return wrap({
|
|
286
350
|
count: events.length,
|
|
287
351
|
range: { start: startCycle, end: endCycle },
|
|
288
352
|
events: events.map((e) => ({
|
|
@@ -291,9 +355,22 @@ export async function execute(name, args, ctx) {
|
|
|
291
355
|
end: e.end,
|
|
292
356
|
duration: e.end - e.start,
|
|
293
357
|
})),
|
|
294
|
-
};
|
|
358
|
+
});
|
|
295
359
|
}
|
|
296
360
|
catch (error) {
|
|
361
|
+
// The engine dying is not a syntax problem, and telling the
|
|
362
|
+
// caller to go run the validator would send them to a tool that
|
|
363
|
+
// will die the same way. Let it reach isolationFailure (#307).
|
|
364
|
+
if (error instanceof IsolatedRunnerError)
|
|
365
|
+
throw error;
|
|
366
|
+
// A refusal that already knows it is the caller's input keeps
|
|
367
|
+
// that verdict. Flattening it into this shape sent it through
|
|
368
|
+
// the dispatcher as a bare message, which categorised "Pattern
|
|
369
|
+
// produces roughly 2e10 events, above the 50000 cap" as
|
|
370
|
+
// `internal` — found by the protocol route check, after two
|
|
371
|
+
// rounds of fixing this exact class of bug (#382).
|
|
372
|
+
if (error instanceof ValidationError)
|
|
373
|
+
return err('validation', error.message);
|
|
297
374
|
const message = error instanceof Error ? error.message : String(error);
|
|
298
375
|
return {
|
|
299
376
|
error: message,
|
|
@@ -303,7 +380,7 @@ export async function execute(name, args, ctx) {
|
|
|
303
380
|
}
|
|
304
381
|
case 'transpile_pattern': {
|
|
305
382
|
InputValidator.validateStringLength(args.pattern, 'pattern', 10000, false);
|
|
306
|
-
const transpileResult = ctx.strudelEngine.transpile(args.pattern);
|
|
383
|
+
const transpileResult = await ctx.strudelEngine.transpile(args.pattern);
|
|
307
384
|
if (transpileResult.success) {
|
|
308
385
|
return {
|
|
309
386
|
success: true,
|
|
@@ -311,12 +388,14 @@ export async function execute(name, args, ctx) {
|
|
|
311
388
|
message: 'Pattern transpiled successfully',
|
|
312
389
|
};
|
|
313
390
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
391
|
+
// A syntax error in the caller's pattern is the caller's input,
|
|
392
|
+
// not an internal fault — and `Unexpected token (1:6)` matches
|
|
393
|
+
// nothing the string matcher looks for (#453). The location is
|
|
394
|
+
// kept in the message, since an envelope carries no extra fields.
|
|
395
|
+
return err('validation', `Transpilation failed: ${String(transpileResult.error)}`
|
|
396
|
+
+ (transpileResult.errorLocation !== undefined
|
|
397
|
+
? ` (at ${String(transpileResult.errorLocation)})`
|
|
398
|
+
: ''));
|
|
320
399
|
}
|
|
321
400
|
default:
|
|
322
401
|
throw new Error(`analysis module does not handle tool: ${name}`);
|