pabal-web-mcp 1.3.10 → 1.3.12

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.
@@ -1215,13 +1215,30 @@ function generatePrimaryOptimizationPrompt(args) {
1215
1215
  prompt += `Optimize the PRIMARY locale (${primaryLocale}) using **saved keyword research** + full ASO field optimization.
1216
1216
 
1217
1217
  `;
1218
- prompt += `## Step 1: Keyword Research (${primaryLocale})
1218
+ prompt += `## Step 1: Use Saved Keyword Research (${primaryLocale})
1219
1219
 
1220
1220
  `;
1221
1221
  const researchSections = keywordResearchByLocale[primaryLocale] || [];
1222
1222
  const researchDir = keywordResearchDirByLocale[primaryLocale];
1223
1223
  if (researchSections.length > 0) {
1224
- prompt += `Use the **saved keyword research below**. Do NOT invent new keywords. Choose the top 10 from the recommended set.
1224
+ prompt += `**CRITICAL: Use ONLY the saved keyword research below. Do NOT invent or research new keywords.**
1225
+
1226
+ `;
1227
+ prompt += `The research data includes:
1228
+ `;
1229
+ prompt += `- **Tier 1 (Core):** Use these in title and subtitle - highest traffic, best opportunity
1230
+ `;
1231
+ prompt += `- **Tier 2 (Feature):** Use these in keywords field and descriptions
1232
+ `;
1233
+ prompt += `- **Tier 3 (Longtail):** Use these in intro, outro, and feature descriptions
1234
+ `;
1235
+ prompt += `- **Keyword Details:** Each keyword has traffic/difficulty scores and rationale - use this to prioritize
1236
+ `;
1237
+ prompt += `- **Strategy:** Overall optimization strategy based on competitor analysis
1238
+ `;
1239
+ prompt += `- **Keyword Gaps:** Opportunities where competitors are weak
1240
+ `;
1241
+ prompt += `- **User Language Patterns:** Phrases real users use in reviews - incorporate naturally
1225
1242
 
1226
1243
  `;
1227
1244
  prompt += `Saved research:
@@ -1238,46 +1255,60 @@ ${researchSections.join("\n")}
1238
1255
  prompt += `## Step 2: Optimize All Fields (${primaryLocale})
1239
1256
 
1240
1257
  `;
1241
- prompt += `Apply the selected keywords to ALL fields:
1258
+ prompt += `**Apply keywords strategically based on tier priority:**
1259
+
1242
1260
  `;
1243
- prompt += `- \`aso.title\` (\u226430): **"App Name: Primary Keyword"** format (app name in English, keyword in target language, keyword starts with uppercase after the colon)
1261
+ prompt += `### Tier 1 Keywords (Core) \u2192 Title & Subtitle
1244
1262
  `;
1245
- prompt += ` - **Do NOT translate/rename the app name**; keep the original English app name across all locales.
1263
+ prompt += `- \`aso.title\` (\u226430): **"App Name: [Tier1 Keyword]"** format
1246
1264
  `;
1247
- prompt += `- \`aso.subtitle\` (\u226430): Complementary keywords
1265
+ prompt += ` - App name in English, keyword in target language, uppercase after colon
1248
1266
  `;
1249
- prompt += `- \`aso.shortDescription\` (\u226480): Primary keywords (no emojis/CAPS)
1267
+ prompt += ` - **Do NOT translate/rename the app name**
1250
1268
  `;
1251
- prompt += `- \`aso.keywords\` (\u2264100): Comma-separated 10 keywords
1269
+ prompt += `- \`aso.subtitle\` (\u226430): Use remaining Tier 1 keywords
1252
1270
  `;
1253
- prompt += `- \`aso.template.intro\` (\u2264300): Keyword-rich, use full length
1271
+ prompt += `- \`aso.shortDescription\` (\u226480): Tier 1 + Tier 2 keywords (no emojis/CAPS)
1272
+
1254
1273
  `;
1255
- prompt += `- \`aso.template.outro\` (\u2264200): Natural keyword integration
1274
+ prompt += `### Tier 2 Keywords (Feature) \u2192 Keywords Field & Descriptions
1256
1275
  `;
1257
- prompt += `- \`landing.hero.title\`: Primary keywords
1276
+ prompt += `- \`aso.keywords\` (\u2264100): ALL tiers, comma-separated (Tier 1 first, then Tier 2, then Tier 3)
1258
1277
  `;
1259
- prompt += `- \`landing.hero.description\`: Keywords if present
1278
+ prompt += `- \`landing.hero.title\`: Tier 1 + Tier 2 keywords
1260
1279
  `;
1261
- prompt += `- \`landing.screenshots.images[].title\`: Keywords in screenshot titles
1280
+ prompt += `- \`landing.hero.description\`: Tier 2 keywords naturally integrated
1281
+ `;
1282
+ prompt += `- \`landing.screenshots.images[].title\`: Tier 2 keywords
1283
+ `;
1284
+ prompt += `- \`landing.screenshots.images[].description\`: Tier 2 + Tier 3 keywords
1285
+
1262
1286
  `;
1263
- prompt += `- \`landing.screenshots.images[].description\`: Keywords in screenshot descriptions
1287
+ prompt += `### Tier 3 Keywords (Longtail) \u2192 Content Sections
1264
1288
  `;
1265
- prompt += `- \`landing.features.items[].title\`: Keywords in feature titles
1289
+ prompt += `- \`aso.template.intro\` (\u2264300): Tier 2 + Tier 3 keywords, keyword-rich, use full length
1266
1290
  `;
1267
- prompt += `- \`landing.features.items[].body\`: Keywords in feature descriptions
1291
+ prompt += `- \`aso.template.outro\` (\u2264200): Tier 3 keywords, natural integration
1268
1292
  `;
1269
- prompt += `- \`landing.reviews.title\`: Keywords if applicable
1293
+ prompt += `- \`landing.features.items[].title\`: Tier 2 keywords
1270
1294
  `;
1271
- prompt += `- \`landing.reviews.description\`: Keywords if applicable
1295
+ prompt += `- \`landing.features.items[].body\`: Tier 3 keywords with user language patterns
1272
1296
  `;
1273
- prompt += `- \`landing.cta.headline\`: Keywords if applicable
1297
+ prompt += `- \`landing.reviews.title/description\`: Keywords if applicable
1274
1298
  `;
1275
- prompt += `- \`landing.cta.description\`: Keywords if applicable
1299
+ prompt += `- \`landing.cta.headline/description\`: Keywords if applicable
1300
+
1301
+ `;
1302
+ prompt += `### User Language Integration
1303
+ `;
1304
+ prompt += `- Use **User Language Patterns** from research in intro/outro/features
1305
+ `;
1306
+ prompt += `- These are actual phrases users search for - incorporate naturally
1276
1307
 
1277
1308
  `;
1278
1309
  prompt += `**Guidelines**: 2.5-3% keyword density, natural flow, cultural appropriateness
1279
1310
  `;
1280
- prompt += `**CRITICAL**: You MUST include the complete \`landing\` object in your optimized JSON output, with all screenshots, features, reviews, and cta sections properly translated and keyword-optimized.
1311
+ prompt += `**CRITICAL**: You MUST include the complete \`landing\` object in your optimized JSON output.
1281
1312
 
1282
1313
  `;
1283
1314
  prompt += `## Step 3: Validate
@@ -1410,21 +1441,60 @@ ${optimizedPrimary}
1410
1441
  \`\`\`
1411
1442
 
1412
1443
  `;
1444
+ const primaryResearchSections = keywordResearchByLocale[primaryLocale] || [];
1445
+ const hasPrimaryResearch = primaryResearchSections.length > 0;
1413
1446
  prompt += `## Keyword Research (Per Locale)
1414
1447
 
1415
1448
  `;
1449
+ if (hasPrimaryResearch) {
1450
+ prompt += `**\u{1F4DA} ENGLISH (${primaryLocale}) Keywords - Use as fallback for locales without research:**
1451
+ ${primaryResearchSections.join("\n")}
1452
+
1453
+ `;
1454
+ prompt += `---
1455
+
1456
+ `;
1457
+ }
1416
1458
  nonPrimaryLocales.forEach((loc) => {
1417
1459
  const researchSections = keywordResearchByLocale[loc] || [];
1418
1460
  const researchDir = keywordResearchDirByLocale[loc];
1419
1461
  if (researchSections.length > 0) {
1420
- prompt += `Locale ${loc}: use saved research below. Do NOT invent keywords.
1462
+ prompt += `### Locale ${loc}: \u2705 Saved research found
1421
1463
  ${researchSections.join(
1422
1464
  "\n"
1423
1465
  )}
1424
1466
 
1467
+ `;
1468
+ } else if (hasPrimaryResearch) {
1469
+ prompt += `### Locale ${loc}: \u26A0\uFE0F No saved research - USE ENGLISH (${primaryLocale}) KEYWORDS
1470
+ `;
1471
+ prompt += `No keyword research found at ${researchDir}.
1472
+ `;
1473
+ prompt += `**FALLBACK:** Translate English keywords from primary locale (${primaryLocale}) into ${loc}:
1474
+ `;
1475
+ prompt += `1. Take the Tier 1/2/3 keywords from English research above
1476
+ `;
1477
+ prompt += `2. Translate each English keyword naturally into ${loc} (not literal translation)
1478
+ `;
1479
+ prompt += `3. Use native expressions that ${loc} users would actually search for
1480
+ `;
1481
+ prompt += `4. Verify translations are culturally appropriate
1482
+ `;
1483
+ prompt += `5. Apply translated keywords following the same tier strategy
1484
+
1425
1485
  `;
1426
1486
  } else {
1427
- prompt += `Locale ${loc}: no saved keyword research found at ${researchDir}. Stop and request running 'keyword-research' tool (slug='${slug}', locale='${loc}', platform/country as appropriate\u2014match the store locale), then rerun stage 2.
1487
+ prompt += `### Locale ${loc}: \u26A0\uFE0F No research - USE ENGLISH KEYWORDS FROM optimizedPrimary
1488
+ `;
1489
+ prompt += `No keyword research found. Extract keywords from the optimizedPrimary JSON above and translate them:
1490
+ `;
1491
+ prompt += `1. Extract keywords from \`aso.keywords\` in optimizedPrimary
1492
+ `;
1493
+ prompt += `2. Translate each English keyword naturally into ${loc}
1494
+ `;
1495
+ prompt += `3. Use native expressions that ${loc} users would actually search for
1496
+ `;
1497
+ prompt += `4. Apply translated keywords to all ASO fields
1428
1498
 
1429
1499
  `;
1430
1500
  }
@@ -1511,7 +1581,7 @@ ${researchSections.join(
1511
1581
  `;
1512
1582
  prompt += `Process EACH locale in this batch sequentially:
1513
1583
  `;
1514
- prompt += `1. Use saved keyword research (or pause if missing and request keyword-research run)
1584
+ prompt += `1. Use saved keyword research OR translate from primary locale if missing (see fallback strategy above)
1515
1585
  `;
1516
1586
  prompt += `2. Replace keywords in ALL fields:
1517
1587
  `;
@@ -1567,11 +1637,13 @@ ${researchSections.join(
1567
1637
  prompt += `### Locale [locale-code]:
1568
1638
 
1569
1639
  `;
1570
- prompt += `**1. Keyword Research (saved)**
1640
+ prompt += `**1. Keyword Source**
1571
1641
  `;
1572
- prompt += ` - Cite file(s) used; list selected top 10 keywords (no new research)
1642
+ prompt += ` - If saved research exists: Cite file(s) used; list selected top 10 keywords
1573
1643
  `;
1574
- prompt += ` - Rationale: why these were chosen from saved research
1644
+ prompt += ` - If using fallback: List translated keywords from primary locale with translation rationale
1645
+ `;
1646
+ prompt += ` - Show final 10 keywords in target language with tier assignments
1575
1647
 
1576
1648
  `;
1577
1649
  prompt += `**2. Updated JSON** (complete locale structure with keyword replacements)
@@ -1622,13 +1694,46 @@ function extractRecommended(data) {
1622
1694
  const summary = data?.summary || data?.data?.summary;
1623
1695
  const recommended = summary?.recommendedKeywords;
1624
1696
  if (Array.isArray(recommended)) {
1625
- return recommended.map(String);
1626
- }
1627
- if (typeof recommended === "string") {
1628
- return [recommended];
1697
+ return recommended.map((item) => {
1698
+ if (typeof item === "object" && item?.keyword) {
1699
+ return {
1700
+ keyword: String(item.keyword),
1701
+ tier: String(item.tier || ""),
1702
+ difficulty: Number(item.difficulty) || 0,
1703
+ traffic: Number(item.traffic) || 0,
1704
+ rationale: String(item.rationale || "")
1705
+ };
1706
+ }
1707
+ if (typeof item === "string") {
1708
+ return { keyword: item, tier: "", difficulty: 0, traffic: 0, rationale: "" };
1709
+ }
1710
+ return null;
1711
+ }).filter((item) => item !== null);
1629
1712
  }
1630
1713
  return [];
1631
1714
  }
1715
+ function extractKeywordsByTier(data) {
1716
+ const summary = data?.summary || data?.data?.summary;
1717
+ const byTier = summary?.keywordsByTier || {};
1718
+ const extractKeywords = (tier) => Array.isArray(tier) ? tier.map((k) => typeof k === "object" ? k.keyword : String(k)).filter(Boolean) : [];
1719
+ return {
1720
+ tier1_core: extractKeywords(byTier.tier1_core),
1721
+ tier2_feature: extractKeywords(byTier.tier2_feature),
1722
+ tier3_longtail: extractKeywords(byTier.tier3_longtail)
1723
+ };
1724
+ }
1725
+ function extractRationale(data) {
1726
+ const summary = data?.summary || data?.data?.summary;
1727
+ return summary?.rationale || "";
1728
+ }
1729
+ function extractCompetitorInsights(data) {
1730
+ const summary = data?.summary || data?.data?.summary;
1731
+ const insights = summary?.competitorInsights || {};
1732
+ return {
1733
+ keywordGaps: Array.isArray(insights.keywordGaps) ? insights.keywordGaps : [],
1734
+ userLanguagePatterns: Array.isArray(insights.userLanguagePatterns) ? insights.userLanguagePatterns : []
1735
+ };
1736
+ }
1632
1737
  function extractMeta(data) {
1633
1738
  const meta = data?.meta || data?.data?.meta || {};
1634
1739
  return {
@@ -1642,31 +1747,170 @@ function formatEntry(entry) {
1642
1747
  const { filePath, data } = entry;
1643
1748
  const recommended = extractRecommended(data);
1644
1749
  const meta = extractMeta(data);
1750
+ const byTier = extractKeywordsByTier(data);
1751
+ const rationale = extractRationale(data);
1752
+ const insights = extractCompetitorInsights(data);
1645
1753
  if (data?.parseError) {
1646
1754
  return `File: ${filePath}
1647
1755
  Parse error: ${data.parseError}
1648
1756
  ----`;
1649
1757
  }
1650
1758
  const lines = [];
1651
- lines.push(`File: ${filePath}`);
1759
+ lines.push(`### File: ${filePath}`);
1652
1760
  if (meta.platform || meta.country) {
1653
1761
  lines.push(
1654
1762
  `Platform: ${meta.platform || "unknown"} | Country: ${meta.country || "unknown"}`
1655
1763
  );
1656
1764
  }
1657
- if (meta.seedKeywords?.length) {
1658
- lines.push(`Seeds: ${meta.seedKeywords.join(", ")}`);
1765
+ if (byTier.tier1_core.length > 0) {
1766
+ lines.push(`
1767
+ **Tier 1 (Core - use in title/subtitle):** ${byTier.tier1_core.join(", ")}`);
1768
+ }
1769
+ if (byTier.tier2_feature.length > 0) {
1770
+ lines.push(`**Tier 2 (Feature - use in keywords field/descriptions):** ${byTier.tier2_feature.join(", ")}`);
1771
+ }
1772
+ if (byTier.tier3_longtail.length > 0) {
1773
+ lines.push(`**Tier 3 (Longtail - use in intro/outro/features):** ${byTier.tier3_longtail.join(", ")}`);
1774
+ }
1775
+ if (recommended.length > 0) {
1776
+ lines.push(`
1777
+ **Keyword Details (${recommended.length} keywords):**`);
1778
+ recommended.forEach((kw, idx) => {
1779
+ const tierLabel = kw.tier ? ` [${kw.tier}]` : "";
1780
+ const scores = kw.traffic > 0 || kw.difficulty > 0 ? ` (traffic: ${kw.traffic.toFixed(2)}, difficulty: ${kw.difficulty.toFixed(2)})` : "";
1781
+ lines.push(`${idx + 1}. **${kw.keyword}**${tierLabel}${scores}`);
1782
+ if (kw.rationale) {
1783
+ lines.push(` \u2192 ${kw.rationale}`);
1784
+ }
1785
+ });
1659
1786
  }
1660
- if (meta.competitorApps?.length) {
1661
- const competitors = meta.competitorApps.map((c) => `${c.platform || "?"}:${c.appId || "?"}`).join(", ");
1662
- lines.push(`Competitors: ${competitors}`);
1787
+ if (rationale) {
1788
+ lines.push(`
1789
+ **Strategy:** ${rationale}`);
1663
1790
  }
1664
- if (recommended.length) {
1665
- lines.push(`Recommended keywords (${recommended.length}): ${recommended.join(", ")}`);
1666
- } else {
1667
- lines.push("Recommended keywords: (not provided)");
1791
+ if (insights.keywordGaps.length > 0) {
1792
+ lines.push(`
1793
+ **Keyword Gaps (opportunities):**`);
1794
+ insights.keywordGaps.forEach((gap) => lines.push(`- ${gap}`));
1795
+ }
1796
+ if (insights.userLanguagePatterns.length > 0) {
1797
+ lines.push(`
1798
+ **User Language Patterns (from reviews):**`);
1799
+ insights.userLanguagePatterns.forEach((pattern) => lines.push(`- ${pattern}`));
1668
1800
  }
1669
- lines.push("----");
1801
+ lines.push("\n----");
1802
+ return lines.join("\n");
1803
+ }
1804
+ function mergeKeywordData(entries) {
1805
+ const merged = {
1806
+ tier1_core: [],
1807
+ tier2_feature: [],
1808
+ tier3_longtail: [],
1809
+ allKeywords: [],
1810
+ rationale: "",
1811
+ keywordGaps: [],
1812
+ userLanguagePatterns: [],
1813
+ platforms: []
1814
+ };
1815
+ const seenKeywords = /* @__PURE__ */ new Set();
1816
+ const seenGaps = /* @__PURE__ */ new Set();
1817
+ const seenPatterns = /* @__PURE__ */ new Set();
1818
+ for (const entry of entries) {
1819
+ if (entry.data?.parseError) continue;
1820
+ const meta = extractMeta(entry.data);
1821
+ if (meta.platform && !merged.platforms.includes(meta.platform)) {
1822
+ merged.platforms.push(meta.platform);
1823
+ }
1824
+ const byTier = extractKeywordsByTier(entry.data);
1825
+ byTier.tier1_core.forEach((kw) => {
1826
+ if (!seenKeywords.has(kw.toLowerCase())) {
1827
+ merged.tier1_core.push(kw);
1828
+ seenKeywords.add(kw.toLowerCase());
1829
+ }
1830
+ });
1831
+ byTier.tier2_feature.forEach((kw) => {
1832
+ if (!seenKeywords.has(kw.toLowerCase())) {
1833
+ merged.tier2_feature.push(kw);
1834
+ seenKeywords.add(kw.toLowerCase());
1835
+ }
1836
+ });
1837
+ byTier.tier3_longtail.forEach((kw) => {
1838
+ if (!seenKeywords.has(kw.toLowerCase())) {
1839
+ merged.tier3_longtail.push(kw);
1840
+ seenKeywords.add(kw.toLowerCase());
1841
+ }
1842
+ });
1843
+ const recommended = extractRecommended(entry.data);
1844
+ for (const kw of recommended) {
1845
+ if (!seenKeywords.has(kw.keyword.toLowerCase())) {
1846
+ merged.allKeywords.push(kw);
1847
+ seenKeywords.add(kw.keyword.toLowerCase());
1848
+ }
1849
+ }
1850
+ const rationale = extractRationale(entry.data);
1851
+ if (rationale && !merged.rationale) {
1852
+ merged.rationale = rationale;
1853
+ } else if (rationale && merged.rationale) {
1854
+ merged.rationale += ` | ${meta.platform}: ${rationale}`;
1855
+ }
1856
+ const insights = extractCompetitorInsights(entry.data);
1857
+ insights.keywordGaps.forEach((gap) => {
1858
+ if (!seenGaps.has(gap)) {
1859
+ merged.keywordGaps.push(gap);
1860
+ seenGaps.add(gap);
1861
+ }
1862
+ });
1863
+ insights.userLanguagePatterns.forEach((pattern) => {
1864
+ if (!seenPatterns.has(pattern)) {
1865
+ merged.userLanguagePatterns.push(pattern);
1866
+ seenPatterns.add(pattern);
1867
+ }
1868
+ });
1869
+ }
1870
+ merged.allKeywords.sort((a, b) => b.traffic - a.traffic);
1871
+ return merged;
1872
+ }
1873
+ function formatMergedData(merged, researchDir) {
1874
+ const lines = [];
1875
+ lines.push(`### Combined Keyword Research (${merged.platforms.join(" + ")})`);
1876
+ lines.push(`Source: ${researchDir}`);
1877
+ if (merged.tier1_core.length > 0) {
1878
+ lines.push(`
1879
+ **Tier 1 (Core - use in title/subtitle):** ${merged.tier1_core.join(", ")}`);
1880
+ }
1881
+ if (merged.tier2_feature.length > 0) {
1882
+ lines.push(`**Tier 2 (Feature - use in keywords field/descriptions):** ${merged.tier2_feature.join(", ")}`);
1883
+ }
1884
+ if (merged.tier3_longtail.length > 0) {
1885
+ lines.push(`**Tier 3 (Longtail - use in intro/outro/features):** ${merged.tier3_longtail.join(", ")}`);
1886
+ }
1887
+ if (merged.allKeywords.length > 0) {
1888
+ lines.push(`
1889
+ **Top Keywords by Traffic (${merged.allKeywords.length} total):**`);
1890
+ merged.allKeywords.slice(0, 15).forEach((kw, idx) => {
1891
+ const tierLabel = kw.tier ? ` [${kw.tier}]` : "";
1892
+ const scores = kw.traffic > 0 || kw.difficulty > 0 ? ` (traffic: ${kw.traffic.toFixed(2)}, difficulty: ${kw.difficulty.toFixed(2)})` : "";
1893
+ lines.push(`${idx + 1}. **${kw.keyword}**${tierLabel}${scores}`);
1894
+ if (kw.rationale) {
1895
+ lines.push(` \u2192 ${kw.rationale}`);
1896
+ }
1897
+ });
1898
+ }
1899
+ if (merged.rationale) {
1900
+ lines.push(`
1901
+ **Strategy:** ${merged.rationale}`);
1902
+ }
1903
+ if (merged.keywordGaps.length > 0) {
1904
+ lines.push(`
1905
+ **Keyword Gaps (opportunities):**`);
1906
+ merged.keywordGaps.slice(0, 5).forEach((gap) => lines.push(`- ${gap}`));
1907
+ }
1908
+ if (merged.userLanguagePatterns.length > 0) {
1909
+ lines.push(`
1910
+ **User Language Patterns (from reviews):**`);
1911
+ merged.userLanguagePatterns.slice(0, 5).forEach((pattern) => lines.push(`- ${pattern}`));
1912
+ }
1913
+ lines.push("\n----");
1670
1914
  return lines.join("\n");
1671
1915
  }
1672
1916
  function loadKeywordResearchForLocale(slug, locale) {
@@ -1697,6 +1941,15 @@ function loadKeywordResearchForLocale(slug, locale) {
1697
1941
  });
1698
1942
  }
1699
1943
  }
1944
+ const validEntries = entries.filter((e) => !e.data?.parseError);
1945
+ if (validEntries.length > 1) {
1946
+ const merged = mergeKeywordData(validEntries);
1947
+ const mergedSection = formatMergedData(merged, researchDir);
1948
+ return { entries, sections: [mergedSection], researchDir };
1949
+ } else if (validEntries.length === 1) {
1950
+ const sections2 = entries.map(formatEntry);
1951
+ return { entries, sections: sections2, researchDir };
1952
+ }
1700
1953
  const sections = entries.map(formatEntry);
1701
1954
  return { entries, sections, researchDir };
1702
1955
  }
@@ -2596,11 +2849,31 @@ var keywordResearchTool = {
2596
2849
 
2597
2850
  **IMPORTANT:** Always use 'search-app' tool first to resolve the exact slug before calling this tool. The user may provide an approximate name, bundleId, or packageName - search-app will find and return the correct slug. Never pass user input directly as slug.
2598
2851
 
2599
- **Locale coverage:** If the product ships multiple locales, run this tool SEPARATELY for EVERY locale (including non-primary markets). Do NOT rely on "template-only" coverage for secondary locales\u2014produce a full keyword research file per locale.
2852
+ ## CRITICAL: Multi-Locale Execution Plan
2853
+
2854
+ **MANDATORY WORKFLOW - Complete each locale fully before moving to next:**
2855
+
2856
+ For EACH locale+platform combination, execute this cycle:
2857
+ 1. **Plan:** Call keyword-research(slug, locale, platform) with writeTemplate=false \u2192 get research plan
2858
+ 2. **Research:** Execute COMPLETE mcp-appstore workflow (all 16 steps) for that locale
2859
+ 3. **Save:** Call keyword-research again with researchData or researchDataPath \u2192 persist actual data
2860
+ 4. **Next:** Move to next locale+platform and repeat steps 1-3
2861
+
2862
+ **IMPORTANT: Research \u2192 Save \u2192 Next pattern**
2863
+ - Complete ONE locale fully (research + save) before starting the next
2864
+ - This prevents data loss if the session is interrupted
2865
+ - Each locale's data is persisted immediately after research
2600
2866
 
2601
- **Platform coverage:** Use search-app results to confirm supported platforms/locales (App Store + Google Play). Run this tool for EVERY supported platform/locale combination\u2014ios + android runs are separate.
2867
+ **FORBIDDEN:**
2868
+ - \u274C Using writeTemplate=true as final output
2869
+ - \u274C Skipping secondary locales
2870
+ - \u274C Researching multiple locales then saving all at once at the end
2871
+ - \u274C Stopping before all locale+platform combinations are done
2602
2872
 
2603
- Run this before improve-public. It gives a concrete MCP-powered research plan and a storage path under .aso/keywordResearch/products/[slug]/locales/[locale]/. Optionally writes a template or saves raw JSON from mcp-appstore tools.`,
2873
+ **REQUIRED:**
2874
+ - \u2705 Research locale \u2192 Save locale \u2192 Move to next (one at a time)
2875
+ - \u2705 Run for EVERY platform (ios AND android separately)
2876
+ - \u2705 Use researchData or researchDataPath to save (NOT writeTemplate)`,
2604
2877
  inputSchema: inputSchema7
2605
2878
  };
2606
2879
  function buildTemplate({
@@ -2733,7 +3006,6 @@ async function handleKeywordResearch(input) {
2733
3006
  const { config, locales } = loadProductLocales(slug);
2734
3007
  const primaryLocale = resolvePrimaryLocale(config, locales);
2735
3008
  const productLocales = Object.keys(locales);
2736
- const remainingLocales = productLocales.filter((loc) => loc !== locale);
2737
3009
  const primaryLocaleData = locales[primaryLocale];
2738
3010
  const { supportedLocales, path: supportedPath } = getSupportedLocalesForSlug(slug, platform);
2739
3011
  const appStoreLocales = registeredApp?.appStore?.supportedLocales || [];
@@ -2879,24 +3151,56 @@ Context around ${pos}: ${context}`
2879
3151
  `Registered supported locales not found for ${platform} (checked: ${supportedPath}).`
2880
3152
  );
2881
3153
  }
3154
+ const allCombinations = [];
3155
+ const platformsToRun = declaredPlatforms.length > 0 ? declaredPlatforms : [platform];
3156
+ for (const plat of platformsToRun) {
3157
+ for (const loc of productLocales.length > 0 ? productLocales : [locale]) {
3158
+ allCombinations.push({ loc, plat });
3159
+ }
3160
+ }
3161
+ const currentIndex = allCombinations.findIndex(
3162
+ (c) => c.loc === locale && c.plat === platform
3163
+ );
3164
+ const completedCount = currentIndex >= 0 ? currentIndex : 0;
3165
+ const remainingCombinations = allCombinations.slice(currentIndex + 1);
2882
3166
  if (productLocales.length > 0) {
2883
3167
  lines.push(
2884
3168
  `Existing product locales (${productLocales.length}): ${productLocales.join(", ")}`
2885
3169
  );
2886
- lines.push(
2887
- "MANDATORY: Run FULL keyword research (mcp-appstore workflow) for EVERY locale above\u2014no template-only coverage for secondary markets."
2888
- );
2889
- if (remainingLocales.length > 0) {
2890
- lines.push(
2891
- `After finishing ${locale}, immediately queue runs for: ${remainingLocales.join(", ")}`
2892
- );
2893
- }
2894
- if (declaredPlatforms.length > 1) {
2895
- lines.push(
2896
- "Also run separate FULL keyword research for each supported platform (e.g., ios + android) across all locales."
2897
- );
2898
- }
2899
3170
  }
3171
+ lines.push("");
3172
+ lines.push("---");
3173
+ lines.push("## \u{1F3AF} EXECUTION PROGRESS TRACKER");
3174
+ lines.push("");
3175
+ lines.push(`**Total combinations to complete:** ${allCombinations.length} (${platformsToRun.length} platforms \xD7 ${productLocales.length || 1} locales)`);
3176
+ lines.push(`**Current:** ${locale} + ${platform} (${completedCount + 1}/${allCombinations.length})`);
3177
+ lines.push("");
3178
+ if (researchData || researchDataPath) {
3179
+ lines.push(`\u2705 SAVED: ${locale} + ${platform} - Full research data persisted`);
3180
+ } else if (writeTemplate) {
3181
+ lines.push(`\u26A0\uFE0F WARNING: ${locale} + ${platform} - Only template written! You MUST run full mcp-appstore research and save actual data.`);
3182
+ } else {
3183
+ lines.push(`\u{1F4CB} PLANNING: ${locale} + ${platform} - Research plan shown. Now execute mcp-appstore workflow.`);
3184
+ }
3185
+ if (remainingCombinations.length > 0) {
3186
+ lines.push("");
3187
+ lines.push("## \u23ED\uFE0F MANDATORY NEXT STEPS");
3188
+ lines.push("");
3189
+ lines.push("**After completing current locale+platform, you MUST continue with:**");
3190
+ lines.push("");
3191
+ remainingCombinations.forEach((combo, idx) => {
3192
+ lines.push(`${idx + 1}. keyword-research(slug="${slug}", locale="${combo.loc}", platform="${combo.plat}") \u2192 full mcp-appstore workflow \u2192 save results`);
3193
+ });
3194
+ lines.push("");
3195
+ lines.push("\u26D4 DO NOT mark this task as complete until ALL combinations above have FULL research data (not templates).");
3196
+ } else {
3197
+ lines.push("");
3198
+ lines.push("## \u2705 FINAL STEP");
3199
+ lines.push("");
3200
+ lines.push("This is the LAST locale+platform combination. After saving full research data for this one, the task is complete.");
3201
+ }
3202
+ lines.push("---");
3203
+ lines.push("");
2900
3204
  lines.push(
2901
3205
  `Seeds: ${resolvedSeeds.length > 0 ? resolvedSeeds.join(", ") : "(none set; add seedKeywords or ensure ASO keywords/title exist)"}`
2902
3206
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pabal-web-mcp",
3
- "version": "1.3.10",
3
+ "version": "1.3.12",
4
4
  "type": "module",
5
5
  "description": "MCP server for ASO data management with shared types and utilities",
6
6
  "author": "skyu",