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.
- package/dist/bin/mcp-server.js +363 -59
- package/package.json +1 -1
package/dist/bin/mcp-server.js
CHANGED
|
@@ -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 +=
|
|
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 +=
|
|
1258
|
+
prompt += `**Apply keywords strategically based on tier priority:**
|
|
1259
|
+
|
|
1242
1260
|
`;
|
|
1243
|
-
prompt +=
|
|
1261
|
+
prompt += `### Tier 1 Keywords (Core) \u2192 Title & Subtitle
|
|
1244
1262
|
`;
|
|
1245
|
-
prompt +=
|
|
1263
|
+
prompt += `- \`aso.title\` (\u226430): **"App Name: [Tier1 Keyword]"** format
|
|
1246
1264
|
`;
|
|
1247
|
-
prompt +=
|
|
1265
|
+
prompt += ` - App name in English, keyword in target language, uppercase after colon
|
|
1248
1266
|
`;
|
|
1249
|
-
prompt +=
|
|
1267
|
+
prompt += ` - **Do NOT translate/rename the app name**
|
|
1250
1268
|
`;
|
|
1251
|
-
prompt += `- \`aso.
|
|
1269
|
+
prompt += `- \`aso.subtitle\` (\u226430): Use remaining Tier 1 keywords
|
|
1252
1270
|
`;
|
|
1253
|
-
prompt += `- \`aso.
|
|
1271
|
+
prompt += `- \`aso.shortDescription\` (\u226480): Tier 1 + Tier 2 keywords (no emojis/CAPS)
|
|
1272
|
+
|
|
1254
1273
|
`;
|
|
1255
|
-
prompt +=
|
|
1274
|
+
prompt += `### Tier 2 Keywords (Feature) \u2192 Keywords Field & Descriptions
|
|
1256
1275
|
`;
|
|
1257
|
-
prompt += `- \`
|
|
1276
|
+
prompt += `- \`aso.keywords\` (\u2264100): ALL tiers, comma-separated (Tier 1 first, then Tier 2, then Tier 3)
|
|
1258
1277
|
`;
|
|
1259
|
-
prompt += `- \`landing.hero.
|
|
1278
|
+
prompt += `- \`landing.hero.title\`: Tier 1 + Tier 2 keywords
|
|
1260
1279
|
`;
|
|
1261
|
-
prompt += `- \`landing.
|
|
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 +=
|
|
1287
|
+
prompt += `### Tier 3 Keywords (Longtail) \u2192 Content Sections
|
|
1264
1288
|
`;
|
|
1265
|
-
prompt += `- \`
|
|
1289
|
+
prompt += `- \`aso.template.intro\` (\u2264300): Tier 2 + Tier 3 keywords, keyword-rich, use full length
|
|
1266
1290
|
`;
|
|
1267
|
-
prompt += `- \`
|
|
1291
|
+
prompt += `- \`aso.template.outro\` (\u2264200): Tier 3 keywords, natural integration
|
|
1268
1292
|
`;
|
|
1269
|
-
prompt += `- \`landing.
|
|
1293
|
+
prompt += `- \`landing.features.items[].title\`: Tier 2 keywords
|
|
1270
1294
|
`;
|
|
1271
|
-
prompt += `- \`landing.
|
|
1295
|
+
prompt += `- \`landing.features.items[].body\`: Tier 3 keywords with user language patterns
|
|
1272
1296
|
`;
|
|
1273
|
-
prompt += `- \`landing.
|
|
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
|
|
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 +=
|
|
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 +=
|
|
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
|
|
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
|
|
1640
|
+
prompt += `**1. Keyword Source**
|
|
1571
1641
|
`;
|
|
1572
|
-
prompt += ` - Cite file(s) used; list selected top 10 keywords
|
|
1642
|
+
prompt += ` - If saved research exists: Cite file(s) used; list selected top 10 keywords
|
|
1573
1643
|
`;
|
|
1574
|
-
prompt += ` -
|
|
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(
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
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(
|
|
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 (
|
|
1658
|
-
lines.push(`
|
|
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 (
|
|
1661
|
-
|
|
1662
|
-
|
|
1787
|
+
if (rationale) {
|
|
1788
|
+
lines.push(`
|
|
1789
|
+
**Strategy:** ${rationale}`);
|
|
1663
1790
|
}
|
|
1664
|
-
if (
|
|
1665
|
-
lines.push(`
|
|
1666
|
-
|
|
1667
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
);
|