pabal-web-mcp 1.4.5 → 1.4.7
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/dist/bin/mcp-server.js +527 -181
- package/package.json +1 -1
package/dist/bin/mcp-server.js
CHANGED
|
@@ -352,6 +352,209 @@ import { z as z2 } from "zod";
|
|
|
352
352
|
import { zodToJsonSchema as zodToJsonSchema2 } from "zod-to-json-schema";
|
|
353
353
|
import path4 from "path";
|
|
354
354
|
|
|
355
|
+
// src/utils/aso-validation.util.ts
|
|
356
|
+
var FIELD_LIMITS_DOC_PATH = "docs/aso/ASO_FIELD_LIMITS.md";
|
|
357
|
+
var APP_STORE_LIMITS = {
|
|
358
|
+
name: 30,
|
|
359
|
+
subtitle: 30,
|
|
360
|
+
keywords: 100,
|
|
361
|
+
promotionalText: 170,
|
|
362
|
+
description: 4e3,
|
|
363
|
+
whatsNew: 4e3
|
|
364
|
+
};
|
|
365
|
+
var GOOGLE_PLAY_LIMITS = {
|
|
366
|
+
title: 50,
|
|
367
|
+
shortDescription: 80,
|
|
368
|
+
fullDescription: 4e3,
|
|
369
|
+
releaseNotes: 500
|
|
370
|
+
};
|
|
371
|
+
var INVALID_CHAR_REGEX = /[\u0000-\u0008\u000B-\u000C\u000E-\u001F\u007F-\u009F\uFEFF\u200B-\u200F\u202A-\u202E\u2060\uFE00-\uFE0F]/g;
|
|
372
|
+
function sanitizeText(value, fieldPath, warnings) {
|
|
373
|
+
if (typeof value !== "string") return value;
|
|
374
|
+
const cleaned = value.replace(INVALID_CHAR_REGEX, "");
|
|
375
|
+
if (cleaned !== value) {
|
|
376
|
+
warnings.push(`Removed invalid characters from ${fieldPath}`);
|
|
377
|
+
}
|
|
378
|
+
return cleaned;
|
|
379
|
+
}
|
|
380
|
+
function sanitizeAsoData(configData) {
|
|
381
|
+
const sanitizedData = JSON.parse(JSON.stringify(configData));
|
|
382
|
+
const warnings = [];
|
|
383
|
+
if (sanitizedData.appStore) {
|
|
384
|
+
const appStoreData = sanitizedData.appStore;
|
|
385
|
+
const locales = isAppStoreMultilingual(appStoreData) ? appStoreData.locales : { [appStoreData.locale || DEFAULT_LOCALE]: appStoreData };
|
|
386
|
+
for (const [locale, data] of Object.entries(locales)) {
|
|
387
|
+
data.name = sanitizeText(
|
|
388
|
+
data.name,
|
|
389
|
+
`App Store [${locale}].name`,
|
|
390
|
+
warnings
|
|
391
|
+
);
|
|
392
|
+
data.subtitle = sanitizeText(
|
|
393
|
+
data.subtitle,
|
|
394
|
+
`App Store [${locale}].subtitle`,
|
|
395
|
+
warnings
|
|
396
|
+
);
|
|
397
|
+
data.keywords = sanitizeText(
|
|
398
|
+
data.keywords,
|
|
399
|
+
`App Store [${locale}].keywords`,
|
|
400
|
+
warnings
|
|
401
|
+
);
|
|
402
|
+
data.promotionalText = sanitizeText(
|
|
403
|
+
data.promotionalText,
|
|
404
|
+
`App Store [${locale}].promotionalText`,
|
|
405
|
+
warnings
|
|
406
|
+
);
|
|
407
|
+
data.description = sanitizeText(
|
|
408
|
+
data.description,
|
|
409
|
+
`App Store [${locale}].description`,
|
|
410
|
+
warnings
|
|
411
|
+
);
|
|
412
|
+
data.whatsNew = sanitizeText(
|
|
413
|
+
data.whatsNew,
|
|
414
|
+
`App Store [${locale}].whatsNew`,
|
|
415
|
+
warnings
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (sanitizedData.googlePlay) {
|
|
420
|
+
const googlePlayData = sanitizedData.googlePlay;
|
|
421
|
+
const locales = isGooglePlayMultilingual(googlePlayData) ? googlePlayData.locales : {
|
|
422
|
+
[googlePlayData.defaultLanguage || DEFAULT_LOCALE]: googlePlayData
|
|
423
|
+
};
|
|
424
|
+
for (const [locale, data] of Object.entries(locales)) {
|
|
425
|
+
data.title = sanitizeText(
|
|
426
|
+
data.title,
|
|
427
|
+
`Google Play [${locale}].title`,
|
|
428
|
+
warnings
|
|
429
|
+
);
|
|
430
|
+
data.shortDescription = sanitizeText(
|
|
431
|
+
data.shortDescription,
|
|
432
|
+
`Google Play [${locale}].shortDescription`,
|
|
433
|
+
warnings
|
|
434
|
+
);
|
|
435
|
+
data.fullDescription = sanitizeText(
|
|
436
|
+
data.fullDescription,
|
|
437
|
+
`Google Play [${locale}].fullDescription`,
|
|
438
|
+
warnings
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return { sanitizedData, warnings };
|
|
443
|
+
}
|
|
444
|
+
function validateFieldLimits(configData) {
|
|
445
|
+
const issues = [];
|
|
446
|
+
if (configData.appStore) {
|
|
447
|
+
const appStoreData = configData.appStore;
|
|
448
|
+
const locales = isAppStoreMultilingual(appStoreData) ? appStoreData.locales : { [appStoreData.locale || DEFAULT_LOCALE]: appStoreData };
|
|
449
|
+
for (const [locale, data] of Object.entries(locales)) {
|
|
450
|
+
const checkField = (field, value) => {
|
|
451
|
+
if (typeof value === "string" && value.length > APP_STORE_LIMITS[field]) {
|
|
452
|
+
issues.push({
|
|
453
|
+
locale,
|
|
454
|
+
store: "appStore",
|
|
455
|
+
field,
|
|
456
|
+
currentLength: value.length,
|
|
457
|
+
limit: APP_STORE_LIMITS[field],
|
|
458
|
+
severity: "error"
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
checkField("name", data.name);
|
|
463
|
+
checkField("subtitle", data.subtitle);
|
|
464
|
+
checkField("keywords", data.keywords);
|
|
465
|
+
checkField("promotionalText", data.promotionalText);
|
|
466
|
+
checkField("description", data.description);
|
|
467
|
+
checkField("whatsNew", data.whatsNew);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (configData.googlePlay) {
|
|
471
|
+
const googlePlayData = configData.googlePlay;
|
|
472
|
+
const locales = isGooglePlayMultilingual(googlePlayData) ? googlePlayData.locales : {
|
|
473
|
+
[googlePlayData.defaultLanguage || DEFAULT_LOCALE]: googlePlayData
|
|
474
|
+
};
|
|
475
|
+
for (const [locale, data] of Object.entries(locales)) {
|
|
476
|
+
const checkField = (field, value) => {
|
|
477
|
+
if (typeof value === "string" && value.length > GOOGLE_PLAY_LIMITS[field]) {
|
|
478
|
+
issues.push({
|
|
479
|
+
locale,
|
|
480
|
+
store: "googlePlay",
|
|
481
|
+
field,
|
|
482
|
+
currentLength: value.length,
|
|
483
|
+
limit: GOOGLE_PLAY_LIMITS[field],
|
|
484
|
+
severity: "error"
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
checkField("title", data.title);
|
|
489
|
+
checkField("shortDescription", data.shortDescription);
|
|
490
|
+
checkField("fullDescription", data.fullDescription);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return issues;
|
|
494
|
+
}
|
|
495
|
+
function formatValidationIssues(issues) {
|
|
496
|
+
if (issues.length === 0) {
|
|
497
|
+
return `\u2705 All fields within limits (checked against ${FIELD_LIMITS_DOC_PATH})`;
|
|
498
|
+
}
|
|
499
|
+
const grouped = {};
|
|
500
|
+
for (const issue of issues) {
|
|
501
|
+
const key = `${issue.store} [${issue.locale}]`;
|
|
502
|
+
if (!grouped[key]) grouped[key] = [];
|
|
503
|
+
grouped[key].push(issue);
|
|
504
|
+
}
|
|
505
|
+
const lines = [
|
|
506
|
+
`\u26A0\uFE0F Field limit violations (see ${FIELD_LIMITS_DOC_PATH}):`
|
|
507
|
+
];
|
|
508
|
+
for (const [key, localeIssues] of Object.entries(grouped)) {
|
|
509
|
+
lines.push(`
|
|
510
|
+
${key}:`);
|
|
511
|
+
for (const issue of localeIssues) {
|
|
512
|
+
const over = issue.currentLength - issue.limit;
|
|
513
|
+
lines.push(
|
|
514
|
+
` - ${issue.field}: ${issue.currentLength}/${issue.limit} (${over} over)`
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return lines.join("\n");
|
|
519
|
+
}
|
|
520
|
+
function checkKeywordDuplicates(keywords) {
|
|
521
|
+
const keywordList = keywords.split(",").map((k) => k.trim().toLowerCase()).filter(Boolean);
|
|
522
|
+
const seen = /* @__PURE__ */ new Set();
|
|
523
|
+
const duplicates = [];
|
|
524
|
+
const uniqueKeywords = [];
|
|
525
|
+
for (const keyword of keywordList) {
|
|
526
|
+
if (seen.has(keyword)) {
|
|
527
|
+
if (!duplicates.includes(keyword)) {
|
|
528
|
+
duplicates.push(keyword);
|
|
529
|
+
}
|
|
530
|
+
} else {
|
|
531
|
+
seen.add(keyword);
|
|
532
|
+
uniqueKeywords.push(keyword);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return {
|
|
536
|
+
hasDuplicates: duplicates.length > 0,
|
|
537
|
+
duplicates,
|
|
538
|
+
uniqueKeywords
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
function validateKeywords(configData) {
|
|
542
|
+
const issues = [];
|
|
543
|
+
if (configData.appStore) {
|
|
544
|
+
const appStoreData = configData.appStore;
|
|
545
|
+
const locales = isAppStoreMultilingual(appStoreData) ? appStoreData.locales : { [appStoreData.locale || DEFAULT_LOCALE]: appStoreData };
|
|
546
|
+
for (const [locale, data] of Object.entries(locales)) {
|
|
547
|
+
if (data.keywords) {
|
|
548
|
+
const result = checkKeywordDuplicates(data.keywords);
|
|
549
|
+
if (result.hasDuplicates) {
|
|
550
|
+
issues.push({ locale, duplicates: result.duplicates });
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return issues;
|
|
556
|
+
}
|
|
557
|
+
|
|
355
558
|
// src/tools/utils/public-to-aso/prepare-aso-data-for-push.util.ts
|
|
356
559
|
function prepareAsoDataForPush(slug, configData) {
|
|
357
560
|
const storeData = {};
|
|
@@ -516,21 +719,6 @@ function convertToMultilingual(data, locale) {
|
|
|
516
719
|
|
|
517
720
|
// src/tools/public-to-aso.ts
|
|
518
721
|
import fs4 from "fs";
|
|
519
|
-
var FIELD_LIMITS_DOC_PATH = "docs/aso/ASO_FIELD_LIMITS.md";
|
|
520
|
-
var APP_STORE_LIMITS = {
|
|
521
|
-
name: 30,
|
|
522
|
-
subtitle: 30,
|
|
523
|
-
keywords: 100,
|
|
524
|
-
promotionalText: 170,
|
|
525
|
-
description: 4e3,
|
|
526
|
-
whatsNew: 4e3
|
|
527
|
-
};
|
|
528
|
-
var GOOGLE_PLAY_LIMITS = {
|
|
529
|
-
title: 50,
|
|
530
|
-
shortDescription: 80,
|
|
531
|
-
fullDescription: 4e3
|
|
532
|
-
};
|
|
533
|
-
var INVALID_CHAR_REGEX = /[\u0000-\u0008\u000B-\u000C\u000E-\u001F\u007F-\u009F\uFEFF\u200B-\u200F\u202A-\u202E\u2060\uFE00-\uFE0F]/g;
|
|
534
722
|
var toJsonSchema2 = zodToJsonSchema2;
|
|
535
723
|
var publicToAsoInputSchema = z2.object({
|
|
536
724
|
slug: z2.string().describe("Product slug"),
|
|
@@ -541,121 +729,6 @@ var jsonSchema2 = toJsonSchema2(publicToAsoInputSchema, {
|
|
|
541
729
|
$refStrategy: "none"
|
|
542
730
|
});
|
|
543
731
|
var inputSchema2 = jsonSchema2.definitions?.PublicToAsoInput || jsonSchema2;
|
|
544
|
-
function sanitizeText(value, fieldPath, warnings) {
|
|
545
|
-
if (typeof value !== "string") {
|
|
546
|
-
return value;
|
|
547
|
-
}
|
|
548
|
-
const cleaned = value.replace(INVALID_CHAR_REGEX, "");
|
|
549
|
-
if (cleaned !== value) {
|
|
550
|
-
warnings.push(`Removed invalid characters from ${fieldPath}`);
|
|
551
|
-
}
|
|
552
|
-
return cleaned;
|
|
553
|
-
}
|
|
554
|
-
function sanitizeAsoData(configData) {
|
|
555
|
-
const sanitizedData = JSON.parse(JSON.stringify(configData));
|
|
556
|
-
const warnings = [];
|
|
557
|
-
if (sanitizedData.appStore) {
|
|
558
|
-
const appStoreData = sanitizedData.appStore;
|
|
559
|
-
const locales = isAppStoreMultilingual(appStoreData) ? appStoreData.locales : { [appStoreData.locale || DEFAULT_LOCALE]: appStoreData };
|
|
560
|
-
for (const [locale, data] of Object.entries(locales)) {
|
|
561
|
-
data.name = sanitizeText(data.name, `App Store [${locale}].name`, warnings);
|
|
562
|
-
data.subtitle = sanitizeText(
|
|
563
|
-
data.subtitle,
|
|
564
|
-
`App Store [${locale}].subtitle`,
|
|
565
|
-
warnings
|
|
566
|
-
);
|
|
567
|
-
data.keywords = sanitizeText(
|
|
568
|
-
data.keywords,
|
|
569
|
-
`App Store [${locale}].keywords`,
|
|
570
|
-
warnings
|
|
571
|
-
);
|
|
572
|
-
data.promotionalText = sanitizeText(
|
|
573
|
-
data.promotionalText,
|
|
574
|
-
`App Store [${locale}].promotionalText`,
|
|
575
|
-
warnings
|
|
576
|
-
);
|
|
577
|
-
data.description = sanitizeText(
|
|
578
|
-
data.description,
|
|
579
|
-
`App Store [${locale}].description`,
|
|
580
|
-
warnings
|
|
581
|
-
);
|
|
582
|
-
data.whatsNew = sanitizeText(
|
|
583
|
-
data.whatsNew,
|
|
584
|
-
`App Store [${locale}].whatsNew`,
|
|
585
|
-
warnings
|
|
586
|
-
);
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
if (sanitizedData.googlePlay) {
|
|
590
|
-
const googlePlayData = sanitizedData.googlePlay;
|
|
591
|
-
const locales = isGooglePlayMultilingual(googlePlayData) ? googlePlayData.locales : {
|
|
592
|
-
[googlePlayData.defaultLanguage || DEFAULT_LOCALE]: googlePlayData
|
|
593
|
-
};
|
|
594
|
-
for (const [locale, data] of Object.entries(locales)) {
|
|
595
|
-
data.title = sanitizeText(
|
|
596
|
-
data.title,
|
|
597
|
-
`Google Play [${locale}].title`,
|
|
598
|
-
warnings
|
|
599
|
-
);
|
|
600
|
-
data.shortDescription = sanitizeText(
|
|
601
|
-
data.shortDescription,
|
|
602
|
-
`Google Play [${locale}].shortDescription`,
|
|
603
|
-
warnings
|
|
604
|
-
);
|
|
605
|
-
data.fullDescription = sanitizeText(
|
|
606
|
-
data.fullDescription,
|
|
607
|
-
`Google Play [${locale}].fullDescription`,
|
|
608
|
-
warnings
|
|
609
|
-
);
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
return { sanitizedData, warnings };
|
|
613
|
-
}
|
|
614
|
-
function validateFieldLimits(configData) {
|
|
615
|
-
const issues = [];
|
|
616
|
-
if (configData.appStore) {
|
|
617
|
-
const appStoreData = configData.appStore;
|
|
618
|
-
const locales = isAppStoreMultilingual(appStoreData) ? appStoreData.locales : { [appStoreData.locale || DEFAULT_LOCALE]: appStoreData };
|
|
619
|
-
for (const [locale, data] of Object.entries(locales)) {
|
|
620
|
-
const { name, subtitle, keywords, promotionalText, description, whatsNew } = data;
|
|
621
|
-
const push = (field, value, limit) => {
|
|
622
|
-
if (typeof value === "string" && value.length > limit) {
|
|
623
|
-
issues.push(
|
|
624
|
-
`App Store [${locale}].${field}: ${value.length}/${limit}`
|
|
625
|
-
);
|
|
626
|
-
}
|
|
627
|
-
};
|
|
628
|
-
push("name", name, APP_STORE_LIMITS.name);
|
|
629
|
-
push("subtitle", subtitle, APP_STORE_LIMITS.subtitle);
|
|
630
|
-
push("keywords", keywords, APP_STORE_LIMITS.keywords);
|
|
631
|
-
push("promotionalText", promotionalText, APP_STORE_LIMITS.promotionalText);
|
|
632
|
-
push("description", description, APP_STORE_LIMITS.description);
|
|
633
|
-
push("whatsNew", whatsNew, APP_STORE_LIMITS.whatsNew);
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
if (configData.googlePlay) {
|
|
637
|
-
const googlePlayData = configData.googlePlay;
|
|
638
|
-
const locales = isGooglePlayMultilingual(googlePlayData) ? googlePlayData.locales : { [googlePlayData.defaultLanguage || DEFAULT_LOCALE]: googlePlayData };
|
|
639
|
-
for (const [locale, data] of Object.entries(locales)) {
|
|
640
|
-
const { title, shortDescription, fullDescription } = data;
|
|
641
|
-
const push = (field, value, limit) => {
|
|
642
|
-
if (typeof value === "string" && value.length > limit) {
|
|
643
|
-
issues.push(
|
|
644
|
-
`Google Play [${locale}].${field}: ${value.length}/${limit}`
|
|
645
|
-
);
|
|
646
|
-
}
|
|
647
|
-
};
|
|
648
|
-
push("title", title, GOOGLE_PLAY_LIMITS.title);
|
|
649
|
-
push(
|
|
650
|
-
"shortDescription",
|
|
651
|
-
shortDescription,
|
|
652
|
-
GOOGLE_PLAY_LIMITS.shortDescription
|
|
653
|
-
);
|
|
654
|
-
push("fullDescription", fullDescription, GOOGLE_PLAY_LIMITS.fullDescription);
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
return issues;
|
|
658
|
-
}
|
|
659
732
|
async function downloadScreenshotsToAsoDir(slug, asoData) {
|
|
660
733
|
const rootDir = getPushDataDir();
|
|
661
734
|
const productStoreRoot = path4.join(rootDir, "products", slug, "store");
|
|
@@ -893,10 +966,7 @@ Possible causes:
|
|
|
893
966
|
}
|
|
894
967
|
const storeData = prepareAsoDataForPush(slug, sanitizedData);
|
|
895
968
|
const validationIssues = validateFieldLimits(sanitizedData);
|
|
896
|
-
const validationMessage = validationIssues
|
|
897
|
-
- ${validationIssues.join(
|
|
898
|
-
"\n- "
|
|
899
|
-
)}` : `Field limits OK (checked against ${FIELD_LIMITS_DOC_PATH})`;
|
|
969
|
+
const validationMessage = formatValidationIssues(validationIssues);
|
|
900
970
|
const pushDataRoot = getPushDataDir();
|
|
901
971
|
if (dryRun) {
|
|
902
972
|
return {
|
|
@@ -913,7 +983,9 @@ ${JSON.stringify(
|
|
|
913
983
|
|
|
914
984
|
${validationMessage}${sanitizeWarnings.length ? `
|
|
915
985
|
Sanitized invalid characters:
|
|
916
|
-
- ${sanitizeWarnings.join(
|
|
986
|
+
- ${sanitizeWarnings.join(
|
|
987
|
+
"\n- "
|
|
988
|
+
)}` : ""}`
|
|
917
989
|
}
|
|
918
990
|
]
|
|
919
991
|
};
|
|
@@ -1530,6 +1602,19 @@ ${researchSections.join("\n")}
|
|
|
1530
1602
|
|
|
1531
1603
|
`;
|
|
1532
1604
|
prompt += `**Reference**: ${FIELD_LIMITS_DOC_PATH2}
|
|
1605
|
+
|
|
1606
|
+
`;
|
|
1607
|
+
prompt += `---
|
|
1608
|
+
|
|
1609
|
+
`;
|
|
1610
|
+
prompt += `## Next Step
|
|
1611
|
+
|
|
1612
|
+
`;
|
|
1613
|
+
prompt += `After saving the optimized JSON, proceed to **Stage 2** to optimize other locales:
|
|
1614
|
+
`;
|
|
1615
|
+
prompt += `\`\`\`
|
|
1616
|
+
improve-public(slug="${slug}", stage="2", optimizedPrimary=<the JSON you just created>)
|
|
1617
|
+
\`\`\`
|
|
1533
1618
|
`;
|
|
1534
1619
|
return prompt;
|
|
1535
1620
|
}
|
|
@@ -1603,17 +1688,30 @@ ${optimizedPrimary}
|
|
|
1603
1688
|
\`\`\`
|
|
1604
1689
|
|
|
1605
1690
|
`;
|
|
1691
|
+
const { keywordResearchFallbackByLocale } = args;
|
|
1692
|
+
const localesNeedingFallback = nonPrimaryLocales.filter((loc) => {
|
|
1693
|
+
const fallbackInfo = keywordResearchFallbackByLocale?.[loc];
|
|
1694
|
+
const researchSections = keywordResearchByLocale[loc] || [];
|
|
1695
|
+
return researchSections.length === 0 || fallbackInfo?.isFallback;
|
|
1696
|
+
});
|
|
1606
1697
|
const primaryResearchSections = keywordResearchByLocale[primaryLocale] || [];
|
|
1607
1698
|
const hasPrimaryResearch = primaryResearchSections.length > 0;
|
|
1608
1699
|
prompt += `## Keyword Research (Per Locale)
|
|
1609
1700
|
|
|
1701
|
+
`;
|
|
1702
|
+
prompt += `**Priority:** Use each locale's own keyword research. English fallback is ONLY used when locale-specific research is missing.
|
|
1610
1703
|
`;
|
|
1611
1704
|
prompt += `When both iOS and Android research exist for a locale, treat iOS keywords as primary; use Android keywords only if space remains after fitting iOS keywords within character limits.
|
|
1612
1705
|
|
|
1613
1706
|
`;
|
|
1614
|
-
if (hasPrimaryResearch) {
|
|
1615
|
-
prompt +=
|
|
1616
|
-
|
|
1707
|
+
if (hasPrimaryResearch && localesNeedingFallback.length > 0) {
|
|
1708
|
+
prompt += `---
|
|
1709
|
+
`;
|
|
1710
|
+
prompt += `**\u{1F4DA} ENGLISH FALLBACK (${primaryLocale})** - Only for locales without their own research: ${localesNeedingFallback.join(
|
|
1711
|
+
", "
|
|
1712
|
+
)}
|
|
1713
|
+
`;
|
|
1714
|
+
prompt += `${primaryResearchSections.join("\n")}
|
|
1617
1715
|
|
|
1618
1716
|
`;
|
|
1619
1717
|
prompt += `---
|
|
@@ -1623,50 +1721,41 @@ ${primaryResearchSections.join("\n")}
|
|
|
1623
1721
|
nonPrimaryLocales.forEach((loc) => {
|
|
1624
1722
|
const researchSections = keywordResearchByLocale[loc] || [];
|
|
1625
1723
|
const researchDir = keywordResearchDirByLocale[loc];
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
${
|
|
1629
|
-
"\n"
|
|
1630
|
-
)}
|
|
1631
|
-
|
|
1724
|
+
const fallbackInfo = keywordResearchFallbackByLocale?.[loc];
|
|
1725
|
+
if (researchSections.length > 0 && !fallbackInfo?.isFallback) {
|
|
1726
|
+
prompt += `### Locale ${loc}: \u2705 Using locale-specific keyword research
|
|
1632
1727
|
`;
|
|
1633
|
-
|
|
1634
|
-
prompt +=
|
|
1635
|
-
|
|
1636
|
-
prompt += `No keyword research found at ${researchDir}.
|
|
1637
|
-
`;
|
|
1638
|
-
prompt += `**CRITICAL FALLBACK STRATEGY:** You MUST translate English keywords from primary locale (${primaryLocale}) into ${loc}. DO NOT use English keywords directly.
|
|
1728
|
+
prompt += researchSections.join("\n");
|
|
1729
|
+
prompt += `
|
|
1730
|
+
**Use these ${loc} keywords directly** - they are already in the target language.
|
|
1639
1731
|
|
|
1640
1732
|
`;
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
prompt += `1. Take the Tier 1/2/3 keywords from English research above (${primaryLocale})
|
|
1644
|
-
`;
|
|
1645
|
-
prompt += `2. **TRANSLATE each English keyword into ${loc}** - use natural, native expressions (NOT literal word-for-word translation)
|
|
1733
|
+
} else if (researchSections.length > 0 && fallbackInfo?.isFallback) {
|
|
1734
|
+
prompt += `### Locale ${loc}: \u{1F504} No ${loc} research found - Using ${fallbackInfo.fallbackLocale} as fallback
|
|
1646
1735
|
`;
|
|
1647
|
-
prompt +=
|
|
1736
|
+
prompt += researchSections.join("\n");
|
|
1737
|
+
prompt += `
|
|
1738
|
+
**MUST TRANSLATE:** The keywords above are in ${fallbackInfo.fallbackLocale}. You MUST:
|
|
1648
1739
|
`;
|
|
1649
|
-
prompt += `
|
|
1740
|
+
prompt += `1. **TRANSLATE each keyword into ${loc}** - use natural, native expressions
|
|
1650
1741
|
`;
|
|
1651
|
-
prompt += `
|
|
1742
|
+
prompt += `2. Ensure translated keywords are what ${loc} users would actually search for
|
|
1652
1743
|
`;
|
|
1653
|
-
prompt += `
|
|
1744
|
+
prompt += `3. **DO NOT use ${fallbackInfo.fallbackLocale} keywords directly** - all keywords must be in ${loc} language
|
|
1654
1745
|
|
|
1655
1746
|
`;
|
|
1656
|
-
} else {
|
|
1657
|
-
prompt += `### Locale ${loc}: \u26A0\uFE0F No research - TRANSLATE
|
|
1658
|
-
`;
|
|
1659
|
-
prompt += `No keyword research found. Extract keywords from the optimizedPrimary JSON above and **TRANSLATE them to ${loc}**:
|
|
1660
|
-
`;
|
|
1661
|
-
prompt += `1. Extract keywords from \`aso.keywords\` in optimizedPrimary (these are in English/${primaryLocale})
|
|
1747
|
+
} else if (hasPrimaryResearch) {
|
|
1748
|
+
prompt += `### Locale ${loc}: \u26A0\uFE0F No research found - TRANSLATE from English fallback above
|
|
1662
1749
|
`;
|
|
1663
|
-
prompt += `
|
|
1750
|
+
prompt += `No keyword research found at ${researchDir}.
|
|
1664
1751
|
`;
|
|
1665
|
-
prompt +=
|
|
1752
|
+
prompt += `**Use the ENGLISH FALLBACK section above** and TRANSLATE all keywords to ${loc}.
|
|
1753
|
+
|
|
1666
1754
|
`;
|
|
1667
|
-
|
|
1755
|
+
} else {
|
|
1756
|
+
prompt += `### Locale ${loc}: \u274C No research available
|
|
1668
1757
|
`;
|
|
1669
|
-
prompt += `
|
|
1758
|
+
prompt += `No keyword research found. Extract keywords from \`aso.keywords\` in optimizedPrimary and **TRANSLATE them to ${loc}**.
|
|
1670
1759
|
|
|
1671
1760
|
`;
|
|
1672
1761
|
}
|
|
@@ -1807,6 +1896,12 @@ ${researchSections.join(
|
|
|
1807
1896
|
`;
|
|
1808
1897
|
} else {
|
|
1809
1898
|
prompt += `2. All batches completed! \u2705
|
|
1899
|
+
`;
|
|
1900
|
+
prompt += `3. **Run validate-aso** to verify all locales:
|
|
1901
|
+
`;
|
|
1902
|
+
prompt += ` \`\`\`
|
|
1903
|
+
validate-aso(slug="${slug}")
|
|
1904
|
+
\`\`\`
|
|
1810
1905
|
|
|
1811
1906
|
`;
|
|
1812
1907
|
}
|
|
@@ -1882,7 +1977,32 @@ ${researchSections.join(
|
|
|
1882
1977
|
prompt += `Repeat for all locales in this batch: ${nonPrimaryLocales.join(
|
|
1883
1978
|
", "
|
|
1884
1979
|
)}
|
|
1980
|
+
|
|
1981
|
+
`;
|
|
1982
|
+
const isLastBatch = batchIndex === void 0 || totalBatches && batchIndex + 1 >= totalBatches;
|
|
1983
|
+
if (isLastBatch) {
|
|
1984
|
+
prompt += `---
|
|
1985
|
+
|
|
1986
|
+
`;
|
|
1987
|
+
prompt += `## Final Step: Validate All Locales
|
|
1988
|
+
|
|
1989
|
+
`;
|
|
1990
|
+
prompt += `After completing ALL locale optimizations, run validation:
|
|
1991
|
+
`;
|
|
1992
|
+
prompt += `\`\`\`
|
|
1993
|
+
validate-aso(slug="${slug}")
|
|
1994
|
+
\`\`\`
|
|
1995
|
+
|
|
1996
|
+
`;
|
|
1997
|
+
prompt += `This checks:
|
|
1998
|
+
`;
|
|
1999
|
+
prompt += `- Field length limits (title \u226430, subtitle \u226430, keywords \u2264100, etc.)
|
|
2000
|
+
`;
|
|
2001
|
+
prompt += `- Keyword duplicates
|
|
2002
|
+
`;
|
|
2003
|
+
prompt += `- Invalid characters
|
|
1885
2004
|
`;
|
|
2005
|
+
}
|
|
1886
2006
|
return prompt;
|
|
1887
2007
|
}
|
|
1888
2008
|
|
|
@@ -2130,7 +2250,7 @@ function formatMergedData(merged, researchDir) {
|
|
|
2130
2250
|
lines.push("\n----");
|
|
2131
2251
|
return lines.join("\n");
|
|
2132
2252
|
}
|
|
2133
|
-
function
|
|
2253
|
+
function loadKeywordResearchForLocaleInternal(slug, locale) {
|
|
2134
2254
|
const researchDir = path6.join(
|
|
2135
2255
|
getKeywordResearchDir(),
|
|
2136
2256
|
"products",
|
|
@@ -2139,9 +2259,12 @@ function loadKeywordResearchForLocale(slug, locale) {
|
|
|
2139
2259
|
locale
|
|
2140
2260
|
);
|
|
2141
2261
|
if (!fs6.existsSync(researchDir)) {
|
|
2142
|
-
return
|
|
2262
|
+
return null;
|
|
2143
2263
|
}
|
|
2144
2264
|
const files = fs6.readdirSync(researchDir).filter((file) => file.endsWith(".json"));
|
|
2265
|
+
if (files.length === 0) {
|
|
2266
|
+
return null;
|
|
2267
|
+
}
|
|
2145
2268
|
const entries = [];
|
|
2146
2269
|
for (const file of files) {
|
|
2147
2270
|
const filePath = path6.join(researchDir, file);
|
|
@@ -2159,17 +2282,53 @@ function loadKeywordResearchForLocale(slug, locale) {
|
|
|
2159
2282
|
}
|
|
2160
2283
|
}
|
|
2161
2284
|
const validEntries = entries.filter((e) => !e.data?.parseError);
|
|
2285
|
+
if (validEntries.length === 0) {
|
|
2286
|
+
return null;
|
|
2287
|
+
}
|
|
2162
2288
|
if (validEntries.length > 1) {
|
|
2163
2289
|
const merged = mergeKeywordData(validEntries);
|
|
2164
2290
|
const mergedSection = formatMergedData(merged, researchDir);
|
|
2165
2291
|
return { entries, sections: [mergedSection], researchDir };
|
|
2166
|
-
} else if (validEntries.length === 1) {
|
|
2167
|
-
const sections2 = entries.map(formatEntry);
|
|
2168
|
-
return { entries, sections: sections2, researchDir };
|
|
2169
2292
|
}
|
|
2170
2293
|
const sections = entries.map(formatEntry);
|
|
2171
2294
|
return { entries, sections, researchDir };
|
|
2172
2295
|
}
|
|
2296
|
+
var FALLBACK_LOCALES = ["en-US", "en"];
|
|
2297
|
+
function loadKeywordResearchForLocale(slug, locale) {
|
|
2298
|
+
const researchDir = path6.join(
|
|
2299
|
+
getKeywordResearchDir(),
|
|
2300
|
+
"products",
|
|
2301
|
+
slug,
|
|
2302
|
+
"locales",
|
|
2303
|
+
locale
|
|
2304
|
+
);
|
|
2305
|
+
const result = loadKeywordResearchForLocaleInternal(slug, locale);
|
|
2306
|
+
if (result) {
|
|
2307
|
+
return { ...result, isFallback: false };
|
|
2308
|
+
}
|
|
2309
|
+
for (const fallbackLocale of FALLBACK_LOCALES) {
|
|
2310
|
+
if (fallbackLocale === locale) continue;
|
|
2311
|
+
const fallbackResult = loadKeywordResearchForLocaleInternal(
|
|
2312
|
+
slug,
|
|
2313
|
+
fallbackLocale
|
|
2314
|
+
);
|
|
2315
|
+
if (fallbackResult) {
|
|
2316
|
+
const fallbackNotice = `\u26A0\uFE0F **FALLBACK: Using ${fallbackLocale} keywords** - No research found for ${locale}. You MUST TRANSLATE these keywords to ${locale}.
|
|
2317
|
+
`;
|
|
2318
|
+
const sectionsWithNotice = fallbackResult.sections.map(
|
|
2319
|
+
(section) => fallbackNotice + section
|
|
2320
|
+
);
|
|
2321
|
+
return {
|
|
2322
|
+
entries: fallbackResult.entries,
|
|
2323
|
+
sections: sectionsWithNotice,
|
|
2324
|
+
researchDir: fallbackResult.researchDir,
|
|
2325
|
+
isFallback: true,
|
|
2326
|
+
fallbackLocale
|
|
2327
|
+
};
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
return { entries: [], sections: [], researchDir, isFallback: false };
|
|
2331
|
+
}
|
|
2173
2332
|
|
|
2174
2333
|
// src/tools/improve-public.ts
|
|
2175
2334
|
var toJsonSchema3 = zodToJsonSchema3;
|
|
@@ -2217,12 +2376,12 @@ This tool returns a PROMPT containing:
|
|
|
2217
2376
|
|
|
2218
2377
|
## STAGES
|
|
2219
2378
|
- **Stage 1:** Primary locale optimization using saved keyword research (ios + android combined)
|
|
2220
|
-
- **Stage 2:** Localize to other languages
|
|
2379
|
+
- **Stage 2:** Localize to other languages - **each locale uses its OWN keyword research**
|
|
2221
2380
|
|
|
2222
|
-
## KEYWORD SOURCES
|
|
2223
|
-
- Uses SAVED keyword research from .aso/keywordResearch/products/[slug]/locales/
|
|
2224
|
-
-
|
|
2225
|
-
-
|
|
2381
|
+
## KEYWORD SOURCES (Per Locale)
|
|
2382
|
+
- **Priority 1:** Uses each locale's SAVED keyword research from .aso/keywordResearch/products/[slug]/locales/[locale]/
|
|
2383
|
+
- **Priority 2 (Fallback):** If locale-specific research is missing, falls back to en-US/en keywords and TRANSLATES them
|
|
2384
|
+
- iOS and Android research are automatically combined per locale (iOS prioritized)
|
|
2226
2385
|
|
|
2227
2386
|
**CRITICAL:** Only processes existing locale files. Does NOT create new files.`,
|
|
2228
2387
|
inputSchema: inputSchema3
|
|
@@ -2279,10 +2438,15 @@ async function handleImprovePublic(input) {
|
|
|
2279
2438
|
}
|
|
2280
2439
|
const keywordResearchByLocale = {};
|
|
2281
2440
|
const keywordResearchDirByLocale = {};
|
|
2441
|
+
const keywordResearchFallbackByLocale = {};
|
|
2282
2442
|
for (const loc of targetLocales) {
|
|
2283
2443
|
const research = loadKeywordResearchForLocale(slug, loc);
|
|
2284
2444
|
keywordResearchByLocale[loc] = research.sections;
|
|
2285
2445
|
keywordResearchDirByLocale[loc] = research.researchDir;
|
|
2446
|
+
keywordResearchFallbackByLocale[loc] = {
|
|
2447
|
+
isFallback: research.isFallback,
|
|
2448
|
+
fallbackLocale: research.fallbackLocale
|
|
2449
|
+
};
|
|
2286
2450
|
}
|
|
2287
2451
|
const baseArgs = {
|
|
2288
2452
|
slug,
|
|
@@ -2291,7 +2455,8 @@ async function handleImprovePublic(input) {
|
|
|
2291
2455
|
targetLocales,
|
|
2292
2456
|
localeSections,
|
|
2293
2457
|
keywordResearchByLocale,
|
|
2294
|
-
keywordResearchDirByLocale
|
|
2458
|
+
keywordResearchDirByLocale,
|
|
2459
|
+
keywordResearchFallbackByLocale
|
|
2295
2460
|
};
|
|
2296
2461
|
if (stage === "1" || stage === "both") {
|
|
2297
2462
|
const prompt = generatePrimaryOptimizationPrompt(baseArgs);
|
|
@@ -2339,6 +2504,7 @@ async function handleImprovePublic(input) {
|
|
|
2339
2504
|
localeSections: baseArgs.localeSections,
|
|
2340
2505
|
keywordResearchByLocale: baseArgs.keywordResearchByLocale,
|
|
2341
2506
|
keywordResearchDirByLocale: baseArgs.keywordResearchDirByLocale,
|
|
2507
|
+
keywordResearchFallbackByLocale: baseArgs.keywordResearchFallbackByLocale,
|
|
2342
2508
|
optimizedPrimary,
|
|
2343
2509
|
batchLocales,
|
|
2344
2510
|
batchIndex: currentBatchIndex,
|
|
@@ -3508,6 +3674,177 @@ Context around ${pos}: ${context}`
|
|
|
3508
3674
|
};
|
|
3509
3675
|
}
|
|
3510
3676
|
|
|
3677
|
+
// src/tools/validate-aso.ts
|
|
3678
|
+
import { z as z8 } from "zod";
|
|
3679
|
+
import { zodToJsonSchema as zodToJsonSchema8 } from "zod-to-json-schema";
|
|
3680
|
+
var toJsonSchema5 = zodToJsonSchema8;
|
|
3681
|
+
var validateAsoInputSchema = z8.object({
|
|
3682
|
+
slug: z8.string().describe("Product slug"),
|
|
3683
|
+
locale: z8.string().optional().describe("Specific locale to validate (default: all locales)"),
|
|
3684
|
+
fix: z8.boolean().optional().default(false).describe("Auto-fix issues where possible (e.g., remove invalid chars)")
|
|
3685
|
+
});
|
|
3686
|
+
var jsonSchema8 = toJsonSchema5(validateAsoInputSchema, {
|
|
3687
|
+
name: "ValidateAsoInput",
|
|
3688
|
+
$refStrategy: "none"
|
|
3689
|
+
});
|
|
3690
|
+
var inputSchema8 = jsonSchema8.definitions?.ValidateAsoInput || jsonSchema8;
|
|
3691
|
+
var validateAsoTool = {
|
|
3692
|
+
name: "validate-aso",
|
|
3693
|
+
description: `Validates ASO data against App Store / Google Play field limits and rules.
|
|
3694
|
+
|
|
3695
|
+
**IMPORTANT:** Use 'search-app' tool first to resolve the exact slug.
|
|
3696
|
+
|
|
3697
|
+
## WHAT IT VALIDATES
|
|
3698
|
+
1. **Field Length Limits** (${FIELD_LIMITS_DOC_PATH}):
|
|
3699
|
+
- App Store: name \u2264${APP_STORE_LIMITS.name}, subtitle \u2264${APP_STORE_LIMITS.subtitle}, keywords \u2264${APP_STORE_LIMITS.keywords}, description \u2264${APP_STORE_LIMITS.description}
|
|
3700
|
+
- Google Play: title \u2264${GOOGLE_PLAY_LIMITS.title}, shortDescription \u2264${GOOGLE_PLAY_LIMITS.shortDescription}, fullDescription \u2264${GOOGLE_PLAY_LIMITS.fullDescription}
|
|
3701
|
+
|
|
3702
|
+
2. **Keyword Duplicates** (App Store only):
|
|
3703
|
+
- Checks for duplicate keywords in comma-separated list
|
|
3704
|
+
|
|
3705
|
+
3. **Invalid Characters**:
|
|
3706
|
+
- Control characters, BOM, zero-width/invisible characters, variation selectors
|
|
3707
|
+
|
|
3708
|
+
## WHEN TO USE
|
|
3709
|
+
- After running improve-public Stage 1/2 to verify optimization results
|
|
3710
|
+
- Before running public-to-aso to ensure data is valid
|
|
3711
|
+
- Anytime you want to check ASO data validity
|
|
3712
|
+
|
|
3713
|
+
## OPTIONS
|
|
3714
|
+
- \`locale\`: Validate specific locale only (e.g., "ko-KR")
|
|
3715
|
+
- \`fix\`: Auto-fix issues where possible (removes invalid characters)`,
|
|
3716
|
+
inputSchema: inputSchema8
|
|
3717
|
+
};
|
|
3718
|
+
function getLocaleStats(configData) {
|
|
3719
|
+
const stats = [];
|
|
3720
|
+
if (configData.appStore) {
|
|
3721
|
+
const appStoreData = configData.appStore;
|
|
3722
|
+
const locales = isAppStoreMultilingual(appStoreData) ? appStoreData.locales : { [appStoreData.locale || DEFAULT_LOCALE]: appStoreData };
|
|
3723
|
+
for (const [locale, data] of Object.entries(locales)) {
|
|
3724
|
+
const fields = [];
|
|
3725
|
+
const checkField = (field, value, limit) => {
|
|
3726
|
+
const length = value?.length || 0;
|
|
3727
|
+
let status = "ok";
|
|
3728
|
+
if (length > limit) status = "error";
|
|
3729
|
+
else if (length > limit * 0.9) status = "warning";
|
|
3730
|
+
fields.push({ field, length, limit, status });
|
|
3731
|
+
};
|
|
3732
|
+
checkField("name", data.name, APP_STORE_LIMITS.name);
|
|
3733
|
+
checkField("subtitle", data.subtitle, APP_STORE_LIMITS.subtitle);
|
|
3734
|
+
checkField("keywords", data.keywords, APP_STORE_LIMITS.keywords);
|
|
3735
|
+
checkField("promotionalText", data.promotionalText, APP_STORE_LIMITS.promotionalText);
|
|
3736
|
+
checkField("description", data.description, APP_STORE_LIMITS.description);
|
|
3737
|
+
stats.push({ locale, store: "appStore", fields });
|
|
3738
|
+
}
|
|
3739
|
+
}
|
|
3740
|
+
if (configData.googlePlay) {
|
|
3741
|
+
const googlePlayData = configData.googlePlay;
|
|
3742
|
+
const locales = isGooglePlayMultilingual(googlePlayData) ? googlePlayData.locales : { [googlePlayData.defaultLanguage || DEFAULT_LOCALE]: googlePlayData };
|
|
3743
|
+
for (const [locale, data] of Object.entries(locales)) {
|
|
3744
|
+
const fields = [];
|
|
3745
|
+
const checkField = (field, value, limit) => {
|
|
3746
|
+
const length = value?.length || 0;
|
|
3747
|
+
let status = "ok";
|
|
3748
|
+
if (length > limit) status = "error";
|
|
3749
|
+
else if (length > limit * 0.9) status = "warning";
|
|
3750
|
+
fields.push({ field, length, limit, status });
|
|
3751
|
+
};
|
|
3752
|
+
checkField("title", data.title, GOOGLE_PLAY_LIMITS.title);
|
|
3753
|
+
checkField("shortDescription", data.shortDescription, GOOGLE_PLAY_LIMITS.shortDescription);
|
|
3754
|
+
checkField("fullDescription", data.fullDescription, GOOGLE_PLAY_LIMITS.fullDescription);
|
|
3755
|
+
stats.push({ locale, store: "googlePlay", fields });
|
|
3756
|
+
}
|
|
3757
|
+
}
|
|
3758
|
+
return stats;
|
|
3759
|
+
}
|
|
3760
|
+
function formatStats(stats, filterLocale) {
|
|
3761
|
+
const filteredStats = filterLocale ? stats.filter((s) => s.locale === filterLocale) : stats;
|
|
3762
|
+
if (filteredStats.length === 0) {
|
|
3763
|
+
return filterLocale ? `No data found for locale: ${filterLocale}` : "No ASO data found";
|
|
3764
|
+
}
|
|
3765
|
+
const lines = ["## Field Length Report\n"];
|
|
3766
|
+
for (const stat of filteredStats) {
|
|
3767
|
+
const storeLabel = stat.store === "appStore" ? "App Store" : "Google Play";
|
|
3768
|
+
lines.push(`### ${storeLabel} [${stat.locale}]
|
|
3769
|
+
`);
|
|
3770
|
+
lines.push("| Field | Length | Limit | Status |");
|
|
3771
|
+
lines.push("|-------|--------|-------|--------|");
|
|
3772
|
+
for (const field of stat.fields) {
|
|
3773
|
+
const statusEmoji = field.status === "error" ? "\u274C" : field.status === "warning" ? "\u26A0\uFE0F" : "\u2705";
|
|
3774
|
+
lines.push(
|
|
3775
|
+
`| ${field.field} | ${field.length} | ${field.limit} | ${statusEmoji} |`
|
|
3776
|
+
);
|
|
3777
|
+
}
|
|
3778
|
+
lines.push("");
|
|
3779
|
+
}
|
|
3780
|
+
return lines.join("\n");
|
|
3781
|
+
}
|
|
3782
|
+
async function handleValidateAso(input) {
|
|
3783
|
+
const { slug, locale, fix } = input;
|
|
3784
|
+
const configData = loadAsoFromConfig(slug);
|
|
3785
|
+
if (!configData.googlePlay && !configData.appStore) {
|
|
3786
|
+
throw new Error(`No ASO data found for ${slug}`);
|
|
3787
|
+
}
|
|
3788
|
+
const results = [];
|
|
3789
|
+
results.push(`# ASO Validation Report: ${slug}
|
|
3790
|
+
`);
|
|
3791
|
+
const { sanitizedData, warnings: sanitizeWarnings } = sanitizeAsoData(configData);
|
|
3792
|
+
if (sanitizeWarnings.length > 0) {
|
|
3793
|
+
results.push(`## Invalid Characters Found
|
|
3794
|
+
`);
|
|
3795
|
+
if (fix) {
|
|
3796
|
+
results.push(
|
|
3797
|
+
`The following invalid characters were ${fix ? "removed" : "detected"}:
|
|
3798
|
+
`
|
|
3799
|
+
);
|
|
3800
|
+
}
|
|
3801
|
+
for (const warning of sanitizeWarnings) {
|
|
3802
|
+
results.push(`- ${warning}`);
|
|
3803
|
+
}
|
|
3804
|
+
results.push("");
|
|
3805
|
+
}
|
|
3806
|
+
const dataToValidate = fix ? sanitizedData : configData;
|
|
3807
|
+
const limitIssues = validateFieldLimits(dataToValidate);
|
|
3808
|
+
const filteredIssues = locale ? limitIssues.filter((issue) => issue.locale === locale) : limitIssues;
|
|
3809
|
+
results.push(formatValidationIssues(filteredIssues));
|
|
3810
|
+
results.push("");
|
|
3811
|
+
const keywordIssues = validateKeywords(dataToValidate);
|
|
3812
|
+
const filteredKeywordIssues = locale ? keywordIssues.filter((issue) => issue.locale === locale) : keywordIssues;
|
|
3813
|
+
if (filteredKeywordIssues.length > 0) {
|
|
3814
|
+
results.push(`## Keyword Duplicates
|
|
3815
|
+
`);
|
|
3816
|
+
for (const issue of filteredKeywordIssues) {
|
|
3817
|
+
results.push(
|
|
3818
|
+
`- [${issue.locale}]: ${issue.duplicates.join(", ")}`
|
|
3819
|
+
);
|
|
3820
|
+
}
|
|
3821
|
+
results.push("");
|
|
3822
|
+
}
|
|
3823
|
+
const stats = getLocaleStats(dataToValidate);
|
|
3824
|
+
results.push(formatStats(stats, locale));
|
|
3825
|
+
const hasErrors = filteredIssues.length > 0 || filteredKeywordIssues.length > 0;
|
|
3826
|
+
const hasSanitizeWarnings = sanitizeWarnings.length > 0;
|
|
3827
|
+
results.push(`---
|
|
3828
|
+
`);
|
|
3829
|
+
if (hasErrors) {
|
|
3830
|
+
results.push(`\u274C **Validation failed** - Fix the issues above before pushing to stores.`);
|
|
3831
|
+
results.push(`
|
|
3832
|
+
Reference: ${FIELD_LIMITS_DOC_PATH}`);
|
|
3833
|
+
} else if (hasSanitizeWarnings && !fix) {
|
|
3834
|
+
results.push(`\u26A0\uFE0F **Invalid characters detected** - Run with \`fix: true\` to auto-remove.`);
|
|
3835
|
+
} else {
|
|
3836
|
+
results.push(`\u2705 **Validation passed** - Ready to push to stores.`);
|
|
3837
|
+
}
|
|
3838
|
+
return {
|
|
3839
|
+
content: [
|
|
3840
|
+
{
|
|
3841
|
+
type: "text",
|
|
3842
|
+
text: results.join("\n")
|
|
3843
|
+
}
|
|
3844
|
+
]
|
|
3845
|
+
};
|
|
3846
|
+
}
|
|
3847
|
+
|
|
3511
3848
|
// src/tools/index.ts
|
|
3512
3849
|
var tools = [
|
|
3513
3850
|
{
|
|
@@ -3565,6 +3902,14 @@ var tools = [
|
|
|
3565
3902
|
zodSchema: searchAppInputSchema,
|
|
3566
3903
|
handler: handleSearchApp,
|
|
3567
3904
|
category: "App Management"
|
|
3905
|
+
},
|
|
3906
|
+
{
|
|
3907
|
+
name: validateAsoTool.name,
|
|
3908
|
+
description: validateAsoTool.description,
|
|
3909
|
+
inputSchema: validateAsoTool.inputSchema,
|
|
3910
|
+
zodSchema: validateAsoInputSchema,
|
|
3911
|
+
handler: handleValidateAso,
|
|
3912
|
+
category: "ASO Validation"
|
|
3568
3913
|
}
|
|
3569
3914
|
];
|
|
3570
3915
|
function getToolDefinitions() {
|
|
@@ -3575,7 +3920,8 @@ function getToolDefinitions() {
|
|
|
3575
3920
|
initProjectTool,
|
|
3576
3921
|
createBlogHtmlTool,
|
|
3577
3922
|
keywordResearchTool,
|
|
3578
|
-
searchAppTool
|
|
3923
|
+
searchAppTool,
|
|
3924
|
+
validateAsoTool
|
|
3579
3925
|
];
|
|
3580
3926
|
}
|
|
3581
3927
|
function getToolHandler(name) {
|