pabal-web-mcp 1.2.3 → 1.3.0
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/README.md +57 -194
- package/dist/bin/mcp-server.js +421 -61
- package/dist/browser.d.ts +15 -0
- package/dist/browser.js +60 -0
- package/dist/chunk-BOWRBVVV.js +716 -0
- package/dist/chunk-DLCIXAUB.js +6 -0
- package/dist/chunk-FXCHLO7O.js +351 -0
- package/dist/chunk-W62HB2ZL.js +355 -0
- package/dist/index.d.ts +9 -797
- package/dist/index.js +13 -9
- package/dist/locale-converter-B_NCFuS8.d.ts +798 -0
- package/package.json +12 -2
package/dist/bin/mcp-server.js
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
DEFAULT_APP_SLUG
|
|
4
|
+
} from "../chunk-DLCIXAUB.js";
|
|
5
|
+
import {
|
|
6
|
+
getKeywordResearchDir,
|
|
5
7
|
getProductsDir,
|
|
6
8
|
getPublicDir,
|
|
7
9
|
getPullDataDir,
|
|
8
10
|
getPushDataDir,
|
|
11
|
+
loadAsoFromConfig,
|
|
12
|
+
saveAsoToAsoDir
|
|
13
|
+
} from "../chunk-W62HB2ZL.js";
|
|
14
|
+
import {
|
|
15
|
+
DEFAULT_LOCALE,
|
|
16
|
+
appStoreToUnified,
|
|
9
17
|
googlePlayToUnified,
|
|
10
18
|
isAppStoreLocale,
|
|
11
19
|
isAppStoreMultilingual,
|
|
12
20
|
isGooglePlayLocale,
|
|
13
21
|
isGooglePlayMultilingual,
|
|
14
|
-
loadAsoFromConfig,
|
|
15
|
-
saveAsoToAsoDir,
|
|
16
22
|
unifiedToAppStore,
|
|
17
23
|
unifiedToGooglePlay
|
|
18
|
-
} from "../chunk-
|
|
24
|
+
} from "../chunk-BOWRBVVV.js";
|
|
19
25
|
|
|
20
26
|
// src/bin/mcp-server.ts
|
|
21
27
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -1189,7 +1195,14 @@ ${json}
|
|
|
1189
1195
|
// src/tools/utils/improve-public/generate-aso-prompt.util.ts
|
|
1190
1196
|
var FIELD_LIMITS_DOC_PATH2 = "docs/aso/ASO_FIELD_LIMITS.md";
|
|
1191
1197
|
function generatePrimaryOptimizationPrompt(args) {
|
|
1192
|
-
const {
|
|
1198
|
+
const {
|
|
1199
|
+
slug,
|
|
1200
|
+
category,
|
|
1201
|
+
primaryLocale,
|
|
1202
|
+
localeSections,
|
|
1203
|
+
keywordResearchByLocale,
|
|
1204
|
+
keywordResearchDirByLocale
|
|
1205
|
+
} = args;
|
|
1193
1206
|
let prompt = `# ASO Optimization - Stage 1: Primary Locale
|
|
1194
1207
|
|
|
1195
1208
|
`;
|
|
@@ -1199,32 +1212,33 @@ function generatePrimaryOptimizationPrompt(args) {
|
|
|
1199
1212
|
prompt += `## Task
|
|
1200
1213
|
|
|
1201
1214
|
`;
|
|
1202
|
-
prompt += `Optimize the PRIMARY locale (${primaryLocale})
|
|
1215
|
+
prompt += `Optimize the PRIMARY locale (${primaryLocale}) using **saved keyword research** + full ASO field optimization.
|
|
1203
1216
|
|
|
1204
1217
|
`;
|
|
1205
1218
|
prompt += `## Step 1: Keyword Research (${primaryLocale})
|
|
1206
1219
|
|
|
1207
1220
|
`;
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
`;
|
|
1214
|
-
prompt += `3. **Feature-Based**: Identify 2-3 core features \u2192 search feature-specific keywords
|
|
1215
|
-
`;
|
|
1216
|
-
prompt += `4. **User Intent**: Research user search patterns for the app's use case
|
|
1221
|
+
const researchSections = keywordResearchByLocale[primaryLocale] || [];
|
|
1222
|
+
const researchDir = keywordResearchDirByLocale[primaryLocale];
|
|
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.
|
|
1225
|
+
|
|
1217
1226
|
`;
|
|
1218
|
-
|
|
1227
|
+
prompt += `Saved research:
|
|
1228
|
+
${researchSections.join("\n")}
|
|
1219
1229
|
|
|
1220
1230
|
`;
|
|
1221
|
-
|
|
1231
|
+
} else {
|
|
1232
|
+
prompt += `No saved keyword research found at ${researchDir}.
|
|
1233
|
+
`;
|
|
1234
|
+
prompt += `**Stop and request action**: Run the 'keyword-research' tool with slug='${slug}', locale='${primaryLocale}', and the appropriate platform/country, then rerun improve-public stage 1.
|
|
1222
1235
|
|
|
1223
1236
|
`;
|
|
1237
|
+
}
|
|
1224
1238
|
prompt += `## Step 2: Optimize All Fields (${primaryLocale})
|
|
1225
1239
|
|
|
1226
1240
|
`;
|
|
1227
|
-
prompt += `Apply the
|
|
1241
|
+
prompt += `Apply the selected keywords to ALL fields:
|
|
1228
1242
|
`;
|
|
1229
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)
|
|
1230
1244
|
`;
|
|
@@ -1279,11 +1293,11 @@ function generatePrimaryOptimizationPrompt(args) {
|
|
|
1279
1293
|
prompt += `## Output Format
|
|
1280
1294
|
|
|
1281
1295
|
`;
|
|
1282
|
-
prompt += `**1. Keyword Research**
|
|
1296
|
+
prompt += `**1. Keyword Research (from saved data)**
|
|
1283
1297
|
`;
|
|
1284
|
-
prompt += ` -
|
|
1298
|
+
prompt += ` - Cite file(s) used and list the selected top 10 keywords (no new research)
|
|
1285
1299
|
`;
|
|
1286
|
-
prompt += ` -
|
|
1300
|
+
prompt += ` - Rationale: why these 10 were chosen from saved research
|
|
1287
1301
|
|
|
1288
1302
|
`;
|
|
1289
1303
|
prompt += `**2. Optimized JSON** (complete ${primaryLocale} locale structure)
|
|
@@ -1331,6 +1345,8 @@ function generateKeywordLocalizationPrompt(args) {
|
|
|
1331
1345
|
targetLocales,
|
|
1332
1346
|
localeSections,
|
|
1333
1347
|
optimizedPrimary,
|
|
1348
|
+
keywordResearchByLocale,
|
|
1349
|
+
keywordResearchDirByLocale,
|
|
1334
1350
|
batchLocales,
|
|
1335
1351
|
batchIndex,
|
|
1336
1352
|
totalBatches,
|
|
@@ -1374,7 +1390,7 @@ function generateKeywordLocalizationPrompt(args) {
|
|
|
1374
1390
|
`;
|
|
1375
1391
|
prompt += `For EACH target locale in this batch:
|
|
1376
1392
|
`;
|
|
1377
|
-
prompt += `1.
|
|
1393
|
+
prompt += `1. Use SAVED keyword research (see per-locale data below). Do NOT invent keywords.
|
|
1378
1394
|
`;
|
|
1379
1395
|
prompt += `2. Replace keywords in translated content (preserve structure/tone/context)
|
|
1380
1396
|
`;
|
|
@@ -1395,18 +1411,22 @@ ${optimizedPrimary}
|
|
|
1395
1411
|
prompt += `## Keyword Research (Per Locale)
|
|
1396
1412
|
|
|
1397
1413
|
`;
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1414
|
+
nonPrimaryLocales.forEach((loc) => {
|
|
1415
|
+
const researchSections = keywordResearchByLocale[loc] || [];
|
|
1416
|
+
const researchDir = keywordResearchDirByLocale[loc];
|
|
1417
|
+
if (researchSections.length > 0) {
|
|
1418
|
+
prompt += `Locale ${loc}: use saved research below. Do NOT invent keywords.
|
|
1419
|
+
${researchSections.join(
|
|
1420
|
+
"\n"
|
|
1421
|
+
)}
|
|
1405
1422
|
|
|
1406
1423
|
`;
|
|
1407
|
-
|
|
1424
|
+
} else {
|
|
1425
|
+
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.
|
|
1408
1426
|
|
|
1409
1427
|
`;
|
|
1428
|
+
}
|
|
1429
|
+
});
|
|
1410
1430
|
prompt += `## Keyword Replacement Strategy
|
|
1411
1431
|
|
|
1412
1432
|
`;
|
|
@@ -1487,7 +1507,7 @@ ${optimizedPrimary}
|
|
|
1487
1507
|
`;
|
|
1488
1508
|
prompt += `Process EACH locale in this batch sequentially:
|
|
1489
1509
|
`;
|
|
1490
|
-
prompt += `1.
|
|
1510
|
+
prompt += `1. Use saved keyword research (or pause if missing and request keyword-research run)
|
|
1491
1511
|
`;
|
|
1492
1512
|
prompt += `2. Replace keywords in ALL fields:
|
|
1493
1513
|
`;
|
|
@@ -1543,11 +1563,11 @@ ${optimizedPrimary}
|
|
|
1543
1563
|
prompt += `### Locale [locale-code]:
|
|
1544
1564
|
|
|
1545
1565
|
`;
|
|
1546
|
-
prompt += `**1. Keyword Research**
|
|
1566
|
+
prompt += `**1. Keyword Research (saved)**
|
|
1547
1567
|
`;
|
|
1548
|
-
prompt += ` -
|
|
1568
|
+
prompt += ` - Cite file(s) used; list selected top 10 keywords (no new research)
|
|
1549
1569
|
`;
|
|
1550
|
-
prompt += ` -
|
|
1570
|
+
prompt += ` - Rationale: why these were chosen from saved research
|
|
1551
1571
|
|
|
1552
1572
|
`;
|
|
1553
1573
|
prompt += `**2. Updated JSON** (complete locale structure with keyword replacements)
|
|
@@ -1591,6 +1611,92 @@ ${optimizedPrimary}
|
|
|
1591
1611
|
return prompt;
|
|
1592
1612
|
}
|
|
1593
1613
|
|
|
1614
|
+
// src/tools/utils/improve-public/load-keyword-research.util.ts
|
|
1615
|
+
import fs6 from "fs";
|
|
1616
|
+
import path6 from "path";
|
|
1617
|
+
function extractRecommended(data) {
|
|
1618
|
+
const summary = data?.summary || data?.data?.summary;
|
|
1619
|
+
const recommended = summary?.recommendedKeywords;
|
|
1620
|
+
if (Array.isArray(recommended)) {
|
|
1621
|
+
return recommended.map(String);
|
|
1622
|
+
}
|
|
1623
|
+
if (typeof recommended === "string") {
|
|
1624
|
+
return [recommended];
|
|
1625
|
+
}
|
|
1626
|
+
return [];
|
|
1627
|
+
}
|
|
1628
|
+
function extractMeta(data) {
|
|
1629
|
+
const meta = data?.meta || data?.data?.meta || {};
|
|
1630
|
+
return {
|
|
1631
|
+
platform: meta.platform,
|
|
1632
|
+
country: meta.country,
|
|
1633
|
+
seedKeywords: Array.isArray(meta.seedKeywords) ? meta.seedKeywords.map(String) : void 0,
|
|
1634
|
+
competitorApps: Array.isArray(meta.competitorApps) ? meta.competitorApps : void 0
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
function formatEntry(entry) {
|
|
1638
|
+
const { filePath, data } = entry;
|
|
1639
|
+
const recommended = extractRecommended(data);
|
|
1640
|
+
const meta = extractMeta(data);
|
|
1641
|
+
if (data?.parseError) {
|
|
1642
|
+
return `File: ${filePath}
|
|
1643
|
+
Parse error: ${data.parseError}
|
|
1644
|
+
----`;
|
|
1645
|
+
}
|
|
1646
|
+
const lines = [];
|
|
1647
|
+
lines.push(`File: ${filePath}`);
|
|
1648
|
+
if (meta.platform || meta.country) {
|
|
1649
|
+
lines.push(
|
|
1650
|
+
`Platform: ${meta.platform || "unknown"} | Country: ${meta.country || "unknown"}`
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
if (meta.seedKeywords?.length) {
|
|
1654
|
+
lines.push(`Seeds: ${meta.seedKeywords.join(", ")}`);
|
|
1655
|
+
}
|
|
1656
|
+
if (meta.competitorApps?.length) {
|
|
1657
|
+
const competitors = meta.competitorApps.map((c) => `${c.platform || "?"}:${c.appId || "?"}`).join(", ");
|
|
1658
|
+
lines.push(`Competitors: ${competitors}`);
|
|
1659
|
+
}
|
|
1660
|
+
if (recommended.length) {
|
|
1661
|
+
lines.push(`Recommended keywords (${recommended.length}): ${recommended.join(", ")}`);
|
|
1662
|
+
} else {
|
|
1663
|
+
lines.push("Recommended keywords: (not provided)");
|
|
1664
|
+
}
|
|
1665
|
+
lines.push("----");
|
|
1666
|
+
return lines.join("\n");
|
|
1667
|
+
}
|
|
1668
|
+
function loadKeywordResearchForLocale(slug, locale) {
|
|
1669
|
+
const researchDir = path6.join(
|
|
1670
|
+
getKeywordResearchDir(),
|
|
1671
|
+
"products",
|
|
1672
|
+
slug,
|
|
1673
|
+
"locales",
|
|
1674
|
+
locale
|
|
1675
|
+
);
|
|
1676
|
+
if (!fs6.existsSync(researchDir)) {
|
|
1677
|
+
return { entries: [], sections: [], researchDir };
|
|
1678
|
+
}
|
|
1679
|
+
const files = fs6.readdirSync(researchDir).filter((file) => file.endsWith(".json"));
|
|
1680
|
+
const entries = [];
|
|
1681
|
+
for (const file of files) {
|
|
1682
|
+
const filePath = path6.join(researchDir, file);
|
|
1683
|
+
try {
|
|
1684
|
+
const raw = fs6.readFileSync(filePath, "utf-8");
|
|
1685
|
+
const data = JSON.parse(raw);
|
|
1686
|
+
entries.push({ filePath, data });
|
|
1687
|
+
} catch (err) {
|
|
1688
|
+
entries.push({
|
|
1689
|
+
filePath,
|
|
1690
|
+
data: {
|
|
1691
|
+
parseError: err instanceof Error ? err.message : "Unknown parse error"
|
|
1692
|
+
}
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
const sections = entries.map(formatEntry);
|
|
1697
|
+
return { entries, sections, researchDir };
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1594
1700
|
// src/tools/improve-public.ts
|
|
1595
1701
|
var FIELD_LIMITS_DOC_PATH3 = "docs/aso/ASO_FIELD_LIMITS.md";
|
|
1596
1702
|
var toJsonSchema3 = zodToJsonSchema3;
|
|
@@ -1707,12 +1813,21 @@ async function handleImprovePublic(input) {
|
|
|
1707
1813
|
})
|
|
1708
1814
|
);
|
|
1709
1815
|
}
|
|
1816
|
+
const keywordResearchByLocale = {};
|
|
1817
|
+
const keywordResearchDirByLocale = {};
|
|
1818
|
+
for (const loc of targetLocales) {
|
|
1819
|
+
const research = loadKeywordResearchForLocale(slug, loc);
|
|
1820
|
+
keywordResearchByLocale[loc] = research.sections;
|
|
1821
|
+
keywordResearchDirByLocale[loc] = research.researchDir;
|
|
1822
|
+
}
|
|
1710
1823
|
const baseArgs = {
|
|
1711
1824
|
slug,
|
|
1712
1825
|
category,
|
|
1713
1826
|
primaryLocale,
|
|
1714
1827
|
targetLocales,
|
|
1715
|
-
localeSections
|
|
1828
|
+
localeSections,
|
|
1829
|
+
keywordResearchByLocale,
|
|
1830
|
+
keywordResearchDirByLocale
|
|
1716
1831
|
};
|
|
1717
1832
|
if (stage === "1" || stage === "both") {
|
|
1718
1833
|
const prompt = generatePrimaryOptimizationPrompt(baseArgs);
|
|
@@ -1758,6 +1873,8 @@ async function handleImprovePublic(input) {
|
|
|
1758
1873
|
primaryLocale: baseArgs.primaryLocale,
|
|
1759
1874
|
targetLocales: baseArgs.targetLocales,
|
|
1760
1875
|
localeSections: baseArgs.localeSections,
|
|
1876
|
+
keywordResearchByLocale: baseArgs.keywordResearchByLocale,
|
|
1877
|
+
keywordResearchDirByLocale: baseArgs.keywordResearchDirByLocale,
|
|
1761
1878
|
optimizedPrimary,
|
|
1762
1879
|
batchLocales,
|
|
1763
1880
|
batchIndex: currentBatchIndex,
|
|
@@ -1780,13 +1897,13 @@ async function handleImprovePublic(input) {
|
|
|
1780
1897
|
}
|
|
1781
1898
|
|
|
1782
1899
|
// src/tools/init-project.ts
|
|
1783
|
-
import
|
|
1784
|
-
import
|
|
1900
|
+
import fs7 from "fs";
|
|
1901
|
+
import path7 from "path";
|
|
1785
1902
|
import { z as z4 } from "zod";
|
|
1786
1903
|
import { zodToJsonSchema as zodToJsonSchema4 } from "zod-to-json-schema";
|
|
1787
1904
|
var listSlugDirs = (dir) => {
|
|
1788
|
-
if (!
|
|
1789
|
-
return
|
|
1905
|
+
if (!fs7.existsSync(dir)) return [];
|
|
1906
|
+
return fs7.readdirSync(dir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
|
|
1790
1907
|
};
|
|
1791
1908
|
var initProjectInputSchema = z4.object({
|
|
1792
1909
|
slug: z4.string().trim().optional().describe(
|
|
@@ -1811,7 +1928,7 @@ Steps:
|
|
|
1811
1928
|
inputSchema: inputSchema4
|
|
1812
1929
|
};
|
|
1813
1930
|
async function handleInitProject(input) {
|
|
1814
|
-
const pullDataDir =
|
|
1931
|
+
const pullDataDir = path7.join(getPullDataDir(), "products");
|
|
1815
1932
|
const publicDir = getProductsDir();
|
|
1816
1933
|
const pullDataSlugs = listSlugDirs(pullDataDir);
|
|
1817
1934
|
const publicSlugs = listSlugDirs(publicDir);
|
|
@@ -1889,14 +2006,14 @@ async function handleInitProject(input) {
|
|
|
1889
2006
|
}
|
|
1890
2007
|
|
|
1891
2008
|
// src/tools/create-blog-html.ts
|
|
1892
|
-
import
|
|
1893
|
-
import
|
|
2009
|
+
import fs9 from "fs";
|
|
2010
|
+
import path9 from "path";
|
|
1894
2011
|
import { z as z5 } from "zod";
|
|
1895
2012
|
import { zodToJsonSchema as zodToJsonSchema5 } from "zod-to-json-schema";
|
|
1896
2013
|
|
|
1897
2014
|
// src/utils/blog.util.ts
|
|
1898
|
-
import
|
|
1899
|
-
import
|
|
2015
|
+
import fs8 from "fs";
|
|
2016
|
+
import path8 from "path";
|
|
1900
2017
|
var DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
|
|
1901
2018
|
var BLOG_ROOT = "blogs";
|
|
1902
2019
|
var removeDiacritics = (value) => value.normalize("NFKD").replace(/[\u0300-\u036f]/g, "");
|
|
@@ -1984,13 +2101,13 @@ function resolveTargetLocales(input) {
|
|
|
1984
2101
|
return fallback ? [fallback] : [];
|
|
1985
2102
|
}
|
|
1986
2103
|
function getBlogOutputPaths(options) {
|
|
1987
|
-
const baseDir =
|
|
2104
|
+
const baseDir = path8.join(
|
|
1988
2105
|
options.publicDir,
|
|
1989
2106
|
BLOG_ROOT,
|
|
1990
2107
|
options.appSlug,
|
|
1991
2108
|
options.slug
|
|
1992
2109
|
);
|
|
1993
|
-
const filePath =
|
|
2110
|
+
const filePath = path8.join(baseDir, `${options.locale}.html`);
|
|
1994
2111
|
const publicBasePath = toPublicBlogBase(options.appSlug, options.slug);
|
|
1995
2112
|
return { baseDir, filePath, publicBasePath };
|
|
1996
2113
|
}
|
|
@@ -2015,18 +2132,18 @@ function findExistingBlogPosts({
|
|
|
2015
2132
|
publicDir,
|
|
2016
2133
|
limit = 2
|
|
2017
2134
|
}) {
|
|
2018
|
-
const blogAppDir =
|
|
2019
|
-
if (!
|
|
2135
|
+
const blogAppDir = path8.join(publicDir, BLOG_ROOT, appSlug);
|
|
2136
|
+
if (!fs8.existsSync(blogAppDir)) {
|
|
2020
2137
|
return [];
|
|
2021
2138
|
}
|
|
2022
2139
|
const posts = [];
|
|
2023
|
-
const subdirs =
|
|
2140
|
+
const subdirs = fs8.readdirSync(blogAppDir, { withFileTypes: true });
|
|
2024
2141
|
for (const subdir of subdirs) {
|
|
2025
2142
|
if (!subdir.isDirectory()) continue;
|
|
2026
|
-
const localeFile =
|
|
2027
|
-
if (!
|
|
2143
|
+
const localeFile = path8.join(blogAppDir, subdir.name, `${locale}.html`);
|
|
2144
|
+
if (!fs8.existsSync(localeFile)) continue;
|
|
2028
2145
|
try {
|
|
2029
|
-
const htmlContent =
|
|
2146
|
+
const htmlContent = fs8.readFileSync(localeFile, "utf-8");
|
|
2030
2147
|
const { meta, body } = parseBlogHtml(htmlContent);
|
|
2031
2148
|
if (meta && meta.locale === locale) {
|
|
2032
2149
|
posts.push({
|
|
@@ -2052,9 +2169,6 @@ function findExistingBlogPosts({
|
|
|
2052
2169
|
}));
|
|
2053
2170
|
}
|
|
2054
2171
|
|
|
2055
|
-
// src/constants/blog.constants.ts
|
|
2056
|
-
var DEFAULT_APP_SLUG = "developer";
|
|
2057
|
-
|
|
2058
2172
|
// src/tools/create-blog-html.ts
|
|
2059
2173
|
var toJsonSchema4 = zodToJsonSchema5;
|
|
2060
2174
|
var DATE_REGEX2 = /^\d{4}-\d{2}-\d{2}$/;
|
|
@@ -2172,7 +2286,7 @@ async function handleCreateBlogHtml(input) {
|
|
|
2172
2286
|
}
|
|
2173
2287
|
const output = {
|
|
2174
2288
|
slug,
|
|
2175
|
-
baseDir:
|
|
2289
|
+
baseDir: path9.join(publicDir, "blogs", appSlug, slug),
|
|
2176
2290
|
files: [],
|
|
2177
2291
|
coverImage: coverImage && coverImage.trim().length > 0 ? coverImage.trim() : `/products/${appSlug}/og-image.png`,
|
|
2178
2292
|
metaByLocale: {}
|
|
@@ -2186,7 +2300,7 @@ async function handleCreateBlogHtml(input) {
|
|
|
2186
2300
|
})
|
|
2187
2301
|
);
|
|
2188
2302
|
const existing = plannedFiles.filter(
|
|
2189
|
-
({ filePath }) =>
|
|
2303
|
+
({ filePath }) => fs9.existsSync(filePath)
|
|
2190
2304
|
);
|
|
2191
2305
|
if (existing.length > 0 && !overwrite) {
|
|
2192
2306
|
const existingList = existing.map((f) => f.filePath).join("\n- ");
|
|
@@ -2195,7 +2309,7 @@ async function handleCreateBlogHtml(input) {
|
|
|
2195
2309
|
- ${existingList}`
|
|
2196
2310
|
);
|
|
2197
2311
|
}
|
|
2198
|
-
|
|
2312
|
+
fs9.mkdirSync(output.baseDir, { recursive: true });
|
|
2199
2313
|
for (const locale of targetLocales) {
|
|
2200
2314
|
const { filePath } = getBlogOutputPaths({
|
|
2201
2315
|
appSlug,
|
|
@@ -2221,7 +2335,7 @@ async function handleCreateBlogHtml(input) {
|
|
|
2221
2335
|
meta,
|
|
2222
2336
|
content
|
|
2223
2337
|
});
|
|
2224
|
-
|
|
2338
|
+
fs9.writeFileSync(filePath, html, "utf-8");
|
|
2225
2339
|
output.files.push({ locale, path: filePath });
|
|
2226
2340
|
}
|
|
2227
2341
|
const summaryLines = [
|
|
@@ -2257,6 +2371,243 @@ Writing style reference for ${locale}: Found ${posts.length} existing post(s) us
|
|
|
2257
2371
|
};
|
|
2258
2372
|
}
|
|
2259
2373
|
|
|
2374
|
+
// src/tools/keyword-research.ts
|
|
2375
|
+
import fs10 from "fs";
|
|
2376
|
+
import path10 from "path";
|
|
2377
|
+
import { z as z6 } from "zod";
|
|
2378
|
+
import { zodToJsonSchema as zodToJsonSchema6 } from "zod-to-json-schema";
|
|
2379
|
+
var TOOL_NAME = "keyword-research";
|
|
2380
|
+
var keywordResearchInputSchema = z6.object({
|
|
2381
|
+
slug: z6.string().trim().describe("Product slug"),
|
|
2382
|
+
locale: z6.string().trim().describe("Locale code (e.g., en-US, ko-KR). Used for storage under .aso/keywordResearch/products/[slug]/locales/."),
|
|
2383
|
+
platform: z6.enum(["ios", "android"]).default("ios").describe("Store to target ('ios' or 'android'). Run separately per platform."),
|
|
2384
|
+
country: z6.string().length(2).optional().describe(
|
|
2385
|
+
"Two-letter store country code. If omitted, derived from locale region (e.g., ko-KR -> kr), else 'us'."
|
|
2386
|
+
),
|
|
2387
|
+
seedKeywords: z6.array(z6.string().trim()).default([]).describe("Seed keywords to start from."),
|
|
2388
|
+
competitorApps: z6.array(
|
|
2389
|
+
z6.object({
|
|
2390
|
+
appId: z6.string().trim().describe("App ID (package name or iOS ID/bundle)"),
|
|
2391
|
+
platform: z6.enum(["ios", "android"])
|
|
2392
|
+
})
|
|
2393
|
+
).default([]).describe("Known competitor apps to probe."),
|
|
2394
|
+
filename: z6.string().trim().optional().describe("Override output filename. Defaults to keyword-research-[platform]-[country].json"),
|
|
2395
|
+
writeTemplate: z6.boolean().default(false).describe("If true, write a JSON template at the output path."),
|
|
2396
|
+
researchData: z6.string().trim().optional().describe(
|
|
2397
|
+
"Optional JSON string with research results (e.g., from mcp-appstore tools). If provided, saves it to the output path."
|
|
2398
|
+
)
|
|
2399
|
+
});
|
|
2400
|
+
var jsonSchema6 = zodToJsonSchema6(keywordResearchInputSchema, {
|
|
2401
|
+
name: "KeywordResearchInput",
|
|
2402
|
+
$refStrategy: "none"
|
|
2403
|
+
});
|
|
2404
|
+
var inputSchema6 = jsonSchema6.definitions?.KeywordResearchInput || jsonSchema6;
|
|
2405
|
+
var keywordResearchTool = {
|
|
2406
|
+
name: TOOL_NAME,
|
|
2407
|
+
description: `Prep + persist keyword research ahead of improve-public using mcp-appstore outputs.
|
|
2408
|
+
|
|
2409
|
+
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.`,
|
|
2410
|
+
inputSchema: inputSchema6
|
|
2411
|
+
};
|
|
2412
|
+
function buildTemplate({
|
|
2413
|
+
slug,
|
|
2414
|
+
locale,
|
|
2415
|
+
platform,
|
|
2416
|
+
country,
|
|
2417
|
+
seedKeywords,
|
|
2418
|
+
competitorApps
|
|
2419
|
+
}) {
|
|
2420
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2421
|
+
return {
|
|
2422
|
+
meta: {
|
|
2423
|
+
slug,
|
|
2424
|
+
locale,
|
|
2425
|
+
platform,
|
|
2426
|
+
country,
|
|
2427
|
+
seedKeywords,
|
|
2428
|
+
competitorApps,
|
|
2429
|
+
source: "mcp-appstore",
|
|
2430
|
+
updatedAt: timestamp
|
|
2431
|
+
},
|
|
2432
|
+
plan: {
|
|
2433
|
+
steps: [
|
|
2434
|
+
"Start mcp-appstore server (npm start in external-tools/mcp-appstore).",
|
|
2435
|
+
"Discover competitors: search_app(term=seed keyword), get_similar_apps(appId=known competitor).",
|
|
2436
|
+
"Collect candidates: suggest_keywords_by_seeds, suggest_keywords_by_category, suggest_keywords_by_similarity, suggest_keywords_by_competition.",
|
|
2437
|
+
"Score shortlist: get_keyword_scores for 15\u201330 candidates per platform/country.",
|
|
2438
|
+
"Context check: analyze_reviews on top apps for language/tone cues."
|
|
2439
|
+
],
|
|
2440
|
+
note: "Run per platform/country. Save raw tool outputs plus curated top keywords."
|
|
2441
|
+
},
|
|
2442
|
+
data: {
|
|
2443
|
+
raw: {
|
|
2444
|
+
searchApp: [],
|
|
2445
|
+
keywordSuggestions: {
|
|
2446
|
+
bySeeds: [],
|
|
2447
|
+
byCategory: [],
|
|
2448
|
+
bySimilarity: [],
|
|
2449
|
+
byCompetition: [],
|
|
2450
|
+
bySearchHints: []
|
|
2451
|
+
},
|
|
2452
|
+
keywordScores: [],
|
|
2453
|
+
reviewsAnalysis: []
|
|
2454
|
+
},
|
|
2455
|
+
summary: {
|
|
2456
|
+
recommendedKeywords: [],
|
|
2457
|
+
rationale: "",
|
|
2458
|
+
nextActions: "Feed top 10\u201315 into improve-public Stage 1."
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2463
|
+
function saveJsonFile({
|
|
2464
|
+
researchDir,
|
|
2465
|
+
fileName,
|
|
2466
|
+
payload
|
|
2467
|
+
}) {
|
|
2468
|
+
fs10.mkdirSync(researchDir, { recursive: true });
|
|
2469
|
+
const outputPath = path10.join(researchDir, fileName);
|
|
2470
|
+
fs10.writeFileSync(outputPath, JSON.stringify(payload, null, 2) + "\n", "utf-8");
|
|
2471
|
+
return outputPath;
|
|
2472
|
+
}
|
|
2473
|
+
function normalizeKeywords(raw) {
|
|
2474
|
+
if (!raw) return [];
|
|
2475
|
+
if (Array.isArray(raw)) {
|
|
2476
|
+
return raw.map((k) => k.trim()).filter((k) => k.length > 0);
|
|
2477
|
+
}
|
|
2478
|
+
return raw.split(",").map((k) => k.trim()).filter((k) => k.length > 0);
|
|
2479
|
+
}
|
|
2480
|
+
async function handleKeywordResearch(input) {
|
|
2481
|
+
const {
|
|
2482
|
+
slug,
|
|
2483
|
+
locale,
|
|
2484
|
+
platform = "ios",
|
|
2485
|
+
country,
|
|
2486
|
+
seedKeywords = [],
|
|
2487
|
+
competitorApps = [],
|
|
2488
|
+
filename,
|
|
2489
|
+
writeTemplate = false,
|
|
2490
|
+
researchData
|
|
2491
|
+
} = input;
|
|
2492
|
+
const { config, locales } = loadProductLocales(slug);
|
|
2493
|
+
const primaryLocale = resolvePrimaryLocale(config, locales);
|
|
2494
|
+
const primaryLocaleData = locales[primaryLocale];
|
|
2495
|
+
const autoSeeds = [];
|
|
2496
|
+
const autoCompetitors = [];
|
|
2497
|
+
if (primaryLocaleData?.aso?.title) {
|
|
2498
|
+
autoSeeds.push(primaryLocaleData.aso.title);
|
|
2499
|
+
}
|
|
2500
|
+
const parsedKeywords = normalizeKeywords(primaryLocaleData?.aso?.keywords);
|
|
2501
|
+
autoSeeds.push(...parsedKeywords.slice(0, 5));
|
|
2502
|
+
if (config?.name) autoSeeds.push(config.name);
|
|
2503
|
+
if (config?.tagline) autoSeeds.push(config.tagline);
|
|
2504
|
+
if (platform === "ios") {
|
|
2505
|
+
if (config?.appStoreAppId) {
|
|
2506
|
+
autoCompetitors.push({ appId: String(config.appStoreAppId), platform });
|
|
2507
|
+
} else if (config?.bundleId) {
|
|
2508
|
+
autoCompetitors.push({ appId: config.bundleId, platform });
|
|
2509
|
+
}
|
|
2510
|
+
} else if (platform === "android" && config?.packageName) {
|
|
2511
|
+
autoCompetitors.push({ appId: config.packageName, platform });
|
|
2512
|
+
}
|
|
2513
|
+
const resolvedSeeds = seedKeywords.length > 0 ? seedKeywords : Array.from(new Set(autoSeeds));
|
|
2514
|
+
const resolvedCompetitors = competitorApps.length > 0 ? competitorApps : autoCompetitors;
|
|
2515
|
+
const resolvedCountry = country || (locale?.includes("-") ? locale.split("-")[1].toLowerCase() : "us");
|
|
2516
|
+
const researchDir = path10.join(
|
|
2517
|
+
getKeywordResearchDir(),
|
|
2518
|
+
"products",
|
|
2519
|
+
slug,
|
|
2520
|
+
"locales",
|
|
2521
|
+
locale
|
|
2522
|
+
);
|
|
2523
|
+
const defaultFileName = `keyword-research-${platform}-${resolvedCountry}.json`;
|
|
2524
|
+
const fileName = filename || defaultFileName;
|
|
2525
|
+
let outputPath = path10.join(researchDir, fileName);
|
|
2526
|
+
let fileAction;
|
|
2527
|
+
if (writeTemplate || researchData) {
|
|
2528
|
+
const payload = researchData ? (() => {
|
|
2529
|
+
try {
|
|
2530
|
+
return JSON.parse(researchData);
|
|
2531
|
+
} catch (err) {
|
|
2532
|
+
throw new Error(
|
|
2533
|
+
`Failed to parse researchData JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
2534
|
+
);
|
|
2535
|
+
}
|
|
2536
|
+
})() : buildTemplate({
|
|
2537
|
+
slug,
|
|
2538
|
+
locale,
|
|
2539
|
+
platform,
|
|
2540
|
+
country: resolvedCountry,
|
|
2541
|
+
seedKeywords: resolvedSeeds,
|
|
2542
|
+
competitorApps: resolvedCompetitors
|
|
2543
|
+
});
|
|
2544
|
+
outputPath = saveJsonFile({ researchDir, fileName, payload });
|
|
2545
|
+
fileAction = researchData ? "Saved provided researchData" : "Wrote template";
|
|
2546
|
+
}
|
|
2547
|
+
const templatePreview = JSON.stringify(
|
|
2548
|
+
buildTemplate({
|
|
2549
|
+
slug,
|
|
2550
|
+
locale,
|
|
2551
|
+
platform,
|
|
2552
|
+
country: resolvedCountry,
|
|
2553
|
+
seedKeywords: resolvedSeeds,
|
|
2554
|
+
competitorApps: resolvedCompetitors
|
|
2555
|
+
}),
|
|
2556
|
+
null,
|
|
2557
|
+
2
|
|
2558
|
+
);
|
|
2559
|
+
const lines = [];
|
|
2560
|
+
lines.push(`# Keyword research plan (${slug})`);
|
|
2561
|
+
lines.push(`Locale: ${locale} | Platform: ${platform} | Country: ${resolvedCountry}`);
|
|
2562
|
+
lines.push(`Primary locale detected: ${primaryLocale}`);
|
|
2563
|
+
lines.push(
|
|
2564
|
+
`Seeds: ${resolvedSeeds.length > 0 ? resolvedSeeds.join(", ") : "(none set; add seedKeywords or ensure ASO keywords/title exist)"}`
|
|
2565
|
+
);
|
|
2566
|
+
lines.push(
|
|
2567
|
+
`Competitors (from config if empty): ${resolvedCompetitors.length > 0 ? resolvedCompetitors.map((c) => `${c.platform}:${c.appId}`).join(", ") : "(none set; add competitorApps or set appStoreAppId/bundleId/packageName in config.json)"}`
|
|
2568
|
+
);
|
|
2569
|
+
lines.push("");
|
|
2570
|
+
lines.push("How to run (uses mcp-appstore):");
|
|
2571
|
+
lines.push(
|
|
2572
|
+
`1) Start the local mcp-appstore server for this run: node server.js (cwd: /ABSOLUTE/PATH/TO/pabal-web-mcp/external-tools/mcp-appstore). LLM should start it before calling tools and stop it after, if the client supports process management; otherwise, start/stop manually.`
|
|
2573
|
+
);
|
|
2574
|
+
lines.push(
|
|
2575
|
+
`2) Discover apps: search_app(term=seed, platform=${platform}, country=${country}); get_similar_apps(appId=known competitor).`
|
|
2576
|
+
);
|
|
2577
|
+
lines.push(
|
|
2578
|
+
`3) Expand keywords: suggest_keywords_by_seeds, suggest_keywords_by_category, suggest_keywords_by_similarity, suggest_keywords_by_competition, suggest_keywords_by_search.`
|
|
2579
|
+
);
|
|
2580
|
+
lines.push(
|
|
2581
|
+
`4) Score shortlist: get_keyword_scores for 15\u201330 candidates (note: scores are heuristic per README).`
|
|
2582
|
+
);
|
|
2583
|
+
lines.push(
|
|
2584
|
+
`5) Context check: analyze_reviews on top apps to harvest native phrasing; keep snippets for improve-public.`
|
|
2585
|
+
);
|
|
2586
|
+
lines.push(
|
|
2587
|
+
`6) Save all raw responses + your final top 10\u201315 keywords to: ${outputPath} (structure mirrors .aso/pullData/.aso/pushData under products/<slug>/locales/<locale>)`
|
|
2588
|
+
);
|
|
2589
|
+
if (fileAction) {
|
|
2590
|
+
lines.push(`File: ${fileAction} at ${outputPath}`);
|
|
2591
|
+
} else {
|
|
2592
|
+
lines.push(
|
|
2593
|
+
`Tip: set writeTemplate=true to create the JSON skeleton at ${outputPath}`
|
|
2594
|
+
);
|
|
2595
|
+
}
|
|
2596
|
+
lines.push("");
|
|
2597
|
+
lines.push("Suggested JSON shape:");
|
|
2598
|
+
lines.push("```json");
|
|
2599
|
+
lines.push(templatePreview);
|
|
2600
|
+
lines.push("```");
|
|
2601
|
+
return {
|
|
2602
|
+
content: [
|
|
2603
|
+
{
|
|
2604
|
+
type: "text",
|
|
2605
|
+
text: lines.join("\n")
|
|
2606
|
+
}
|
|
2607
|
+
]
|
|
2608
|
+
};
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2260
2611
|
// src/tools/index.ts
|
|
2261
2612
|
var tools = [
|
|
2262
2613
|
{
|
|
@@ -2298,6 +2649,14 @@ var tools = [
|
|
|
2298
2649
|
zodSchema: createBlogHtmlInputSchema,
|
|
2299
2650
|
handler: handleCreateBlogHtml,
|
|
2300
2651
|
category: "Content"
|
|
2652
|
+
},
|
|
2653
|
+
{
|
|
2654
|
+
name: keywordResearchTool.name,
|
|
2655
|
+
description: keywordResearchTool.description,
|
|
2656
|
+
inputSchema: keywordResearchTool.inputSchema,
|
|
2657
|
+
zodSchema: keywordResearchInputSchema,
|
|
2658
|
+
handler: handleKeywordResearch,
|
|
2659
|
+
category: "ASO Research"
|
|
2301
2660
|
}
|
|
2302
2661
|
];
|
|
2303
2662
|
function getToolDefinitions() {
|
|
@@ -2306,7 +2665,8 @@ function getToolDefinitions() {
|
|
|
2306
2665
|
publicToAsoTool,
|
|
2307
2666
|
improvePublicTool,
|
|
2308
2667
|
initProjectTool,
|
|
2309
|
-
createBlogHtmlTool
|
|
2668
|
+
createBlogHtmlTool,
|
|
2669
|
+
keywordResearchTool
|
|
2310
2670
|
];
|
|
2311
2671
|
}
|
|
2312
2672
|
function getToolHandler(name) {
|