pabal-web-mcp 1.4.6 → 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 +471 -190
- 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,89 +1688,74 @@ ${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 += `---
|
|
1620
1718
|
|
|
1621
1719
|
`;
|
|
1622
1720
|
}
|
|
1623
|
-
const { keywordResearchFallbackByLocale } = args;
|
|
1624
1721
|
nonPrimaryLocales.forEach((loc) => {
|
|
1625
1722
|
const researchSections = keywordResearchByLocale[loc] || [];
|
|
1626
1723
|
const researchDir = keywordResearchDirByLocale[loc];
|
|
1627
1724
|
const fallbackInfo = keywordResearchFallbackByLocale?.[loc];
|
|
1628
|
-
if (researchSections.length > 0) {
|
|
1629
|
-
|
|
1630
|
-
prompt += `### Locale ${loc}: \u{1F504} Using ${fallbackInfo.fallbackLocale} keywords as fallback - MUST TRANSLATE TO ${loc.toUpperCase()}
|
|
1725
|
+
if (researchSections.length > 0 && !fallbackInfo?.isFallback) {
|
|
1726
|
+
prompt += `### Locale ${loc}: \u2705 Using locale-specific keyword research
|
|
1631
1727
|
`;
|
|
1632
|
-
|
|
1633
|
-
|
|
1728
|
+
prompt += researchSections.join("\n");
|
|
1729
|
+
prompt += `
|
|
1730
|
+
**Use these ${loc} keywords directly** - they are already in the target language.
|
|
1634
1731
|
|
|
1635
|
-
**CRITICAL:** The keywords above are in ${fallbackInfo.fallbackLocale}. You MUST:
|
|
1636
1732
|
`;
|
|
1637
|
-
|
|
1733
|
+
} else if (researchSections.length > 0 && fallbackInfo?.isFallback) {
|
|
1734
|
+
prompt += `### Locale ${loc}: \u{1F504} No ${loc} research found - Using ${fallbackInfo.fallbackLocale} as fallback
|
|
1638
1735
|
`;
|
|
1639
|
-
|
|
1736
|
+
prompt += researchSections.join("\n");
|
|
1737
|
+
prompt += `
|
|
1738
|
+
**MUST TRANSLATE:** The keywords above are in ${fallbackInfo.fallbackLocale}. You MUST:
|
|
1640
1739
|
`;
|
|
1641
|
-
|
|
1642
|
-
|
|
1740
|
+
prompt += `1. **TRANSLATE each keyword into ${loc}** - use natural, native expressions
|
|
1741
|
+
`;
|
|
1742
|
+
prompt += `2. Ensure translated keywords are what ${loc} users would actually search for
|
|
1643
1743
|
`;
|
|
1644
|
-
}
|
|
1645
|
-
prompt += `### Locale ${loc}: \u2705 Saved research found (locale-specific)
|
|
1646
|
-
${researchSections.join(
|
|
1647
|
-
"\n"
|
|
1648
|
-
)}
|
|
1744
|
+
prompt += `3. **DO NOT use ${fallbackInfo.fallbackLocale} keywords directly** - all keywords must be in ${loc} language
|
|
1649
1745
|
|
|
1650
1746
|
`;
|
|
1651
|
-
}
|
|
1652
1747
|
} else if (hasPrimaryResearch) {
|
|
1653
|
-
prompt += `### Locale ${loc}: \u26A0\uFE0F No
|
|
1748
|
+
prompt += `### Locale ${loc}: \u26A0\uFE0F No research found - TRANSLATE from English fallback above
|
|
1654
1749
|
`;
|
|
1655
1750
|
prompt += `No keyword research found at ${researchDir}.
|
|
1656
1751
|
`;
|
|
1657
|
-
prompt += `**
|
|
1658
|
-
|
|
1659
|
-
`;
|
|
1660
|
-
prompt += `**Translation Steps:**
|
|
1661
|
-
`;
|
|
1662
|
-
prompt += `1. Take the Tier 1/2/3 keywords from English research above (${primaryLocale})
|
|
1663
|
-
`;
|
|
1664
|
-
prompt += `2. **TRANSLATE each English keyword into ${loc}** - use natural, native expressions (NOT literal word-for-word translation)
|
|
1665
|
-
`;
|
|
1666
|
-
prompt += `3. Ensure translated keywords are what ${loc} users would actually search for in their language
|
|
1667
|
-
`;
|
|
1668
|
-
prompt += `4. Verify translations are culturally appropriate and contextually relevant
|
|
1669
|
-
`;
|
|
1670
|
-
prompt += `5. Apply the **TRANSLATED** keywords (in ${loc} language) following the same tier strategy
|
|
1671
|
-
`;
|
|
1672
|
-
prompt += `6. **DO NOT use English keywords in ${loc} locale** - all keywords must be in ${loc} language
|
|
1752
|
+
prompt += `**Use the ENGLISH FALLBACK section above** and TRANSLATE all keywords to ${loc}.
|
|
1673
1753
|
|
|
1674
1754
|
`;
|
|
1675
1755
|
} else {
|
|
1676
|
-
prompt += `### Locale ${loc}: \
|
|
1677
|
-
`;
|
|
1678
|
-
prompt += `No keyword research found. Extract keywords from the optimizedPrimary JSON above and **TRANSLATE them to ${loc}**:
|
|
1679
|
-
`;
|
|
1680
|
-
prompt += `1. Extract keywords from \`aso.keywords\` in optimizedPrimary (these are in English/${primaryLocale})
|
|
1681
|
-
`;
|
|
1682
|
-
prompt += `2. **TRANSLATE each English keyword naturally into ${loc}** - use native search expressions
|
|
1683
|
-
`;
|
|
1684
|
-
prompt += `3. Ensure translated keywords match what ${loc} users would actually search for
|
|
1685
|
-
`;
|
|
1686
|
-
prompt += `4. Apply the **TRANSLATED** keywords (in ${loc} language) to all ASO fields
|
|
1756
|
+
prompt += `### Locale ${loc}: \u274C No research available
|
|
1687
1757
|
`;
|
|
1688
|
-
prompt += `
|
|
1758
|
+
prompt += `No keyword research found. Extract keywords from \`aso.keywords\` in optimizedPrimary and **TRANSLATE them to ${loc}**.
|
|
1689
1759
|
|
|
1690
1760
|
`;
|
|
1691
1761
|
}
|
|
@@ -1826,6 +1896,12 @@ ${researchSections.join(
|
|
|
1826
1896
|
`;
|
|
1827
1897
|
} else {
|
|
1828
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
|
+
\`\`\`
|
|
1829
1905
|
|
|
1830
1906
|
`;
|
|
1831
1907
|
}
|
|
@@ -1901,7 +1977,32 @@ ${researchSections.join(
|
|
|
1901
1977
|
prompt += `Repeat for all locales in this batch: ${nonPrimaryLocales.join(
|
|
1902
1978
|
", "
|
|
1903
1979
|
)}
|
|
1980
|
+
|
|
1904
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
|
|
2004
|
+
`;
|
|
2005
|
+
}
|
|
1905
2006
|
return prompt;
|
|
1906
2007
|
}
|
|
1907
2008
|
|
|
@@ -2275,12 +2376,12 @@ This tool returns a PROMPT containing:
|
|
|
2275
2376
|
|
|
2276
2377
|
## STAGES
|
|
2277
2378
|
- **Stage 1:** Primary locale optimization using saved keyword research (ios + android combined)
|
|
2278
|
-
- **Stage 2:** Localize to other languages
|
|
2379
|
+
- **Stage 2:** Localize to other languages - **each locale uses its OWN keyword research**
|
|
2279
2380
|
|
|
2280
|
-
## KEYWORD SOURCES
|
|
2281
|
-
- Uses SAVED keyword research from .aso/keywordResearch/products/[slug]/locales/
|
|
2282
|
-
-
|
|
2283
|
-
-
|
|
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)
|
|
2284
2385
|
|
|
2285
2386
|
**CRITICAL:** Only processes existing locale files. Does NOT create new files.`,
|
|
2286
2387
|
inputSchema: inputSchema3
|
|
@@ -3573,6 +3674,177 @@ Context around ${pos}: ${context}`
|
|
|
3573
3674
|
};
|
|
3574
3675
|
}
|
|
3575
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
|
+
|
|
3576
3848
|
// src/tools/index.ts
|
|
3577
3849
|
var tools = [
|
|
3578
3850
|
{
|
|
@@ -3630,6 +3902,14 @@ var tools = [
|
|
|
3630
3902
|
zodSchema: searchAppInputSchema,
|
|
3631
3903
|
handler: handleSearchApp,
|
|
3632
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"
|
|
3633
3913
|
}
|
|
3634
3914
|
];
|
|
3635
3915
|
function getToolDefinitions() {
|
|
@@ -3640,7 +3920,8 @@ function getToolDefinitions() {
|
|
|
3640
3920
|
initProjectTool,
|
|
3641
3921
|
createBlogHtmlTool,
|
|
3642
3922
|
keywordResearchTool,
|
|
3643
|
-
searchAppTool
|
|
3923
|
+
searchAppTool,
|
|
3924
|
+
validateAsoTool
|
|
3644
3925
|
];
|
|
3645
3926
|
}
|
|
3646
3927
|
function getToolHandler(name) {
|