@l10nmonster/mcp 3.0.0-alpha.16

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.
@@ -0,0 +1,665 @@
1
+ import { z } from 'zod';
2
+ import { McpTool, McpNotFoundError } from './mcpTool.js';
3
+
4
+ /**
5
+ * Create a language pair key in the format "sourceLang→targetLang"
6
+ * @param {string} sourceLang Source language code
7
+ * @param {string} targetLang Target language code
8
+ * @returns {string} Formatted language pair key
9
+ */
10
+ function makeLangPairKey(sourceLang, targetLang) {
11
+ return `${sourceLang}→${targetLang}`;
12
+ }
13
+
14
+ /**
15
+ * Get channel statistics and warm caches.
16
+ * @param {*} mm MonsterManager instance
17
+ * @param {Object} filters Filter object with optional channel property
18
+ * @returns {Promise<{channelStats: Object, channelIds: string[]}>}
19
+ */
20
+ async function getChannelStats(mm, filters) {
21
+ const channelIds = filters.channel ?
22
+ mm.rm.channelIds.filter(id => id === filters.channel) :
23
+ mm.rm.channelIds;
24
+
25
+ if (filters.channel && channelIds.length === 0) {
26
+ throw new McpNotFoundError(`Channel "${filters.channel}" not found`, {
27
+ hints: [`Available channels: ${mm.rm.channelIds.join(', ')}`]
28
+ });
29
+ }
30
+
31
+ // Warm caches by calling methods that trigger initialization
32
+ const channelStats = {};
33
+ for (const channelId of channelIds) {
34
+ // This call warms the cache
35
+ channelStats[channelId] = await mm.rm.getActiveContentStats(channelId);
36
+ }
37
+
38
+ return { channelStats, channelIds };
39
+ }
40
+
41
+ /**
42
+ * Get desired language pairs from channels.
43
+ * @param {*} mm MonsterManager instance
44
+ * @param {string[]} channelIds List of channel IDs to process
45
+ * @param {Object} filters Filter object with optional sourceLang and targetLang properties
46
+ * @returns {Promise<{desiredPairs: Object}>}
47
+ */
48
+ async function getDesiredLangPairs(mm, channelIds, filters) {
49
+ const desiredPairs = {};
50
+ const allLangPairs = new Set();
51
+
52
+ for (const channelId of channelIds) {
53
+ const channelLangPairs = await mm.rm.getDesiredLangPairs(channelId);
54
+ for (const [sourceLang, targetLang] of channelLangPairs) {
55
+ // Apply language filters
56
+ if (filters.sourceLang && sourceLang !== filters.sourceLang) continue;
57
+ if (filters.targetLang && targetLang !== filters.targetLang) continue;
58
+
59
+ const pairKey = makeLangPairKey(sourceLang, targetLang);
60
+ if (!allLangPairs.has(pairKey)) {
61
+ allLangPairs.add(pairKey);
62
+ desiredPairs[sourceLang] ??= [];
63
+ desiredPairs[sourceLang].push(targetLang);
64
+ }
65
+ }
66
+ }
67
+
68
+ return { desiredPairs };
69
+ }
70
+
71
+ /**
72
+ * Get available translation providers with their status.
73
+ * @param {*} mm MonsterManager instance
74
+ * @param {Object} filters Filter object with optional provider property
75
+ * @returns {Promise<Array>} List of provider objects
76
+ */
77
+ async function getAvailableProviders(mm, filters) {
78
+ const providers = [];
79
+ for (const provider of mm.dispatcher.providers) {
80
+ // Apply provider filter
81
+ if (filters.provider && provider.id !== filters.provider) continue;
82
+
83
+ try {
84
+ const info = await provider.info();
85
+ providers.push({
86
+ id: info.id,
87
+ type: info.type,
88
+ quality: info.quality ?? 'dynamic',
89
+ costPerWord: info.costPerWord ?? 0,
90
+ costPerMChar: info.costPerMChar ?? 0,
91
+ supportedPairs: info.supportedPairs ?? 'any',
92
+ available: true
93
+ });
94
+ } catch (error) {
95
+ providers.push({
96
+ id: provider.id,
97
+ available: false,
98
+ error: error.message
99
+ });
100
+ }
101
+ }
102
+
103
+ if (filters.provider && providers.length === 0) {
104
+ throw new McpNotFoundError(`Provider "${filters.provider}" not found`, {
105
+ hints: [`Available providers: ${mm.dispatcher.providers.map(p => p.id).join(', ')}`]
106
+ });
107
+ }
108
+
109
+ return providers;
110
+ }
111
+
112
+ /**
113
+ * Get supported language pairs from providers.
114
+ * @param {Array} providers List of provider objects
115
+ * @param {Object} filters Filter object with optional sourceLang and targetLang properties
116
+ * @returns {Object} Supported language pairs by source language
117
+ */
118
+ function getSupportedLangPairs(providers, filters) {
119
+ const supportedPairs = {};
120
+ const allSupportedPairs = new Set();
121
+
122
+ if (!Array.isArray(providers)) {
123
+ return supportedPairs;
124
+ }
125
+
126
+ for (const provider of providers) {
127
+ if (!provider.available || !provider.supportedPairs) continue;
128
+
129
+ // Handle different formats of supportedPairs
130
+ if (provider.supportedPairs === 'any') {
131
+ // Provider supports any language pair, we can't enumerate them
132
+ continue;
133
+ }
134
+
135
+ if (Array.isArray(provider.supportedPairs)) {
136
+ // Format: [["en", "es"], ["en", "fr"], ...]
137
+ for (const [sourceLang, targetLang] of provider.supportedPairs) {
138
+ // Apply language filters
139
+ if (filters.sourceLang && sourceLang !== filters.sourceLang) continue;
140
+ if (filters.targetLang && targetLang !== filters.targetLang) continue;
141
+
142
+ const pairKey = makeLangPairKey(sourceLang, targetLang);
143
+ if (!allSupportedPairs.has(pairKey)) {
144
+ allSupportedPairs.add(pairKey);
145
+ supportedPairs[sourceLang] ??= [];
146
+ if (!supportedPairs[sourceLang].includes(targetLang)) {
147
+ supportedPairs[sourceLang].push(targetLang);
148
+ }
149
+ }
150
+ }
151
+ }
152
+ }
153
+
154
+ return supportedPairs;
155
+ }
156
+
157
+ /**
158
+ * Get translation memory statistics for available language pairs.
159
+ * @param {*} mm MonsterManager instance
160
+ * @param {Object} filters Filter object with optional provider, sourceLang, and targetLang properties
161
+ * @returns {Promise<{tmStats: Object, availableLangPairs: Array}>}
162
+ */
163
+ async function getTranslationMemoryStats(mm, filters) {
164
+ const tmStats = {};
165
+ const allAvailableLangPairs = await mm.tmm.getAvailableLangPairs();
166
+
167
+ // Filter language pairs
168
+ const availableLangPairs = allAvailableLangPairs.filter(([sourceLang, targetLang]) => {
169
+ if (filters.sourceLang && sourceLang !== filters.sourceLang) return false;
170
+ if (filters.targetLang && targetLang !== filters.targetLang) return false;
171
+ return true;
172
+ }).sort();
173
+
174
+ for (const [sourceLang, targetLang] of availableLangPairs) {
175
+ const tm = mm.tmm.getTM(sourceLang, targetLang);
176
+ const stats = await tm.getStats();
177
+ const pairKey = makeLangPairKey(sourceLang, targetLang);
178
+
179
+ // Apply provider filter to TM stats
180
+ const filteredStats = filters.provider ?
181
+ stats.filter(s => s.translationProvider === filters.provider) :
182
+ stats;
183
+
184
+ tmStats[pairKey] = filteredStats.map(s => ({
185
+ translationProvider: s.translationProvider,
186
+ status: s.status,
187
+ jobCount: s.jobCount,
188
+ tuCount: s.tuCount,
189
+ redundancy: s.distinctGuids > 0 ? (s.tuCount / s.distinctGuids - 1) : 0
190
+ }));
191
+ }
192
+
193
+ return { tmStats, availableLangPairs };
194
+ }
195
+
196
+ /**
197
+ * Get translation status filtered by channel and language.
198
+ * @param {*} mm MonsterManager instance
199
+ * @param {Object} filters Filter object with optional channel, sourceLang, and targetLang properties
200
+ * @returns {Promise<{translationStatus: Object}>}
201
+ */
202
+ async function getTranslationStatus(mm, filters) {
203
+ // Structure: channelId -> sourceLang -> targetLang -> prj -> { details, pairSummary, pairSummaryByStatus }
204
+ const translationStatusRaw = await mm.getTranslationStatus();
205
+
206
+ // Transform to sourceLang -> targetLang -> channelId -> prj structure for easier processing
207
+ // Apply filters during transformation
208
+ const translationStatus = {};
209
+ for (const [channelId, channelData] of Object.entries(translationStatusRaw)) {
210
+ // Apply channel filter
211
+ if (filters.channel && channelId !== filters.channel) continue;
212
+
213
+ for (const [sourceLang, sourceData] of Object.entries(channelData)) {
214
+ // Apply source language filter
215
+ if (filters.sourceLang && sourceLang !== filters.sourceLang) continue;
216
+
217
+ translationStatus[sourceLang] ??= {};
218
+ for (const [targetLang, targetData] of Object.entries(sourceData)) {
219
+ // Apply target language filter
220
+ if (filters.targetLang && targetLang !== filters.targetLang) continue;
221
+
222
+ translationStatus[sourceLang][targetLang] ??= {};
223
+ translationStatus[sourceLang][targetLang][channelId] = targetData;
224
+ }
225
+ }
226
+ }
227
+
228
+ return { translationStatus };
229
+ }
230
+
231
+ /**
232
+ * Compute coverage metrics from translation status.
233
+ * @param {Object} translationStatus Filtered translation status structure
234
+ * @returns {Object} Coverage metrics including overall and by-pair breakdowns
235
+ */
236
+ function computeCoverage(translationStatus) {
237
+ let totalSegments = 0;
238
+ let translatedSegments = 0;
239
+ let untranslatedSegments = 0;
240
+ let inFlightSegments = 0;
241
+ let lowQualitySegments = 0;
242
+
243
+ const coverageByPair = {};
244
+
245
+ for (const [sourceLang, sourceStatus] of Object.entries(translationStatus)) {
246
+ for (const [targetLang, channelStatus] of Object.entries(sourceStatus)) {
247
+ let pairSegments = 0;
248
+ let pairTranslated = 0;
249
+ let pairUntranslated = 0;
250
+ let pairInFlight = 0;
251
+ let pairLowQuality = 0;
252
+
253
+ for (const projectStatus of Object.values(channelStatus)) {
254
+ for (const { pairSummary, pairSummaryByStatus } of Object.values(projectStatus)) {
255
+ pairSegments += pairSummary.segs;
256
+ pairTranslated += pairSummaryByStatus.translated || 0;
257
+ pairUntranslated += pairSummaryByStatus.untranslated || 0;
258
+ pairInFlight += pairSummaryByStatus['in flight'] || 0;
259
+ pairLowQuality += pairSummaryByStatus['low quality'] || 0;
260
+ }
261
+ }
262
+
263
+ totalSegments += pairSegments;
264
+ translatedSegments += pairTranslated;
265
+ untranslatedSegments += pairUntranslated;
266
+ inFlightSegments += pairInFlight;
267
+ lowQualitySegments += pairLowQuality;
268
+
269
+ const coverage = pairSegments > 0 ? pairTranslated / pairSegments : 0;
270
+ const pairKey = makeLangPairKey(sourceLang, targetLang);
271
+ coverageByPair[pairKey] = {
272
+ total: pairSegments,
273
+ translated: pairTranslated,
274
+ untranslated: pairUntranslated,
275
+ inFlight: pairInFlight,
276
+ lowQuality: pairLowQuality,
277
+ coverage
278
+ };
279
+ }
280
+ }
281
+
282
+ const overallCoverage = totalSegments > 0 ? translatedSegments / totalSegments : 0;
283
+
284
+ return {
285
+ overall: {
286
+ total: totalSegments,
287
+ translated: translatedSegments,
288
+ untranslated: untranslatedSegments,
289
+ inFlight: inFlightSegments,
290
+ lowQuality: lowQualitySegments,
291
+ coverage: overallCoverage
292
+ },
293
+ byPair: coverageByPair,
294
+ };
295
+ }
296
+
297
+ /**
298
+ * Get job summaries including pending and recent jobs.
299
+ * @param {*} mm MonsterManager instance
300
+ * @param {Object} filters Filter object with optional provider property
301
+ * @param {Array} availableLangPairs List of [sourceLang, targetLang] pairs
302
+ * @returns {Promise<{pendingJobs: Array, recentJobs: Array, jobCountsByPair: Object}>}
303
+ */
304
+ async function getJobSummaries(mm, filters, availableLangPairs) {
305
+ const pendingJobs = [];
306
+ const recentJobs = [];
307
+ const jobCountsByPair = {};
308
+
309
+ for (const [sourceLang, targetLang] of availableLangPairs) {
310
+ const allJobs = await mm.tmm.getJobTOCByLangPair(sourceLang, targetLang);
311
+
312
+ // Apply provider filter to jobs
313
+ const filteredJobs = filters.provider ?
314
+ allJobs.filter(job => job.translationProvider === filters.provider) :
315
+ allJobs;
316
+
317
+ const unfinished = filteredJobs.filter(job => job.status !== 'done');
318
+ const recent = filteredJobs
319
+ .filter(job => {
320
+ const updatedAt = new Date(job.updatedAt);
321
+ const weekAgo = new Date();
322
+ weekAgo.setDate(weekAgo.getDate() - 7);
323
+ return updatedAt >= weekAgo;
324
+ })
325
+ .slice(0, 10);
326
+
327
+ const pairKey = makeLangPairKey(sourceLang, targetLang);
328
+ jobCountsByPair[pairKey] = {
329
+ total: filteredJobs.length,
330
+ unfinished: unfinished.length,
331
+ recent: recent.length
332
+ };
333
+
334
+ if (unfinished.length > 0) {
335
+ pendingJobs.push(...unfinished.map(job => ({
336
+ pair: pairKey,
337
+ jobGuid: job.jobGuid,
338
+ status: job.status,
339
+ translationProvider: job.translationProvider,
340
+ updatedAt: job.updatedAt
341
+ })));
342
+ }
343
+
344
+ if (recent.length > 0) {
345
+ recentJobs.push(...recent.map(job => ({
346
+ pair: pairKey,
347
+ jobGuid: job.jobGuid,
348
+ status: job.status,
349
+ translationProvider: job.translationProvider,
350
+ updatedAt: job.updatedAt
351
+ })));
352
+ }
353
+ }
354
+
355
+ return { pendingJobs, recentJobs, jobCountsByPair };
356
+ }
357
+
358
+ function createChannelsMap(channelStats, withDetails) {
359
+ const channelsMap = {};
360
+ for (const [channelId, stats] of Object.entries(channelStats)) {
361
+ channelsMap[channelId] = stats.map(s => ({
362
+ project: s.prj ?? 'default',
363
+ sourceLang: s.sourceLang,
364
+ targetLangs: s.targetLangs || [],
365
+ segmentCount: s.segmentCount,
366
+ lastModified: s.lastModified,
367
+ ...(withDetails && { resourceCount: s.resCount })
368
+ }));
369
+ }
370
+ return channelsMap;
371
+ }
372
+
373
+ function createLanguagePairsMap(desiredPairs, supportedPairs) {
374
+ const result = {};
375
+
376
+ if (Object.keys(desiredPairs).length > 0) {
377
+ const desiredMap = {};
378
+ for (const [sourceLang, targetLangs] of Object.entries(desiredPairs)) {
379
+ desiredMap[sourceLang] = targetLangs;
380
+ }
381
+ result.desired = desiredMap;
382
+ }
383
+
384
+ if (Object.keys(supportedPairs).length > 0) {
385
+ const supportedMap = {};
386
+ for (const [sourceLang, targetLangs] of Object.entries(supportedPairs)) {
387
+ supportedMap[sourceLang] = targetLangs;
388
+ }
389
+ result.supported = supportedMap;
390
+ }
391
+
392
+ return result;
393
+ }
394
+
395
+ function createProvidersMap(providers, withDetails) {
396
+ const providersMap = {};
397
+ for (const p of providers) {
398
+ providersMap[p.id] = {
399
+ type: p.type,
400
+ available: p.available,
401
+ supportedPairs: p.supportedPairs,
402
+ ...(withDetails && {
403
+ quality: p.quality,
404
+ costPerWord: p.costPerWord,
405
+ costPerMChar: p.costPerMChar
406
+ }),
407
+ ...(p.error && { error: p.error })
408
+ };
409
+ }
410
+ return providersMap;
411
+ }
412
+
413
+ function createTranslationMemoryMap(tmStats, withDetails) {
414
+ const translationMemoryMap = {};
415
+ for (const [pairKey, stats] of Object.entries(tmStats)) {
416
+ const [sourceLang, targetLang] = pairKey.split('→');
417
+ const langPairKey = makeLangPairKey(sourceLang, targetLang);
418
+
419
+ if (withDetails) {
420
+ const providerMap = {};
421
+ for (const s of stats) {
422
+ providerMap[s.translationProvider] = {
423
+ translationUnitCount: s.tuCount,
424
+ status: s.status,
425
+ jobCount: s.jobCount,
426
+ redundancy: s.redundancy
427
+ };
428
+ }
429
+ translationMemoryMap[langPairKey] = providerMap;
430
+ } else {
431
+ const totalUnits = stats.reduce((sum, s) => sum + s.tuCount, 0);
432
+ translationMemoryMap[langPairKey] = totalUnits;
433
+ }
434
+ }
435
+ return translationMemoryMap;
436
+ }
437
+
438
+ function createCoverageData(coverage, withDetails) {
439
+ const coverageMap = {};
440
+ for (const [pairKey, pairCoverage] of Object.entries(coverage.byPair)) {
441
+ const [sourceLang, targetLang] = pairKey.split('→');
442
+ const langPairKey = makeLangPairKey(sourceLang, targetLang);
443
+ coverageMap[langPairKey] = {
444
+ totalSegments: pairCoverage.total,
445
+ translated: pairCoverage.translated,
446
+ untranslated: pairCoverage.untranslated,
447
+ coveragePercentage: (pairCoverage.coverage * 100).toFixed(2),
448
+ ...(withDetails && {
449
+ inFlight: pairCoverage.inFlight,
450
+ lowQuality: pairCoverage.lowQuality
451
+ })
452
+ };
453
+ }
454
+
455
+ return {
456
+ map: coverageMap,
457
+ summary: withDetails ? coverage.overall : undefined
458
+ };
459
+ }
460
+
461
+ function createJobsData(jobSummaries) {
462
+ const jobsMap = {};
463
+ const allJobs = jobSummaries.pendingJobs.concat(jobSummaries.recentJobs);
464
+ for (const job of allJobs) {
465
+ const [sourceLang, targetLang] = job.pair.split('→');
466
+ const langPairKey = makeLangPairKey(sourceLang, targetLang);
467
+ if (!jobsMap[langPairKey]) {
468
+ jobsMap[langPairKey] = [];
469
+ }
470
+ jobsMap[langPairKey].push({
471
+ jobGuid: job.jobGuid,
472
+ status: job.status,
473
+ translationProvider: job.translationProvider,
474
+ updatedAt: job.updatedAt
475
+ });
476
+ }
477
+
478
+ return {
479
+ jobsMap,
480
+ counts: jobSummaries.jobCountsByPair
481
+ };
482
+ }
483
+
484
+ function applyOptionalSections(result, { requestedSections, providers, coverage, jobSummaries, withDetails, channelStats, mm }) {
485
+ if (requestedSections.has('providers') && Array.isArray(providers)) {
486
+ result.providers = createProvidersMap(providers, withDetails);
487
+ }
488
+
489
+ if (requestedSections.has('coverage') && coverage) {
490
+ const coverageData = createCoverageData(coverage, withDetails);
491
+ result.coverage = coverageData.map;
492
+ if (coverageData.summary) {
493
+ result.coverageSummary = coverageData.summary;
494
+ }
495
+ }
496
+
497
+ if (requestedSections.has('jobs') && jobSummaries) {
498
+ const jobsData = createJobsData(jobSummaries);
499
+ result.jobs = jobsData.jobsMap;
500
+ if (jobsData.counts) {
501
+ result.jobCounts = jobsData.counts;
502
+ }
503
+ }
504
+
505
+ if (withDetails) {
506
+ result.details = {
507
+ channelCount: Object.keys(channelStats).length,
508
+ autoSnap: mm.rm.autoSnap
509
+ };
510
+ }
511
+ }
512
+
513
+ /**
514
+ * Build the final response object.
515
+ * @param {Object} params Parameters object
516
+ * @param {Object} params.channelStats Channel statistics
517
+ * @param {Object} params.desiredPairs Desired language pairs by source language
518
+ * @param {Object} params.supportedPairs Supported language pairs by source language from providers
519
+ * @param {Array} params.providers List of providers
520
+ * @param {Object} params.tmStats Translation memory statistics
521
+ * @param {Object} params.coverage Coverage metrics
522
+ * @param {Object} params.jobSummaries Job summaries
523
+ * @param {boolean} params.withDetails Whether to include detailed information
524
+ * @param {Set<string>} params.includeSet Requested optional sections
525
+ * @param {*} params.mm MonsterManager instance
526
+ * @returns {Object} Complete response object
527
+ */
528
+ function buildResponse({ channelStats, desiredPairs, supportedPairs, providers, tmStats, coverage, jobSummaries, withDetails, includeSet, mm }) {
529
+ const requestedSections = includeSet instanceof Set ? includeSet : new Set(includeSet ?? []);
530
+
531
+ const result = {
532
+ timestamp: new Date().toISOString()
533
+ };
534
+
535
+ // Add base sections if requested
536
+ if (requestedSections.has('channels')) {
537
+ result.channels = createChannelsMap(channelStats, withDetails);
538
+ }
539
+
540
+ if (requestedSections.has('languagePairs')) {
541
+ result.languagePairs = createLanguagePairsMap(desiredPairs, supportedPairs);
542
+ }
543
+
544
+ if (requestedSections.has('translationMemory')) {
545
+ result.translationMemory = createTranslationMemoryMap(tmStats, withDetails);
546
+ }
547
+
548
+ // Add optional sections
549
+ applyOptionalSections(result, {
550
+ requestedSections,
551
+ providers,
552
+ coverage,
553
+ jobSummaries,
554
+ withDetails,
555
+ channelStats,
556
+ mm
557
+ });
558
+
559
+ return result;
560
+ }
561
+
562
+
563
+ /**
564
+ * MCP tool for assembling the status of the localization system.
565
+ *
566
+ * This tool chains together functionality from monster, source_list (status mode),
567
+ * and ops_jobs to provide a comprehensive overview including:
568
+ * - Channel statistics
569
+ * - Language pairs
570
+ * - Translation memory and coverage
571
+ * - Provider availability
572
+ * - Job summaries
573
+ *
574
+ * Supports optional filtering by:
575
+ * - channel: Filter to specific channel ID (default: all channels)
576
+ * - provider: Filter to specific translation provider (default: all providers)
577
+ * - sourceLang: Filter to specific source language (default: all languages)
578
+ * - targetLang: Filter to specific target language (default: all languages)
579
+ */
580
+ export class StatusTool extends McpTool {
581
+ static metadata = {
582
+ name: 'status',
583
+ description: `Assemble the status of the l10nmonster translation system as a whole or specific parts of it.
584
+ The result may include any of these sections: translation channels, projects, jobs, memory, coverage, providers and supported language pairs.
585
+ The result can be further refined by filtering by channel, provider, source language, and target language.
586
+ `,
587
+ inputSchema: z.object({
588
+ detailLevel: z.enum(['summary', 'detailed'])
589
+ .default('summary')
590
+ .describe('Controls response verbosity: "summary" omits secondary fields, "detailed" includes them'),
591
+ include: z.array(z.enum(['channels', 'providers', 'languagePairs', 'translationMemory', 'coverage', 'jobs']))
592
+ .default(['channels', 'providers', 'languagePairs'])
593
+ .describe('Optional sections to include in the response. Defaults to ["channels", "providers", "languagePairs"]'),
594
+ channel: z.string()
595
+ .optional()
596
+ .describe('Optional channel ID to filter results. Defaults to all channels.'),
597
+ provider: z.string()
598
+ .optional()
599
+ .describe('Optional provider ID to filter results. Defaults to all providers.'),
600
+ sourceLang: z.string()
601
+ .optional()
602
+ .describe('Optional source language to filter results. Defaults to all source languages.'),
603
+ targetLang: z.string()
604
+ .optional()
605
+ .describe('Optional target language to filter results. Defaults to all target languages.'),
606
+ })
607
+ };
608
+
609
+ static async execute(mm, args) {
610
+ const filters = {
611
+ channel: args.channel,
612
+ provider: args.provider,
613
+ sourceLang: args.sourceLang,
614
+ targetLang: args.targetLang
615
+ };
616
+ const withDetails = args.detailLevel === 'detailed';
617
+ const includeSet = new Set(args.include);
618
+
619
+ // Gather all data using helper functions
620
+ const { channelStats, channelIds } = await getChannelStats(mm, filters);
621
+
622
+ // Get providers if needed for languagePairs:supported or providers section
623
+ const needsProviders = includeSet.has('providers') || includeSet.has('languagePairs');
624
+ const providers = needsProviders ?
625
+ await getAvailableProviders(mm, filters) :
626
+ null;
627
+
628
+ const desiredPairs = includeSet.has('languagePairs') ?
629
+ (await getDesiredLangPairs(mm, channelIds, filters)).desiredPairs :
630
+ {};
631
+
632
+ const supportedPairs = includeSet.has('languagePairs') ?
633
+ getSupportedLangPairs(providers, filters) :
634
+ {};
635
+
636
+ // Get TM stats if translationMemory, coverage, or jobs is requested
637
+ const needsTmStats = includeSet.has('translationMemory') || includeSet.has('coverage') || includeSet.has('jobs');
638
+ const { tmStats, availableLangPairs } = needsTmStats ?
639
+ await getTranslationMemoryStats(mm, filters) :
640
+ { tmStats: {}, availableLangPairs: [] };
641
+
642
+ const coverage = includeSet.has('coverage') ?
643
+ computeCoverage((await getTranslationStatus(mm, filters)).translationStatus) :
644
+ null;
645
+
646
+ const jobSummaries = includeSet.has('jobs') ?
647
+ await getJobSummaries(mm, filters, availableLangPairs) :
648
+ null;
649
+
650
+ // Build and return response
651
+ return buildResponse({
652
+ channelStats,
653
+ desiredPairs,
654
+ supportedPairs,
655
+ providers,
656
+ tmStats,
657
+ coverage,
658
+ jobSummaries,
659
+ withDetails,
660
+ includeSet,
661
+ mm
662
+ });
663
+ }
664
+ }
665
+