pabal-resource-mcp 1.10.4 → 1.10.6
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 +119 -36
- package/dist/chunk-H4MYWLFK.js +418 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +1 -1
- package/package.json +2 -1
package/dist/bin/mcp-server.js
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
getPushDataDir,
|
|
15
15
|
loadAsoFromConfig,
|
|
16
16
|
saveAsoToAsoDir
|
|
17
|
-
} from "../chunk-
|
|
17
|
+
} from "../chunk-H4MYWLFK.js";
|
|
18
18
|
import {
|
|
19
19
|
DEFAULT_LOCALE,
|
|
20
20
|
appStoreToUnified,
|
|
@@ -189,7 +189,7 @@ This locale has data from BOTH Google Play and App Store. Use the following merg
|
|
|
189
189
|
- Keep proper nouns and technical terms in English when translating
|
|
190
190
|
- Keep terms like "Always-On Display", "E-Ink", "Android", brand names (e.g., "Boox", "Meebook"), and other technical/proper nouns in English
|
|
191
191
|
- Only translate descriptive text, not technical terminology or proper nouns
|
|
192
|
-
- Example: "Always-On Display" should remain "Always-On Display" in
|
|
192
|
+
- Example: "Always-On Display" should remain "Always-On Display" in translated locales, not translated into a generic phrase like "screen that is always on"
|
|
193
193
|
|
|
194
194
|
${screenshotPaths ? `
|
|
195
195
|
**Screenshot Paths:**
|
|
@@ -358,6 +358,7 @@ import path4 from "path";
|
|
|
358
358
|
|
|
359
359
|
// src/utils/aso-validation.util.ts
|
|
360
360
|
var FIELD_LIMITS_DOC_PATH = "docs/aso/ASO_FIELD_LIMITS.md";
|
|
361
|
+
var ASO_OVERVIEW_DOC_PATH = "docs/aso/ASO_OVERVIEW.md";
|
|
361
362
|
var APP_STORE_LIMITS = {
|
|
362
363
|
name: 30,
|
|
363
364
|
subtitle: 30,
|
|
@@ -542,6 +543,32 @@ function checkKeywordDuplicates(keywords) {
|
|
|
542
543
|
uniqueKeywords
|
|
543
544
|
};
|
|
544
545
|
}
|
|
546
|
+
var normalizeKeyword = (value) => value.trim().toLowerCase();
|
|
547
|
+
function getTitleAndSubtitleKeywordSet(name, subtitle) {
|
|
548
|
+
const sourceText = [name, subtitle].filter((value) => Boolean(value)).join(" ").toLowerCase();
|
|
549
|
+
const terms = sourceText.split(/[^\p{L}\p{N}]+/u).map((term) => term.trim()).filter(Boolean);
|
|
550
|
+
return new Set(terms);
|
|
551
|
+
}
|
|
552
|
+
function validateKeywordRules(keywords, name, subtitle) {
|
|
553
|
+
const duplicateResult = checkKeywordDuplicates(keywords);
|
|
554
|
+
const keywordList = keywords.split(",").map(normalizeKeyword).filter(Boolean);
|
|
555
|
+
const titleAndSubtitleKeywords = getTitleAndSubtitleKeywordSet(
|
|
556
|
+
name,
|
|
557
|
+
subtitle
|
|
558
|
+
);
|
|
559
|
+
const formatting = [];
|
|
560
|
+
if (/\s/.test(keywords)) {
|
|
561
|
+
formatting.push("Use comma-only keywords with no spaces");
|
|
562
|
+
}
|
|
563
|
+
const repeatedFromTitleOrSubtitle = keywordList.filter((keyword) => {
|
|
564
|
+
return titleAndSubtitleKeywords.has(keyword);
|
|
565
|
+
});
|
|
566
|
+
return {
|
|
567
|
+
duplicates: duplicateResult.duplicates,
|
|
568
|
+
formatting,
|
|
569
|
+
repeatedFromTitleOrSubtitle
|
|
570
|
+
};
|
|
571
|
+
}
|
|
545
572
|
function validateKeywords(configData) {
|
|
546
573
|
const issues = [];
|
|
547
574
|
if (configData.appStore) {
|
|
@@ -549,9 +576,14 @@ function validateKeywords(configData) {
|
|
|
549
576
|
const locales = isAppStoreMultilingual(appStoreData) ? appStoreData.locales : { [appStoreData.locale || DEFAULT_LOCALE]: appStoreData };
|
|
550
577
|
for (const [locale, data] of Object.entries(locales)) {
|
|
551
578
|
if (data.keywords) {
|
|
552
|
-
const result =
|
|
553
|
-
|
|
554
|
-
|
|
579
|
+
const result = validateKeywordRules(
|
|
580
|
+
data.keywords,
|
|
581
|
+
data.name,
|
|
582
|
+
data.subtitle
|
|
583
|
+
);
|
|
584
|
+
const hasIssues = result.duplicates.length > 0 || result.formatting.length > 0 || result.repeatedFromTitleOrSubtitle.length > 0;
|
|
585
|
+
if (hasIssues) {
|
|
586
|
+
issues.push({ locale, ...result });
|
|
555
587
|
}
|
|
556
588
|
}
|
|
557
589
|
}
|
|
@@ -597,6 +629,8 @@ function prepareAsoDataForPush(configData) {
|
|
|
597
629
|
const locales = isAppStoreMultilingual(appStoreData) ? appStoreData.locales : { [appStoreData.locale || DEFAULT_LOCALE]: appStoreData };
|
|
598
630
|
const appLevelSupportUrl = isAppStoreMultilingual(appStoreData) ? appStoreData.supportUrl : void 0;
|
|
599
631
|
const appLevelMarketingUrl = isAppStoreMultilingual(appStoreData) ? appStoreData.marketingUrl : void 0;
|
|
632
|
+
const appLevelPrivacyPolicyUrl = isAppStoreMultilingual(appStoreData) ? appStoreData.privacyPolicyUrl : void 0;
|
|
633
|
+
const appLevelTermsUrl = isAppStoreMultilingual(appStoreData) ? appStoreData.termsUrl : void 0;
|
|
600
634
|
const convertedLocales = {};
|
|
601
635
|
for (const [unifiedLocale, localeData] of Object.entries(locales)) {
|
|
602
636
|
const appStoreLocale = unifiedToAppStore(unifiedLocale);
|
|
@@ -605,7 +639,9 @@ function prepareAsoDataForPush(configData) {
|
|
|
605
639
|
...localeData,
|
|
606
640
|
locale: appStoreLocale,
|
|
607
641
|
supportUrl: localeData.supportUrl || appLevelSupportUrl,
|
|
608
|
-
marketingUrl: localeData.marketingUrl || appLevelMarketingUrl
|
|
642
|
+
marketingUrl: localeData.marketingUrl || appLevelMarketingUrl,
|
|
643
|
+
privacyPolicyUrl: localeData.privacyPolicyUrl || appLevelPrivacyPolicyUrl,
|
|
644
|
+
termsUrl: localeData.termsUrl || appLevelTermsUrl
|
|
609
645
|
};
|
|
610
646
|
}
|
|
611
647
|
}
|
|
@@ -890,7 +926,7 @@ This tool:
|
|
|
890
926
|
4. Copies/downloads screenshots to .aso/pushData/products/[slug]/store/screenshots/
|
|
891
927
|
5. Validates text field lengths against ${FIELD_LIMITS_DOC_PATH} (fails if over limits)
|
|
892
928
|
|
|
893
|
-
Before running, review ${FIELD_LIMITS_DOC_PATH} for per-store limits. This prepares data for pushing to stores without actually uploading.`,
|
|
929
|
+
Before running, review ${ASO_OVERVIEW_DOC_PATH} for ASO strategy and ${FIELD_LIMITS_DOC_PATH} for per-store limits. This prepares data for pushing to stores without actually uploading.`,
|
|
894
930
|
inputSchema: inputSchema2
|
|
895
931
|
};
|
|
896
932
|
async function handlePublicToAso(input) {
|
|
@@ -1045,7 +1081,7 @@ ${validationMessage}`
|
|
|
1045
1081
|
responseText += `
|
|
1046
1082
|
Next step: Push to stores using pabal-store-api-mcp's aso-push tool`;
|
|
1047
1083
|
responseText += `
|
|
1048
|
-
|
|
1084
|
+
References: ${ASO_OVERVIEW_DOC_PATH}, ${FIELD_LIMITS_DOC_PATH}`;
|
|
1049
1085
|
if (sanitizeWarnings.length > 0) {
|
|
1050
1086
|
responseText += `
|
|
1051
1087
|
Sanitized invalid characters:
|
|
@@ -1345,7 +1381,11 @@ function generateKeywordSuggestions(args) {
|
|
|
1345
1381
|
`;
|
|
1346
1382
|
suggestions += `6. **template.intro**: Use up to 300 chars to naturally incorporate more keywords and provide richer context
|
|
1347
1383
|
`;
|
|
1348
|
-
suggestions += `7. **App Store Keywords**: 100 char limit, comma-separated, avoid duplicates from name/subtitle
|
|
1384
|
+
suggestions += `7. **App Store Keywords**: 100 char limit, comma-separated with commas only/no spaces, avoid duplicates from name/subtitle, prefer singular forms, order important keywords first, and fill as close to 100 chars as possible with relevant keywords only
|
|
1385
|
+
`;
|
|
1386
|
+
suggestions += `8. **Keyword Selection**: Follow ${ASO_OVERVIEW_DOC_PATH}: popularity >=20 when available, achievable difficulty, beatable top-10 competitors, strong relevance, and target-user search intent
|
|
1387
|
+
`;
|
|
1388
|
+
suggestions += `9. **Field Limits**: Follow ${FIELD_LIMITS_DOC_PATH}
|
|
1349
1389
|
|
|
1350
1390
|
`;
|
|
1351
1391
|
if (category) {
|
|
@@ -1381,7 +1421,7 @@ function formatLocaleSection(args) {
|
|
|
1381
1421
|
const screenshots = landing.screenshots?.images || [];
|
|
1382
1422
|
const features = landing.features?.items || [];
|
|
1383
1423
|
const lengthOf = (value) => value ? value.length : 0;
|
|
1384
|
-
const keywordsLength = Array.isArray(aso.keywords) ? aso.keywords.join(",
|
|
1424
|
+
const keywordsLength = Array.isArray(aso.keywords) ? aso.keywords.join(",").length : lengthOf(typeof aso.keywords === "string" ? aso.keywords : void 0);
|
|
1385
1425
|
const header = `--- ${locale}${locale === primaryLocale ? " (primary)" : ""} ---`;
|
|
1386
1426
|
const stats = [
|
|
1387
1427
|
`Path: public/products/${slug}/locales/${locale}.json`,
|
|
@@ -1438,7 +1478,16 @@ ${json}
|
|
|
1438
1478
|
}
|
|
1439
1479
|
|
|
1440
1480
|
// src/tools/aso/utils/improve/generate-aso-prompt.util.ts
|
|
1441
|
-
var
|
|
1481
|
+
var ASO_RULES_SUMMARY = [
|
|
1482
|
+
`Use ${ASO_OVERVIEW_DOC_PATH} for keyword strategy and ${FIELD_LIMITS_DOC_PATH} for hard limits.`,
|
|
1483
|
+
"`aso.title`: keep app name + the most relevant high-priority keyword, usually `App Name: Primary Keyword`.",
|
|
1484
|
+
"`aso.subtitle`: use important keywords that do not repeat the title.",
|
|
1485
|
+
"`aso.keywords`: comma-separated with commas only, no spaces, no duplicates, no title/subtitle repetition.",
|
|
1486
|
+
"`aso.keywords`: use as much of the 100-character limit as possible with relevant keywords; do not leave meaningful capacity unused and do not use filler.",
|
|
1487
|
+
'`aso.keywords`: split phrase intent into reusable single terms when appropriate, e.g. `sound,relaxing,rain` for "relaxing sound" + "rain sound".',
|
|
1488
|
+
"`aso.keywords`: prefer singular forms, allow real searched misspellings, and order by importance.",
|
|
1489
|
+
"Choose keywords with popularity >=20, achievable difficulty, beatable top-10 competitors, strong relevance, and likely target-user search intent."
|
|
1490
|
+
].join("\n- ");
|
|
1442
1491
|
function generatePrimaryOptimizationPrompt(args) {
|
|
1443
1492
|
const {
|
|
1444
1493
|
slug,
|
|
@@ -1453,6 +1502,12 @@ function generatePrimaryOptimizationPrompt(args) {
|
|
|
1453
1502
|
`;
|
|
1454
1503
|
prompt += `Product: ${slug} | Category: ${category || "N/A"} | Primary: ${primaryLocale}
|
|
1455
1504
|
|
|
1505
|
+
`;
|
|
1506
|
+
prompt += `## ASO Basics
|
|
1507
|
+
|
|
1508
|
+
`;
|
|
1509
|
+
prompt += `- ${ASO_RULES_SUMMARY}
|
|
1510
|
+
|
|
1456
1511
|
`;
|
|
1457
1512
|
prompt += `## Task
|
|
1458
1513
|
|
|
@@ -1510,18 +1565,18 @@ ${researchSections.join("\n")}
|
|
|
1510
1565
|
`;
|
|
1511
1566
|
prompt += `- \`aso.title\` (\u226430): **"App Name: [Tier1 Keyword]"** format
|
|
1512
1567
|
`;
|
|
1513
|
-
prompt += ` - App name in English, keyword in target language
|
|
1568
|
+
prompt += ` - App name in English, keyword in target language with natural casing
|
|
1514
1569
|
`;
|
|
1515
1570
|
prompt += ` - **Do NOT translate/rename the app name**
|
|
1516
1571
|
`;
|
|
1517
|
-
prompt += `- \`aso.subtitle\` (\u226430): Use remaining Tier 1 keywords
|
|
1572
|
+
prompt += `- \`aso.subtitle\` (\u226430): Use remaining Tier 1 keywords without repeating title terms
|
|
1518
1573
|
`;
|
|
1519
1574
|
prompt += `- \`aso.shortDescription\` (\u226480): Tier 1 + Tier 2 keywords (no emojis/CAPS)
|
|
1520
1575
|
|
|
1521
1576
|
`;
|
|
1522
1577
|
prompt += `### Tier 2 Keywords (Feature) \u2192 Keywords Field & Descriptions
|
|
1523
1578
|
`;
|
|
1524
|
-
prompt += `- \`aso.keywords\` (\u2264100): ALL tiers, comma-separated (Tier 1 first, then Tier 2, then Tier 3)
|
|
1579
|
+
prompt += `- \`aso.keywords\` (\u2264100): ALL tiers, comma-separated with commas only and no spaces (Tier 1 first, then Tier 2, then Tier 3); fill as close to 100 chars as possible using relevant keywords only
|
|
1525
1580
|
`;
|
|
1526
1581
|
prompt += `- \`landing.hero.title\`: Tier 1 + Tier 2 keywords
|
|
1527
1582
|
`;
|
|
@@ -1562,11 +1617,15 @@ ${researchSections.join("\n")}
|
|
|
1562
1617
|
prompt += `## Step 3: Validate (after applying all keywords)
|
|
1563
1618
|
|
|
1564
1619
|
`;
|
|
1565
|
-
prompt += `Check all limits using ${
|
|
1620
|
+
prompt += `Check all limits using ${FIELD_LIMITS_DOC_PATH}: title \u226430, subtitle \u226430, shortDescription \u226480, keywords \u2264100, intro \u2264300, outro \u2264200
|
|
1621
|
+
`;
|
|
1622
|
+
prompt += `- Apply ${ASO_OVERVIEW_DOC_PATH}: keyword popularity \u226520 where available, achievable difficulty, relevance, likely user intent, singular forms, important keywords first
|
|
1566
1623
|
`;
|
|
1567
|
-
prompt += `-
|
|
1624
|
+
prompt += `- Maximize keyword field utilization: target 90-100/100 chars when enough relevant keywords exist; explain any lower count
|
|
1568
1625
|
`;
|
|
1569
|
-
prompt += `-
|
|
1626
|
+
prompt += `- Remove keyword duplicates (unique list; avoid repeating title/subtitle terms verbatim; no spaces after commas)
|
|
1627
|
+
`;
|
|
1628
|
+
prompt += `- Ensure App Store/Play Store rules from ${FIELD_LIMITS_DOC_PATH} are satisfied (no disallowed characters/formatting)
|
|
1570
1629
|
|
|
1571
1630
|
`;
|
|
1572
1631
|
prompt += `## Current Data
|
|
@@ -1610,18 +1669,18 @@ ${researchSections.join("\n")}
|
|
|
1610
1669
|
`;
|
|
1611
1670
|
prompt += ` - shortDescription: X/80 \u2713/\u2717
|
|
1612
1671
|
`;
|
|
1613
|
-
prompt += ` - keywords: X/100 \u2713/\u2717 (deduped \u2713/\u2717)
|
|
1672
|
+
prompt += ` - keywords: X/100 \u2713/\u2717 (target 90-100 when possible; deduped \u2713/\u2717)
|
|
1614
1673
|
`;
|
|
1615
1674
|
prompt += ` - intro: X/300 \u2713/\u2717
|
|
1616
1675
|
`;
|
|
1617
1676
|
prompt += ` - outro: X/200 \u2713/\u2717
|
|
1618
1677
|
`;
|
|
1619
|
-
prompt += ` - Store rules (${
|
|
1678
|
+
prompt += ` - Store rules (${FIELD_LIMITS_DOC_PATH}): \u2713/\u2717
|
|
1620
1679
|
`;
|
|
1621
1680
|
prompt += ` - Density: X% (2.5-3%) \u2713/\u2717
|
|
1622
1681
|
|
|
1623
1682
|
`;
|
|
1624
|
-
prompt += `**
|
|
1683
|
+
prompt += `**References**: ${ASO_OVERVIEW_DOC_PATH}, ${FIELD_LIMITS_DOC_PATH}
|
|
1625
1684
|
|
|
1626
1685
|
`;
|
|
1627
1686
|
prompt += `---
|
|
@@ -1665,6 +1724,12 @@ function generateKeywordLocalizationPrompt(args) {
|
|
|
1665
1724
|
", "
|
|
1666
1725
|
)}
|
|
1667
1726
|
|
|
1727
|
+
`;
|
|
1728
|
+
prompt += `## ASO Basics
|
|
1729
|
+
|
|
1730
|
+
`;
|
|
1731
|
+
prompt += `- ${ASO_RULES_SUMMARY}
|
|
1732
|
+
|
|
1668
1733
|
`;
|
|
1669
1734
|
if (batchIndex !== void 0 && totalBatches !== void 0) {
|
|
1670
1735
|
prompt += `**\u26A0\uFE0F BATCH PROCESSING MODE**
|
|
@@ -1694,7 +1759,7 @@ function generateKeywordLocalizationPrompt(args) {
|
|
|
1694
1759
|
`;
|
|
1695
1760
|
prompt += `2. **Replace ONLY keywords with optimized keywords** - keep ALL existing content, structure, tone, and context unchanged. Only swap keywords for better ASO keywords.
|
|
1696
1761
|
`;
|
|
1697
|
-
prompt += `3. After all keywords are applied, validate
|
|
1762
|
+
prompt += `3. After all keywords are applied, validate ASO basics (${ASO_OVERVIEW_DOC_PATH}) + character limits/store rules (${FIELD_LIMITS_DOC_PATH}) + keyword duplication
|
|
1698
1763
|
`;
|
|
1699
1764
|
prompt += `4. **SAVE the updated JSON to file** using the save-locale-file tool (only if file exists)
|
|
1700
1765
|
|
|
@@ -1798,17 +1863,17 @@ ${optimizedPrimary}
|
|
|
1798
1863
|
`;
|
|
1799
1864
|
prompt += ` - App name: **ALWAYS in English** (e.g., "Aurora EOS", "Timeline", "Recaply)
|
|
1800
1865
|
`;
|
|
1801
|
-
prompt += ` - Primary keyword: **In target language** (e.g., "
|
|
1866
|
+
prompt += ` - Primary keyword: **In target language** (e.g., "aurora forecast" for English, "pronostico de auroras" for Spanish)
|
|
1802
1867
|
`;
|
|
1803
|
-
prompt += ` - Example: "Aurora EOS:
|
|
1868
|
+
prompt += ` - Example: "Aurora EOS: Aurora Forecast" (English), "Aurora EOS: Pronostico de Auroras" (Spanish)
|
|
1804
1869
|
`;
|
|
1805
|
-
prompt += ` -
|
|
1870
|
+
prompt += ` - Use natural casing for the target language
|
|
1806
1871
|
`;
|
|
1807
1872
|
prompt += ` - **Do NOT translate/rename the app name**; keep the original English app name across all locales.
|
|
1808
1873
|
`;
|
|
1809
1874
|
prompt += ` - **Only replace the keyword part** - keep the app name and format structure unchanged
|
|
1810
1875
|
`;
|
|
1811
|
-
prompt += `4. Deduplicate keywords: final \`aso.keywords\` must be unique and should not repeat title/subtitle terms verbatim
|
|
1876
|
+
prompt += `4. Deduplicate keywords: final \`aso.keywords\` must be unique, comma-only without spaces, as close to 100 chars as possible, and should not repeat title/subtitle terms verbatim
|
|
1812
1877
|
`;
|
|
1813
1878
|
prompt += `5. **Replace keywords in existing sentences** - swap ONLY the keywords, keep everything else:
|
|
1814
1879
|
`;
|
|
@@ -1850,11 +1915,11 @@ ${optimizedPrimary}
|
|
|
1850
1915
|
`;
|
|
1851
1916
|
prompt += `- Original: "Track aurora with real-time forecasts"
|
|
1852
1917
|
`;
|
|
1853
|
-
prompt += `- Optimized keywords:
|
|
1918
|
+
prompt += `- Optimized keywords: aurora,forecast,real-time
|
|
1854
1919
|
`;
|
|
1855
|
-
prompt += `- Result: "Track
|
|
1920
|
+
prompt += `- Result: "Track aurora with real-time forecasts" (keywords replaced, structure kept)
|
|
1856
1921
|
`;
|
|
1857
|
-
prompt += ` OR: "
|
|
1922
|
+
prompt += ` OR: "Track real-time aurora forecasts" (if natural keyword placement requires minor word order, but keep meaning identical)
|
|
1858
1923
|
|
|
1859
1924
|
`;
|
|
1860
1925
|
prompt += `## Current Translated Locales (This Batch)
|
|
@@ -1895,7 +1960,7 @@ ${optimizedPrimary}
|
|
|
1895
1960
|
`;
|
|
1896
1961
|
prompt += `3. **CRITICAL**: Ensure ALL landing fields are translated (not English)
|
|
1897
1962
|
`;
|
|
1898
|
-
prompt += `4. After swapping keywords, validate
|
|
1963
|
+
prompt += `4. After swapping keywords, validate ASO basics (${ASO_OVERVIEW_DOC_PATH}) + limits/store rules (${FIELD_LIMITS_DOC_PATH}) + keyword utilization (target 90-100/100 when enough relevant keywords exist) + keyword duplication (unique list; avoid repeating title/subtitle terms verbatim; no spaces after commas)
|
|
1899
1964
|
`;
|
|
1900
1965
|
prompt += `5. **SAVE the updated JSON to file** using save-locale-file tool
|
|
1901
1966
|
`;
|
|
@@ -1975,13 +2040,13 @@ ${optimizedPrimary}
|
|
|
1975
2040
|
`;
|
|
1976
2041
|
prompt += ` - shortDescription: X/80 \u2713/\u2717
|
|
1977
2042
|
`;
|
|
1978
|
-
prompt += ` - keywords: X/100 \u2713/\u2717 (deduped \u2713/\u2717; not repeating title/subtitle)
|
|
2043
|
+
prompt += ` - keywords: X/100 \u2713/\u2717 (target 90-100 when possible; deduped \u2713/\u2717; comma-only/no spaces \u2713/\u2717; not repeating title/subtitle)
|
|
1979
2044
|
`;
|
|
1980
2045
|
prompt += ` - intro: X/300 \u2713/\u2717
|
|
1981
2046
|
`;
|
|
1982
2047
|
prompt += ` - outro: X/200 \u2713/\u2717
|
|
1983
2048
|
`;
|
|
1984
|
-
prompt += ` - Store rules (${
|
|
2049
|
+
prompt += ` - Store rules (${FIELD_LIMITS_DOC_PATH}): \u2713/\u2717
|
|
1985
2050
|
|
|
1986
2051
|
`;
|
|
1987
2052
|
prompt += `**4. File Save Confirmation**
|
|
@@ -2382,6 +2447,7 @@ This tool returns a PROMPT containing:
|
|
|
2382
2447
|
- Saved keyword research data (Tier 1/2/3 keywords with traffic/difficulty scores)
|
|
2383
2448
|
- Current locale data
|
|
2384
2449
|
- Optimization instructions
|
|
2450
|
+
- ASO basics from ${ASO_OVERVIEW_DOC_PATH} and field limits from ${FIELD_LIMITS_DOC_PATH}
|
|
2385
2451
|
|
|
2386
2452
|
**YOU MUST:**
|
|
2387
2453
|
1. Read the returned prompt carefully
|
|
@@ -2571,8 +2637,9 @@ var validateAsoTool = {
|
|
|
2571
2637
|
- 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}
|
|
2572
2638
|
- Google Play: title \u2264${GOOGLE_PLAY_LIMITS.title}, shortDescription \u2264${GOOGLE_PLAY_LIMITS.shortDescription}, fullDescription \u2264${GOOGLE_PLAY_LIMITS.fullDescription}
|
|
2573
2639
|
|
|
2574
|
-
2. **Keyword
|
|
2575
|
-
- Checks
|
|
2640
|
+
2. **Keyword Rules** (App Store only):
|
|
2641
|
+
- Checks duplicate keywords, comma-only/no-space formatting, and title/subtitle repetition
|
|
2642
|
+
- Strategy reference: ${ASO_OVERVIEW_DOC_PATH}
|
|
2576
2643
|
|
|
2577
2644
|
3. **Invalid Characters**:
|
|
2578
2645
|
- Control characters, BOM, zero-width/invisible characters, variation selectors
|
|
@@ -2695,10 +2762,26 @@ async function handleValidateAso(input) {
|
|
|
2695
2762
|
const keywordIssues = validateKeywords(dataToValidate);
|
|
2696
2763
|
const filteredKeywordIssues = locale ? keywordIssues.filter((issue) => issue.locale === locale) : keywordIssues;
|
|
2697
2764
|
if (filteredKeywordIssues.length > 0) {
|
|
2698
|
-
results.push(`## Keyword
|
|
2765
|
+
results.push(`## Keyword Rule Violations
|
|
2699
2766
|
`);
|
|
2700
2767
|
for (const issue of filteredKeywordIssues) {
|
|
2701
|
-
|
|
2768
|
+
if (issue.duplicates.length > 0) {
|
|
2769
|
+
results.push(
|
|
2770
|
+
`- [${issue.locale}] duplicates: ${issue.duplicates.join(", ")}`
|
|
2771
|
+
);
|
|
2772
|
+
}
|
|
2773
|
+
if (issue.formatting.length > 0) {
|
|
2774
|
+
results.push(
|
|
2775
|
+
`- [${issue.locale}] formatting: ${issue.formatting.join(", ")}`
|
|
2776
|
+
);
|
|
2777
|
+
}
|
|
2778
|
+
if (issue.repeatedFromTitleOrSubtitle.length > 0) {
|
|
2779
|
+
results.push(
|
|
2780
|
+
`- [${issue.locale}] repeats title/subtitle: ${issue.repeatedFromTitleOrSubtitle.join(
|
|
2781
|
+
", "
|
|
2782
|
+
)}`
|
|
2783
|
+
);
|
|
2784
|
+
}
|
|
2702
2785
|
}
|
|
2703
2786
|
results.push("");
|
|
2704
2787
|
}
|
|
@@ -2713,7 +2796,7 @@ async function handleValidateAso(input) {
|
|
|
2713
2796
|
`\u274C **Validation failed** - Fix the issues above before pushing to stores.`
|
|
2714
2797
|
);
|
|
2715
2798
|
results.push(`
|
|
2716
|
-
|
|
2799
|
+
References: ${ASO_OVERVIEW_DOC_PATH}, ${FIELD_LIMITS_DOC_PATH}`);
|
|
2717
2800
|
} else if (hasSanitizeWarnings && !fix) {
|
|
2718
2801
|
results.push(
|
|
2719
2802
|
`\u26A0\uFE0F **Invalid characters detected** - Run with \`fix: true\` to auto-remove.`
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_LOCALE,
|
|
3
|
+
isAppStoreLocale,
|
|
4
|
+
isGooglePlayLocale,
|
|
5
|
+
isSupportedLocale
|
|
6
|
+
} from "./chunk-BOWRBVVV.js";
|
|
7
|
+
|
|
8
|
+
// src/utils/config.util.ts
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import os from "os";
|
|
12
|
+
function getAsoDataDir() {
|
|
13
|
+
const configPath = path.join(
|
|
14
|
+
os.homedir(),
|
|
15
|
+
".config",
|
|
16
|
+
"pabal-mcp",
|
|
17
|
+
"config.json"
|
|
18
|
+
);
|
|
19
|
+
if (!fs.existsSync(configPath)) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`Config file not found at ${configPath}. Please create the config file and set the 'dataDir' property to specify the ASO data directory.`
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const configContent = fs.readFileSync(configPath, "utf-8");
|
|
26
|
+
const config = JSON.parse(configContent);
|
|
27
|
+
if (!config.dataDir) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`'dataDir' property is not set in ${configPath}. Please set 'dataDir' to specify the ASO data directory.`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
if (path.isAbsolute(config.dataDir)) {
|
|
33
|
+
return config.dataDir;
|
|
34
|
+
}
|
|
35
|
+
return path.resolve(os.homedir(), config.dataDir);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (error instanceof Error && error.message.includes("dataDir")) {
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Failed to read config from ${configPath}: ${error instanceof Error ? error.message : String(error)}`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function getPullDataDir() {
|
|
46
|
+
return path.join(getAsoDataDir(), ".aso", "pullData");
|
|
47
|
+
}
|
|
48
|
+
function getPushDataDir() {
|
|
49
|
+
return path.join(getAsoDataDir(), ".aso", "pushData");
|
|
50
|
+
}
|
|
51
|
+
function getPublicDir() {
|
|
52
|
+
return path.join(getAsoDataDir(), "public");
|
|
53
|
+
}
|
|
54
|
+
function getKeywordResearchDir() {
|
|
55
|
+
return path.join(getAsoDataDir(), ".aso", "keywordResearch");
|
|
56
|
+
}
|
|
57
|
+
function getProductsDir() {
|
|
58
|
+
return path.join(getPublicDir(), "products");
|
|
59
|
+
}
|
|
60
|
+
function loadConfig() {
|
|
61
|
+
const configPath = path.join(
|
|
62
|
+
os.homedir(),
|
|
63
|
+
".config",
|
|
64
|
+
"pabal-mcp",
|
|
65
|
+
"config.json"
|
|
66
|
+
);
|
|
67
|
+
if (!fs.existsSync(configPath)) {
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const configContent = fs.readFileSync(configPath, "utf-8");
|
|
72
|
+
return JSON.parse(configContent);
|
|
73
|
+
} catch {
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function getGeminiApiKey() {
|
|
78
|
+
const config = loadConfig();
|
|
79
|
+
if (config.gemini?.apiKey) {
|
|
80
|
+
return config.gemini.apiKey;
|
|
81
|
+
}
|
|
82
|
+
const envKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
|
|
83
|
+
if (envKey) {
|
|
84
|
+
return envKey;
|
|
85
|
+
}
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Gemini API key not found. Set it in ~/.config/pabal-mcp/config.json under "gemini.apiKey" or use GEMINI_API_KEY environment variable.`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/utils/aso-converter.ts
|
|
92
|
+
import fs2 from "fs";
|
|
93
|
+
import path2 from "path";
|
|
94
|
+
function generateFullDescription(localeData, metadata = {}) {
|
|
95
|
+
const { aso, landing } = localeData;
|
|
96
|
+
const template = aso?.template;
|
|
97
|
+
if (!template) {
|
|
98
|
+
return "";
|
|
99
|
+
}
|
|
100
|
+
const landingFeatures = landing?.features?.items || [];
|
|
101
|
+
const landingScreenshots = landing?.screenshots?.images || [];
|
|
102
|
+
const keyHeading = template.keyFeaturesHeading || "Key Features";
|
|
103
|
+
const featuresHeading = template.featuresHeading || "Additional Features";
|
|
104
|
+
const parts = [template.intro];
|
|
105
|
+
if (landingFeatures.length > 0) {
|
|
106
|
+
parts.push(
|
|
107
|
+
"",
|
|
108
|
+
keyHeading,
|
|
109
|
+
"",
|
|
110
|
+
...landingFeatures.map(
|
|
111
|
+
(feature) => [`\u25B6\uFE0E ${feature.title}`, feature.body || ""].filter(Boolean).join("\n")
|
|
112
|
+
)
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
if (landingScreenshots.length > 0) {
|
|
116
|
+
parts.push("", featuresHeading, "");
|
|
117
|
+
parts.push(
|
|
118
|
+
...landingScreenshots.map(
|
|
119
|
+
(screenshot) => [`\u25B6\uFE0E ${screenshot.title}`, screenshot.description || ""].filter(Boolean).join("\n")
|
|
120
|
+
)
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
parts.push("", template.outro);
|
|
124
|
+
const includeSupport = template.includeSupportLinks ?? true;
|
|
125
|
+
if (includeSupport) {
|
|
126
|
+
const contactLines = [
|
|
127
|
+
metadata.instagram ? `Instagram: ${metadata.instagram}` : null,
|
|
128
|
+
metadata.contactEmail ? `Email: ${metadata.contactEmail}` : null,
|
|
129
|
+
metadata.termsUrl ? `- Terms of Use: ${metadata.termsUrl}` : null,
|
|
130
|
+
metadata.privacyUrl ? `- Privacy Policy: ${metadata.privacyUrl}` : null
|
|
131
|
+
].filter((line) => line !== null);
|
|
132
|
+
if (contactLines.length > 0) {
|
|
133
|
+
parts.push("", "[Contact & Support]", "", ...contactLines);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return parts.join("\n");
|
|
137
|
+
}
|
|
138
|
+
function loadAsoFromConfig(slug) {
|
|
139
|
+
const productsDir = getProductsDir();
|
|
140
|
+
const configPath = path2.join(productsDir, slug, "config.json");
|
|
141
|
+
console.debug(`[loadAsoFromConfig] Looking for ${slug}:`);
|
|
142
|
+
console.debug(` - productsDir: ${productsDir}`);
|
|
143
|
+
console.debug(` - configPath: ${configPath}`);
|
|
144
|
+
console.debug(` - configPath exists: ${fs2.existsSync(configPath)}`);
|
|
145
|
+
if (!fs2.existsSync(configPath)) {
|
|
146
|
+
console.warn(`[loadAsoFromConfig] Config file not found at ${configPath}`);
|
|
147
|
+
return {};
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const configContent = fs2.readFileSync(configPath, "utf-8");
|
|
151
|
+
const config = JSON.parse(configContent);
|
|
152
|
+
const localesDir = path2.join(productsDir, slug, "locales");
|
|
153
|
+
console.debug(` - localesDir: ${localesDir}`);
|
|
154
|
+
console.debug(` - localesDir exists: ${fs2.existsSync(localesDir)}`);
|
|
155
|
+
if (!fs2.existsSync(localesDir)) {
|
|
156
|
+
console.warn(
|
|
157
|
+
`[loadAsoFromConfig] Locales directory not found at ${localesDir}`
|
|
158
|
+
);
|
|
159
|
+
return {};
|
|
160
|
+
}
|
|
161
|
+
const localeFiles = fs2.readdirSync(localesDir).filter((f) => f.endsWith(".json"));
|
|
162
|
+
const locales = {};
|
|
163
|
+
for (const file of localeFiles) {
|
|
164
|
+
const localeCode = file.replace(".json", "");
|
|
165
|
+
const localePath = path2.join(localesDir, file);
|
|
166
|
+
const localeContent = fs2.readFileSync(localePath, "utf-8");
|
|
167
|
+
locales[localeCode] = JSON.parse(localeContent);
|
|
168
|
+
}
|
|
169
|
+
console.debug(
|
|
170
|
+
` - Found ${Object.keys(locales).length} locale file(s): ${Object.keys(
|
|
171
|
+
locales
|
|
172
|
+
).join(", ")}`
|
|
173
|
+
);
|
|
174
|
+
if (Object.keys(locales).length === 0) {
|
|
175
|
+
console.warn(
|
|
176
|
+
`[loadAsoFromConfig] No locale files found in ${localesDir}`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
const defaultLocale = config.content?.defaultLocale || DEFAULT_LOCALE;
|
|
180
|
+
const asoData = {};
|
|
181
|
+
if (config.packageName) {
|
|
182
|
+
const googlePlayLocales = {};
|
|
183
|
+
const metadata = config.metadata || {};
|
|
184
|
+
const screenshots = metadata.screenshots || {};
|
|
185
|
+
for (const [locale, localeData] of Object.entries(locales)) {
|
|
186
|
+
if (!isSupportedLocale(locale)) {
|
|
187
|
+
console.debug(
|
|
188
|
+
`Skipping locale ${locale} - not a valid unified locale`
|
|
189
|
+
);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (!isGooglePlayLocale(locale)) {
|
|
193
|
+
console.debug(
|
|
194
|
+
`Skipping locale ${locale} - not supported by Google Play`
|
|
195
|
+
);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const aso = localeData.aso || {};
|
|
199
|
+
if (!aso || !aso.title && !aso.shortDescription) {
|
|
200
|
+
console.warn(
|
|
201
|
+
`Locale ${locale} has no ASO data (title or shortDescription)`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
const screenshotsDir = path2.join(
|
|
205
|
+
productsDir,
|
|
206
|
+
slug,
|
|
207
|
+
"screenshots",
|
|
208
|
+
locale
|
|
209
|
+
);
|
|
210
|
+
const phoneDir = path2.join(screenshotsDir, "phone");
|
|
211
|
+
const tabletDir = path2.join(screenshotsDir, "tablet");
|
|
212
|
+
const featureGraphicPath = path2.join(
|
|
213
|
+
screenshotsDir,
|
|
214
|
+
"feature-graphic.png"
|
|
215
|
+
);
|
|
216
|
+
const hasPhoneScreenshots = fs2.existsSync(phoneDir);
|
|
217
|
+
const hasTabletScreenshots = fs2.existsSync(tabletDir);
|
|
218
|
+
const hasFeatureGraphic = fs2.existsSync(featureGraphicPath);
|
|
219
|
+
const localeScreenshots = {
|
|
220
|
+
phone: hasPhoneScreenshots ? screenshots.phone?.map(
|
|
221
|
+
(p) => p.replace(/\/screenshots\/[^/]+\//, `/screenshots/${locale}/`)
|
|
222
|
+
) : void 0,
|
|
223
|
+
tablet: hasTabletScreenshots ? screenshots.tablet?.map(
|
|
224
|
+
(p) => p.replace(/\/screenshots\/[^/]+\//, `/screenshots/${locale}/`)
|
|
225
|
+
) : void 0
|
|
226
|
+
};
|
|
227
|
+
googlePlayLocales[locale] = {
|
|
228
|
+
title: aso.title || "",
|
|
229
|
+
shortDescription: aso.shortDescription || "",
|
|
230
|
+
fullDescription: generateFullDescription(localeData, metadata),
|
|
231
|
+
packageName: config.packageName,
|
|
232
|
+
defaultLanguage: locale,
|
|
233
|
+
screenshots: {
|
|
234
|
+
phone: localeScreenshots.phone || [],
|
|
235
|
+
tablet: localeScreenshots.tablet
|
|
236
|
+
},
|
|
237
|
+
featureGraphic: hasFeatureGraphic ? `/products/${slug}/screenshots/${locale}/feature-graphic.png` : metadata.featureGraphic
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
const googleLocaleKeys = Object.keys(googlePlayLocales);
|
|
241
|
+
if (googleLocaleKeys.length > 0) {
|
|
242
|
+
const hasConfigDefault = isGooglePlayLocale(defaultLocale) && Boolean(googlePlayLocales[defaultLocale]);
|
|
243
|
+
const resolvedDefault = hasConfigDefault ? defaultLocale : googlePlayLocales[DEFAULT_LOCALE] ? DEFAULT_LOCALE : googleLocaleKeys[0];
|
|
244
|
+
asoData.googlePlay = {
|
|
245
|
+
locales: googlePlayLocales,
|
|
246
|
+
defaultLocale: resolvedDefault,
|
|
247
|
+
// App-level contact information
|
|
248
|
+
contactEmail: metadata.contactEmail,
|
|
249
|
+
contactWebsite: metadata.supportUrl,
|
|
250
|
+
youtubeUrl: metadata.youtubeUrl
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (config.bundleId) {
|
|
255
|
+
const appStoreLocales = {};
|
|
256
|
+
const metadata = config.metadata || {};
|
|
257
|
+
const screenshots = metadata.screenshots || {};
|
|
258
|
+
for (const [locale, localeData] of Object.entries(locales)) {
|
|
259
|
+
if (!isSupportedLocale(locale)) {
|
|
260
|
+
console.debug(
|
|
261
|
+
`Skipping locale ${locale} - not a valid unified locale`
|
|
262
|
+
);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (!isAppStoreLocale(locale)) {
|
|
266
|
+
console.debug(
|
|
267
|
+
`Skipping locale ${locale} - not supported by App Store`
|
|
268
|
+
);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const aso = localeData.aso || {};
|
|
272
|
+
if (!aso || !aso.title && !aso.shortDescription) {
|
|
273
|
+
console.warn(
|
|
274
|
+
`Locale ${locale} has no ASO data (title or shortDescription)`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
const screenshotsDir = path2.join(
|
|
278
|
+
productsDir,
|
|
279
|
+
slug,
|
|
280
|
+
"screenshots",
|
|
281
|
+
locale
|
|
282
|
+
);
|
|
283
|
+
const phoneDir = path2.join(screenshotsDir, "phone");
|
|
284
|
+
const tabletDir = path2.join(screenshotsDir, "tablet");
|
|
285
|
+
const hasPhoneScreenshots = fs2.existsSync(phoneDir);
|
|
286
|
+
const hasTabletScreenshots = fs2.existsSync(tabletDir);
|
|
287
|
+
const localeScreenshots = {
|
|
288
|
+
phone: hasPhoneScreenshots ? screenshots.phone?.map(
|
|
289
|
+
(p) => p.replace(/\/screenshots\/[^/]+\//, `/screenshots/${locale}/`)
|
|
290
|
+
) : void 0,
|
|
291
|
+
tablet: hasTabletScreenshots ? screenshots.tablet?.map(
|
|
292
|
+
(p) => p.replace(/\/screenshots\/[^/]+\//, `/screenshots/${locale}/`)
|
|
293
|
+
) : void 0
|
|
294
|
+
};
|
|
295
|
+
appStoreLocales[locale] = {
|
|
296
|
+
name: aso.title || "",
|
|
297
|
+
subtitle: aso.subtitle,
|
|
298
|
+
description: generateFullDescription(localeData, metadata),
|
|
299
|
+
keywords: Array.isArray(aso.keywords) ? aso.keywords.join(",") : aso.keywords,
|
|
300
|
+
promotionalText: void 0,
|
|
301
|
+
bundleId: config.bundleId,
|
|
302
|
+
locale,
|
|
303
|
+
screenshots: {
|
|
304
|
+
// Map phone screenshots to iphone65
|
|
305
|
+
iphone65: localeScreenshots.phone || [],
|
|
306
|
+
// Map tablet screenshots to ipadPro129
|
|
307
|
+
ipadPro129: localeScreenshots.tablet
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
const appStoreLocaleKeys = Object.keys(appStoreLocales);
|
|
312
|
+
if (appStoreLocaleKeys.length > 0) {
|
|
313
|
+
const hasConfigDefault = isAppStoreLocale(defaultLocale) && Boolean(appStoreLocales[defaultLocale]);
|
|
314
|
+
const resolvedDefault = hasConfigDefault ? defaultLocale : appStoreLocales[DEFAULT_LOCALE] ? DEFAULT_LOCALE : appStoreLocaleKeys[0];
|
|
315
|
+
asoData.appStore = {
|
|
316
|
+
locales: appStoreLocales,
|
|
317
|
+
defaultLocale: resolvedDefault,
|
|
318
|
+
// App-level contact information
|
|
319
|
+
contactEmail: metadata.contactEmail,
|
|
320
|
+
supportUrl: metadata.supportUrl,
|
|
321
|
+
marketingUrl: metadata.marketingUrl,
|
|
322
|
+
privacyPolicyUrl: metadata.privacyUrl,
|
|
323
|
+
termsUrl: metadata.termsUrl
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const hasGooglePlay = !!asoData.googlePlay;
|
|
328
|
+
const hasAppStore = !!asoData.appStore;
|
|
329
|
+
console.debug(`[loadAsoFromConfig] Result for ${slug}:`);
|
|
330
|
+
console.debug(
|
|
331
|
+
` - Google Play data: ${hasGooglePlay ? "found" : "not found"}`
|
|
332
|
+
);
|
|
333
|
+
console.debug(` - App Store data: ${hasAppStore ? "found" : "not found"}`);
|
|
334
|
+
if (!hasGooglePlay && !hasAppStore) {
|
|
335
|
+
console.warn(`[loadAsoFromConfig] No ASO data generated for ${slug}`);
|
|
336
|
+
}
|
|
337
|
+
return asoData;
|
|
338
|
+
} catch (error) {
|
|
339
|
+
console.error(
|
|
340
|
+
`[loadAsoFromConfig] Failed to load ASO data from config for ${slug}:`,
|
|
341
|
+
error
|
|
342
|
+
);
|
|
343
|
+
return {};
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function saveAsoToConfig(slug, config) {
|
|
347
|
+
const productsDir = getProductsDir();
|
|
348
|
+
const configPath = path2.join(productsDir, slug, "config.json");
|
|
349
|
+
fs2.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
350
|
+
}
|
|
351
|
+
function saveAsoToAsoDir(slug, asoData) {
|
|
352
|
+
const rootDir = getPushDataDir();
|
|
353
|
+
if (asoData.googlePlay) {
|
|
354
|
+
const asoPath = path2.join(
|
|
355
|
+
rootDir,
|
|
356
|
+
"products",
|
|
357
|
+
slug,
|
|
358
|
+
"store",
|
|
359
|
+
"google-play",
|
|
360
|
+
"aso-data.json"
|
|
361
|
+
);
|
|
362
|
+
const dir = path2.dirname(asoPath);
|
|
363
|
+
if (!fs2.existsSync(dir)) {
|
|
364
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
365
|
+
}
|
|
366
|
+
const googlePlayData = asoData.googlePlay;
|
|
367
|
+
const multilingualData = "locales" in googlePlayData ? googlePlayData : {
|
|
368
|
+
locales: {
|
|
369
|
+
[googlePlayData.defaultLanguage || DEFAULT_LOCALE]: googlePlayData
|
|
370
|
+
},
|
|
371
|
+
defaultLocale: googlePlayData.defaultLanguage || DEFAULT_LOCALE
|
|
372
|
+
};
|
|
373
|
+
fs2.writeFileSync(
|
|
374
|
+
asoPath,
|
|
375
|
+
JSON.stringify({ googlePlay: multilingualData }, null, 2) + "\n",
|
|
376
|
+
"utf-8"
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
if (asoData.appStore) {
|
|
380
|
+
const asoPath = path2.join(
|
|
381
|
+
rootDir,
|
|
382
|
+
"products",
|
|
383
|
+
slug,
|
|
384
|
+
"store",
|
|
385
|
+
"app-store",
|
|
386
|
+
"aso-data.json"
|
|
387
|
+
);
|
|
388
|
+
const dir = path2.dirname(asoPath);
|
|
389
|
+
if (!fs2.existsSync(dir)) {
|
|
390
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
391
|
+
}
|
|
392
|
+
const appStoreData = asoData.appStore;
|
|
393
|
+
const multilingualData = "locales" in appStoreData ? appStoreData : {
|
|
394
|
+
locales: {
|
|
395
|
+
[appStoreData.locale || DEFAULT_LOCALE]: appStoreData
|
|
396
|
+
},
|
|
397
|
+
defaultLocale: appStoreData.locale || DEFAULT_LOCALE
|
|
398
|
+
};
|
|
399
|
+
fs2.writeFileSync(
|
|
400
|
+
asoPath,
|
|
401
|
+
JSON.stringify({ appStore: multilingualData }, null, 2) + "\n",
|
|
402
|
+
"utf-8"
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export {
|
|
408
|
+
getAsoDataDir,
|
|
409
|
+
getPullDataDir,
|
|
410
|
+
getPushDataDir,
|
|
411
|
+
getPublicDir,
|
|
412
|
+
getKeywordResearchDir,
|
|
413
|
+
getProductsDir,
|
|
414
|
+
getGeminiApiKey,
|
|
415
|
+
loadAsoFromConfig,
|
|
416
|
+
saveAsoToConfig,
|
|
417
|
+
saveAsoToAsoDir
|
|
418
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -6,20 +6,20 @@ import 'appstore-connect-sdk/openapi';
|
|
|
6
6
|
/**
|
|
7
7
|
* ASO Data Converter
|
|
8
8
|
*
|
|
9
|
-
* config.json (source of truth)
|
|
9
|
+
* Converts between config.json (source of truth) and aso-data.json (build artifact).
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* Read ASO data from config.json.
|
|
14
|
+
* New structure: config.json (metadata) + locales/{locale}.json (content)
|
|
15
15
|
*/
|
|
16
16
|
declare function loadAsoFromConfig(slug: string): AsoData;
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
18
|
+
* Save ASO data to config.json.
|
|
19
19
|
*/
|
|
20
20
|
declare function saveAsoToConfig(slug: string, config: ProductConfig): void;
|
|
21
21
|
/**
|
|
22
|
-
* ASO
|
|
22
|
+
* Save ASO data to the configured ASO directory.
|
|
23
23
|
*/
|
|
24
24
|
declare function saveAsoToAsoDir(slug: string, asoData: AsoData): void;
|
|
25
25
|
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pabal-resource-mcp",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MCP server for ASO data management with shared types and utilities",
|
|
6
6
|
"author": "skyu",
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"scripts": {
|
|
58
58
|
"build": "tsup src/index.ts src/browser.ts src/bin/mcp-server.ts --format esm --dts --external node:fs --external node:path --external node:os",
|
|
59
59
|
"dev": "tsx src/bin/mcp-server.ts",
|
|
60
|
+
"test": "tsx --test \"tests/**/*.test.ts\"",
|
|
60
61
|
"typecheck": "tsc --noEmit",
|
|
61
62
|
"prepublishOnly": "npm run build"
|
|
62
63
|
},
|