@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,8 +1,18 @@
|
|
|
1
1
|
import { Logger } from '../utils/Logger.js';
|
|
2
|
+
import { AiAuthError, AiRateLimitError } from './ai/AiTransport.js';
|
|
3
|
+
import { TransientError } from '../utils/CategorisedError.js';
|
|
2
4
|
import { GoogleAuth } from 'google-auth-library';
|
|
5
|
+
import { cliTransports, hasCliTransport } from './ai/CliTransport.js';
|
|
6
|
+
/**
|
|
7
|
+
* Cheapest possible request that still proves a transport can answer.
|
|
8
|
+
* Being on PATH says nothing about being logged in (#252 review).
|
|
9
|
+
*/
|
|
10
|
+
const TRANSPORT_PROBE_PROMPT = 'Reply with exactly: OK';
|
|
11
|
+
import { buildMeasurementPrompt } from './ai/AudioMeasurements.js';
|
|
3
12
|
import * as os from 'os';
|
|
4
13
|
import * as path from 'path';
|
|
5
14
|
import * as fs from 'fs/promises';
|
|
15
|
+
import { ValidationError } from '../utils/CategorisedError.js';
|
|
6
16
|
/**
|
|
7
17
|
* GeminiService provides AI-powered music analysis and suggestions
|
|
8
18
|
* using Google's Gemini API.
|
|
@@ -25,6 +35,12 @@ export class GeminiService {
|
|
|
25
35
|
cacheTtlMs;
|
|
26
36
|
timeoutMs;
|
|
27
37
|
maxPatternLength;
|
|
38
|
+
/** Whether shelling out to a local CLI is permitted. */
|
|
39
|
+
enableCliTransport;
|
|
40
|
+
/** Resolved transport; undefined = not yet resolved, null = none available. */
|
|
41
|
+
transport = undefined;
|
|
42
|
+
/** In-flight transport resolution, so concurrent callers share one probe. */
|
|
43
|
+
transportPromise = null;
|
|
28
44
|
adcAvailable = null; // null = not checked yet
|
|
29
45
|
adcCheckPromise = null;
|
|
30
46
|
cliCredentialsChecked = false;
|
|
@@ -47,20 +63,40 @@ export class GeminiService {
|
|
|
47
63
|
this.cacheTtlMs = (config?.cacheTtlSeconds || 300) * 1000;
|
|
48
64
|
this.timeoutMs = (config?.timeoutSeconds || 30) * 1000;
|
|
49
65
|
this.maxPatternLength = config?.maxPatternLength || 5000;
|
|
66
|
+
this.enableCliTransport =
|
|
67
|
+
config?.enableCliTransport ?? process.env.STRUDEL_DISABLE_CLI_AI !== '1';
|
|
50
68
|
}
|
|
51
69
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
70
|
+
* Whether AI features can currently be served.
|
|
71
|
+
*
|
|
72
|
+
* Synchronous, so it can only report what is already known: an API key,
|
|
73
|
+
* a completed ADC probe, or a transport resolved by an earlier call.
|
|
74
|
+
* It deliberately does NOT scan for CLIs — that is I/O.
|
|
75
|
+
*
|
|
76
|
+
* Gates call this, so it has to be complete without being async. It
|
|
77
|
+
* covers an API key, a finished ADC probe, an already-resolved
|
|
78
|
+
* transport, and an installed CLI. What it CANNOT see synchronously is
|
|
79
|
+
* whether that CLI is logged in — `resolveTransport()` probes for that
|
|
80
|
+
* and falls through to the next candidate, so a gate saying "available"
|
|
81
|
+
* may still be followed by a transport error naming the reason.
|
|
55
82
|
*/
|
|
56
83
|
isAvailable() {
|
|
57
|
-
return !!this.apiKey ||
|
|
84
|
+
return (!!this.apiKey ||
|
|
85
|
+
this.adcAvailable === true ||
|
|
86
|
+
this.transport != null ||
|
|
87
|
+
// A locally authenticated CLI serves requests perfectly well without
|
|
88
|
+
// any key of ours (#252). Detecting one is a PATH scan, not I/O
|
|
89
|
+
// against a service, so this stays synchronous.
|
|
90
|
+
(this.enableCliTransport && hasCliTransport()));
|
|
58
91
|
}
|
|
59
92
|
/**
|
|
60
93
|
* Async check for Gemini service availability.
|
|
61
94
|
* Checks in order: API key, env var, Gemini CLI config, ADC.
|
|
62
95
|
* @returns Promise<boolean> indicating if service can make API calls
|
|
63
96
|
*/
|
|
97
|
+
async canServe() {
|
|
98
|
+
return (await this.resolveTransport()) !== null;
|
|
99
|
+
}
|
|
64
100
|
async isAvailableAsync() {
|
|
65
101
|
// 1. Explicit API key or env var already set
|
|
66
102
|
if (this.apiKey)
|
|
@@ -225,10 +261,10 @@ export class GeminiService {
|
|
|
225
261
|
* Get authentication error message
|
|
226
262
|
*/
|
|
227
263
|
getAuthErrorMessage() {
|
|
228
|
-
return '
|
|
264
|
+
return ('No AI transport available. Options:\n' +
|
|
229
265
|
'1. Set GEMINI_API_KEY environment variable\n' +
|
|
230
|
-
'2.
|
|
231
|
-
'3.
|
|
266
|
+
'2. Run "gcloud auth application-default login" for ADC\n' +
|
|
267
|
+
'3. Install and authenticate one of: claude, agy, codex');
|
|
232
268
|
}
|
|
233
269
|
/**
|
|
234
270
|
* Ensure authentication is available (API key, Gemini CLI, or ADC)
|
|
@@ -236,18 +272,52 @@ export class GeminiService {
|
|
|
236
272
|
* @throws {Error} When no authentication method is available
|
|
237
273
|
*/
|
|
238
274
|
async ensureAuthentication() {
|
|
239
|
-
//
|
|
240
|
-
|
|
275
|
+
// A resolved transport IS authentication. An authenticated CLI needs
|
|
276
|
+
// no API key of ours — it holds its own credentials, which is the
|
|
277
|
+
// entire point of #252. Checking the key ladder first would reject a
|
|
278
|
+
// machine where AI works perfectly well.
|
|
279
|
+
if ((await this.resolveTransport()) !== null)
|
|
241
280
|
return;
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
281
|
+
// Typed, not phrased. The message lists GEMINI_API_KEY and gcloud
|
|
282
|
+
// commands, and used to be categorised `permission` only because
|
|
283
|
+
// `gemini` was in the matcher's auth list — which also made every
|
|
284
|
+
// unrelated Gemini failure a credentials problem (#382).
|
|
285
|
+
throw new AiAuthError(this.getAuthErrorMessage());
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Analyses audio from locally computed measurements.
|
|
289
|
+
*
|
|
290
|
+
* Preferred over {@link analyzeAudio}. No installed CLI can decode
|
|
291
|
+
* audio — asked directly, they say so — and `agy` will sometimes
|
|
292
|
+
* confabulate detailed analysis of audio it never examined, which is
|
|
293
|
+
* worse than refusing. Measuring locally and sending numbers is
|
|
294
|
+
* deterministic and works with every provider rather than one.
|
|
295
|
+
*
|
|
296
|
+
* @param measurements - Peak, RMS, tempo, key and spectrum measured locally
|
|
297
|
+
* @param pattern - The Strudel source that produced the audio
|
|
298
|
+
* @param context - What the user was aiming for
|
|
299
|
+
* @returns Structured feedback in the same shape as analyzeAudio
|
|
300
|
+
*
|
|
301
|
+
* @example
|
|
302
|
+
* await service.analyzeAudioMeasurements(
|
|
303
|
+
* { durationMs: 5000, peak: 1.12, rms: 0.36 },
|
|
304
|
+
* 'stack(s("bd*4"), s("~ cp"))',
|
|
305
|
+
* );
|
|
306
|
+
*/
|
|
307
|
+
async analyzeAudioMeasurements(measurements, pattern, context) {
|
|
308
|
+
await this.ensureAuthentication();
|
|
309
|
+
this.checkRateLimit();
|
|
310
|
+
const cacheKey = `measure-${await this.getTransportId() ?? 'none'}-${JSON.stringify(measurements)}-${(pattern ?? '').slice(0, 200)}`;
|
|
311
|
+
const cached = this.getFromCache(this.audioCache, cacheKey);
|
|
312
|
+
if (cached) {
|
|
313
|
+
this.logger.debug('Returning cached measurement analysis');
|
|
314
|
+
return cached;
|
|
250
315
|
}
|
|
316
|
+
const prompt = buildMeasurementPrompt(measurements, pattern, context);
|
|
317
|
+
const response = await this.callGeminiAPIWithTimeout(prompt);
|
|
318
|
+
const feedback = this.parseAudioResponse(response);
|
|
319
|
+
this.audioCache.set(cacheKey, { result: feedback, timestamp: Date.now() });
|
|
320
|
+
return feedback;
|
|
251
321
|
}
|
|
252
322
|
/**
|
|
253
323
|
* Analyzes audio data and provides qualitative feedback
|
|
@@ -276,15 +346,31 @@ export class GeminiService {
|
|
|
276
346
|
const prompt = this.buildAudioPrompt(context);
|
|
277
347
|
// Use timeout for audio analysis (may take longer due to media processing)
|
|
278
348
|
const audioTimeoutMs = this.timeoutMs * 2; // Double timeout for audio
|
|
349
|
+
let audioTimer;
|
|
279
350
|
const timeoutPromise = new Promise((_, reject) => {
|
|
280
|
-
setTimeout(() => {
|
|
351
|
+
audioTimer = setTimeout(() => {
|
|
281
352
|
reject(new Error(`Audio analysis timed out after ${audioTimeoutMs / 1000} seconds`));
|
|
282
353
|
}, audioTimeoutMs);
|
|
283
354
|
});
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
355
|
+
let response;
|
|
356
|
+
try {
|
|
357
|
+
response = await Promise.race([
|
|
358
|
+
// Derive from the blob rather than hardcoding: the feedback path
|
|
359
|
+
// now sends WAV (decoded PCM), and telling Gemini it is WebM makes
|
|
360
|
+
// it decode the wrong container.
|
|
361
|
+
this.callGeminiAPI(prompt, audioBase64, audioData.type || 'audio/webm'),
|
|
362
|
+
timeoutPromise
|
|
363
|
+
]);
|
|
364
|
+
}
|
|
365
|
+
finally {
|
|
366
|
+
// The race settling does not disarm the loser. Without this the
|
|
367
|
+
// handle and its closure live for the full 60s even when the API
|
|
368
|
+
// answered at once — eight of them outlived one test file, and in
|
|
369
|
+
// a long-lived server they accumulate (#404). The sibling call
|
|
370
|
+
// site at `callWithTimeout` already did this; this one did not.
|
|
371
|
+
if (audioTimer !== undefined)
|
|
372
|
+
clearTimeout(audioTimer);
|
|
373
|
+
}
|
|
288
374
|
const feedback = this.parseAudioResponse(response);
|
|
289
375
|
this.audioCache.set(cacheKey, { result: feedback, timestamp: Date.now() });
|
|
290
376
|
return feedback;
|
|
@@ -295,7 +381,7 @@ export class GeminiService {
|
|
|
295
381
|
if (error.message?.includes('timed out')) {
|
|
296
382
|
throw new Error('Audio analysis timed out. The audio sample may be too long. Try a shorter recording.');
|
|
297
383
|
}
|
|
298
|
-
if (error
|
|
384
|
+
if (error instanceof AiRateLimitError) {
|
|
299
385
|
throw error; // Re-throw rate limit errors as-is
|
|
300
386
|
}
|
|
301
387
|
throw new Error(`Audio analysis failed: ${error.message}`);
|
|
@@ -324,7 +410,7 @@ export class GeminiService {
|
|
|
324
410
|
if (error.message?.includes('timed out')) {
|
|
325
411
|
throw new Error('Variation suggestion timed out. The pattern may be too complex. Try a simpler pattern.');
|
|
326
412
|
}
|
|
327
|
-
if (error
|
|
413
|
+
if (error instanceof AiRateLimitError) {
|
|
328
414
|
throw error; // Re-throw rate limit errors as-is
|
|
329
415
|
}
|
|
330
416
|
throw new Error(`Variation suggestion failed: ${error.message}`);
|
|
@@ -361,7 +447,7 @@ export class GeminiService {
|
|
|
361
447
|
if (error.message?.includes('timed out')) {
|
|
362
448
|
throw new Error('Creative feedback timed out. The pattern may be too complex. Try a simpler pattern.');
|
|
363
449
|
}
|
|
364
|
-
if (error
|
|
450
|
+
if (error instanceof AiRateLimitError) {
|
|
365
451
|
throw error; // Re-throw rate limit errors as-is
|
|
366
452
|
}
|
|
367
453
|
throw new Error(`Creative feedback failed: ${error.message}`);
|
|
@@ -370,6 +456,16 @@ export class GeminiService {
|
|
|
370
456
|
/**
|
|
371
457
|
* Clears all cached analysis results
|
|
372
458
|
*/
|
|
459
|
+
/**
|
|
460
|
+
* Forget the resolved transport so the next call re-probes.
|
|
461
|
+
*
|
|
462
|
+
* A long-lived server that started with no credentials could otherwise
|
|
463
|
+
* never recover: `resolveTransport()` caches `null` and logging into a
|
|
464
|
+
* CLI afterwards would require a restart.
|
|
465
|
+
*/
|
|
466
|
+
resetTransport() {
|
|
467
|
+
this.transport = undefined;
|
|
468
|
+
}
|
|
373
469
|
clearCache() {
|
|
374
470
|
this.audioCache.clear();
|
|
375
471
|
this.patternCache.clear();
|
|
@@ -392,7 +488,12 @@ export class GeminiService {
|
|
|
392
488
|
const waitTimeMs = this.rateLimit.lastResetTime + this.rateLimitWindowMs - now;
|
|
393
489
|
const waitTimeSec = Math.ceil(waitTimeMs / 1000);
|
|
394
490
|
this.rateLimit.nextAvailableTime = now + waitTimeMs;
|
|
395
|
-
|
|
491
|
+
// A typed error, not a phrase. Three call sites and
|
|
492
|
+
// `categorizeError` all used to recognise this by matching "rate
|
|
493
|
+
// limit" in the message, so rewording it would have silently
|
|
494
|
+
// recategorised a rate limit as an internal failure that is not
|
|
495
|
+
// worth retrying — which is the opposite of the truth (#380).
|
|
496
|
+
throw new AiRateLimitError(`Rate limit exceeded (${this.maxRequestsPerMinute} requests/minute). ` +
|
|
396
497
|
`Wait ${waitTimeSec} seconds before retrying.`);
|
|
397
498
|
}
|
|
398
499
|
this.rateLimit.requestCount++;
|
|
@@ -445,7 +546,7 @@ export class GeminiService {
|
|
|
445
546
|
}
|
|
446
547
|
// Check for excessive length
|
|
447
548
|
if (trimmed.length > this.maxPatternLength) {
|
|
448
|
-
throw new
|
|
549
|
+
throw new ValidationError(`${methodName}: Pattern exceeds maximum length of ${this.maxPatternLength} characters ` +
|
|
449
550
|
`(current: ${trimmed.length}). Truncate or simplify the pattern.`);
|
|
450
551
|
}
|
|
451
552
|
return trimmed;
|
|
@@ -456,16 +557,100 @@ export class GeminiService {
|
|
|
456
557
|
* @returns API response text
|
|
457
558
|
* @throws {Error} When request times out or API fails
|
|
458
559
|
*/
|
|
560
|
+
/**
|
|
561
|
+
* Resolves the transport that will actually serve requests.
|
|
562
|
+
*
|
|
563
|
+
* The Gemini HTTP API when credentials exist, otherwise the first
|
|
564
|
+
* locally authenticated CLI. Cached after the first resolution so a
|
|
565
|
+
* PATH scan and auth probe do not run per request.
|
|
566
|
+
*
|
|
567
|
+
* @returns The active transport, or null when nothing can serve
|
|
568
|
+
*/
|
|
569
|
+
async resolveTransport() {
|
|
570
|
+
if (this.transport !== undefined)
|
|
571
|
+
return this.transport;
|
|
572
|
+
// Single-flight, matching checkADC() above. Without it two concurrent
|
|
573
|
+
// ai_assist calls each ran the full PATH scan and each SPAWNED A
|
|
574
|
+
// PROBE SUBPROCESS per candidate CLI — real quota, burned twice —
|
|
575
|
+
// before the last writer won the assignment anyway (#265).
|
|
576
|
+
this.transportPromise ??= this.doResolveTransport()
|
|
577
|
+
.finally(() => { this.transportPromise = null; });
|
|
578
|
+
return this.transportPromise;
|
|
579
|
+
}
|
|
580
|
+
async doResolveTransport() {
|
|
581
|
+
if (this.transport !== undefined)
|
|
582
|
+
return this.transport;
|
|
583
|
+
if (await this.isAvailableAsync()) {
|
|
584
|
+
this.transport = {
|
|
585
|
+
id: `gemini-api:${this.model}`,
|
|
586
|
+
label: 'Gemini API',
|
|
587
|
+
isAvailable: () => Promise.resolve(true),
|
|
588
|
+
send: (prompt) => this.callGeminiAPI(prompt),
|
|
589
|
+
};
|
|
590
|
+
return this.transport;
|
|
591
|
+
}
|
|
592
|
+
// No API credentials. A locally authenticated CLI still works, which
|
|
593
|
+
// is the whole of #252: `agy` holds valid Gemini auth that this
|
|
594
|
+
// service's credential ladder cannot see.
|
|
595
|
+
//
|
|
596
|
+
// Being on PATH is not the same as being usable. An installed but
|
|
597
|
+
// unauthenticated `claude` would otherwise win — it is first in the
|
|
598
|
+
// preference order — and every request would fail without `agy` ever
|
|
599
|
+
// being tried, on exactly the machine this feature exists to serve.
|
|
600
|
+
// So probe each candidate and fall through when it cannot answer.
|
|
601
|
+
for (const candidate of this.enableCliTransport ? cliTransports() : []) {
|
|
602
|
+
if (!(await candidate.isAvailable()))
|
|
603
|
+
continue;
|
|
604
|
+
try {
|
|
605
|
+
await candidate.send(TRANSPORT_PROBE_PROMPT);
|
|
606
|
+
}
|
|
607
|
+
catch (error) {
|
|
608
|
+
this.logger.debug(`Transport ${candidate.label} unusable: ${error instanceof Error ? error.message : String(error)}`);
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
this.logger.debug(`Using AI transport: ${candidate.label}`);
|
|
612
|
+
this.transport = candidate;
|
|
613
|
+
return this.transport;
|
|
614
|
+
}
|
|
615
|
+
this.transport = null;
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
/** Identifier of the transport in use, for diagnostics. */
|
|
619
|
+
async getTransportId() {
|
|
620
|
+
return (await this.resolveTransport())?.id ?? null;
|
|
621
|
+
}
|
|
459
622
|
async callGeminiAPIWithTimeout(prompt) {
|
|
623
|
+
const transport = await this.resolveTransport();
|
|
624
|
+
if (transport === null) {
|
|
625
|
+
// Typed, not phrased. The message lists GEMINI_API_KEY and gcloud
|
|
626
|
+
// commands, and used to be categorised `permission` only because
|
|
627
|
+
// `gemini` was in the matcher's auth list — which also made every
|
|
628
|
+
// unrelated Gemini failure a credentials problem (#382).
|
|
629
|
+
throw new AiAuthError(this.getAuthErrorMessage());
|
|
630
|
+
}
|
|
631
|
+
// A CLI transport enforces its own timeout and can kill the process
|
|
632
|
+
// tree; racing it against a shorter one here would leave the loser
|
|
633
|
+
// running. The caller would see "timed out" while an agentic CLI kept
|
|
634
|
+
// working for another 90 seconds, burning quota, and repeated
|
|
635
|
+
// timeouts would stack live subprocesses.
|
|
636
|
+
if (transport.id.startsWith('cli:')) {
|
|
637
|
+
return transport.send(prompt);
|
|
638
|
+
}
|
|
639
|
+
let timer;
|
|
460
640
|
const timeoutPromise = new Promise((_, reject) => {
|
|
461
|
-
setTimeout(() => {
|
|
641
|
+
timer = setTimeout(() => {
|
|
462
642
|
reject(new Error(`Request timed out after ${this.timeoutMs / 1000} seconds`));
|
|
463
643
|
}, this.timeoutMs);
|
|
464
644
|
});
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
645
|
+
try {
|
|
646
|
+
return await Promise.race([transport.send(prompt), timeoutPromise]);
|
|
647
|
+
}
|
|
648
|
+
finally {
|
|
649
|
+
// Otherwise the handle and its closure are held until the deadline
|
|
650
|
+
// even when the request succeeded immediately.
|
|
651
|
+
if (timer !== undefined)
|
|
652
|
+
clearTimeout(timer);
|
|
653
|
+
}
|
|
469
654
|
}
|
|
470
655
|
async callGeminiAPI(prompt, mediaData, mimeType) {
|
|
471
656
|
// Dynamic import for ES module compatibility
|
|
@@ -600,7 +785,7 @@ Consider:
|
|
|
600
785
|
try {
|
|
601
786
|
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
602
787
|
if (!jsonMatch)
|
|
603
|
-
throw new
|
|
788
|
+
throw new TransientError('No JSON found in response');
|
|
604
789
|
const parsed = JSON.parse(jsonMatch[0]);
|
|
605
790
|
return {
|
|
606
791
|
mood: parsed.mood || 'unknown',
|
|
@@ -625,7 +810,7 @@ Consider:
|
|
|
625
810
|
try {
|
|
626
811
|
const jsonMatch = response.match(/\[[\s\S]*\]/);
|
|
627
812
|
if (!jsonMatch)
|
|
628
|
-
throw new
|
|
813
|
+
throw new TransientError('No JSON array found in response');
|
|
629
814
|
const parsed = JSON.parse(jsonMatch[0]);
|
|
630
815
|
return parsed.map((item) => ({
|
|
631
816
|
description: item.description || '',
|
|
@@ -642,7 +827,7 @@ Consider:
|
|
|
642
827
|
try {
|
|
643
828
|
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
644
829
|
if (!jsonMatch)
|
|
645
|
-
throw new
|
|
830
|
+
throw new TransientError('No JSON found in response');
|
|
646
831
|
const parsed = JSON.parse(jsonMatch[0]);
|
|
647
832
|
return {
|
|
648
833
|
complexity: parsed.complexity || 'moderate',
|
|
@@ -705,7 +890,7 @@ Consider:
|
|
|
705
890
|
if (error.message?.includes('timed out')) {
|
|
706
891
|
throw new Error('Pattern modification timed out. The pattern may be too complex. Try a simpler pattern.');
|
|
707
892
|
}
|
|
708
|
-
if (error
|
|
893
|
+
if (error instanceof AiRateLimitError) {
|
|
709
894
|
throw error; // Re-throw rate limit errors as-is
|
|
710
895
|
}
|
|
711
896
|
throw new Error(`Pattern modification failed: ${error.message}`);
|