pabal-web-mcp 1.2.4 → 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 +412 -54
- package/dist/chunk-W62HB2ZL.js +355 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,142 +2,58 @@
|
|
|
2
2
|
|
|
3
3
|
MCP (Model Context Protocol) server for bidirectional conversion between ASO (App Store Optimization) and web SEO data.
|
|
4
4
|
|
|
5
|
-
This library enables seamless reuse of ASO data for web SEO purposes, allowing you to convert ASO metadata directly into web SEO content and vice versa.
|
|
5
|
+
This library enables seamless reuse of ASO data for web SEO purposes, allowing you to convert ASO metadata directly into web SEO content and vice versa. **Build your own synced website** based on ASO data from App Store Connect and Google Play Console, keeping your app store listings and web presence perfectly synchronized.
|
|
6
|
+
|
|
7
|
+
> 💡 **Example**: Check out [labs.quartz.best](https://labs.quartz.best/) to see a live website built with this library, where app store data is automatically synced to create a beautiful, SEO-optimized web presence.
|
|
6
8
|
|
|
7
9
|
[](./i18n/README.ko.md)
|
|
8
10
|
|
|
9
|
-
## 🛠️
|
|
11
|
+
## 🛠️ Installation
|
|
10
12
|
|
|
11
13
|
### Requirements
|
|
12
14
|
|
|
13
15
|
- Node.js >= 18
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
> [!TIP]
|
|
17
|
-
> If you repeatedly do ASO/store tasks, add a client rule like "always use pabal-web-mcp" so the MCP server auto-invokes without typing it every time.
|
|
18
|
-
|
|
19
|
-
<details>
|
|
20
|
-
<summary><b>Install in Cursor</b></summary>
|
|
21
|
-
|
|
22
|
-
Add to `~/.cursor/mcp.json` (global) or project `.cursor/mcp.json`:
|
|
23
|
-
|
|
24
|
-
```json
|
|
25
|
-
{
|
|
26
|
-
"mcpServers": {
|
|
27
|
-
"pabal-web-mcp": {
|
|
28
|
-
"command": "npx",
|
|
29
|
-
"args": ["-y", "pabal-web-mcp"]
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Or if installed globally:
|
|
36
|
-
|
|
37
|
-
```json
|
|
38
|
-
{
|
|
39
|
-
"mcpServers": {
|
|
40
|
-
"pabal-web-mcp": {
|
|
41
|
-
"command": "pabal-web-mcp"
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
</details>
|
|
48
|
-
|
|
49
|
-
<details>
|
|
50
|
-
<summary><b>Install in VS Code</b></summary>
|
|
51
|
-
|
|
52
|
-
Example `settings.json` MCP section:
|
|
53
|
-
|
|
54
|
-
```json
|
|
55
|
-
"mcp": {
|
|
56
|
-
"servers": {
|
|
57
|
-
"pabal-web-mcp": {
|
|
58
|
-
"type": "stdio",
|
|
59
|
-
"command": "npx",
|
|
60
|
-
"args": ["-y", "pabal-web-mcp"]
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
Or if installed globally:
|
|
67
|
-
|
|
68
|
-
```json
|
|
69
|
-
"mcp": {
|
|
70
|
-
"servers": {
|
|
71
|
-
"pabal-web-mcp": {
|
|
72
|
-
"type": "stdio",
|
|
73
|
-
"command": "pabal-web-mcp"
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
</details>
|
|
80
|
-
|
|
81
|
-
<details>
|
|
82
|
-
<summary><b>Install in Claude Code</b></summary>
|
|
16
|
+
- [pabal-mcp](https://github.com/quartz-labs-dev/pabal-mcp) must be installed and configured
|
|
83
17
|
|
|
84
|
-
|
|
85
|
-
> See the [official Claude Code MCP documentation](https://code.claude.com/docs/en/mcp#setting-up-enterprise-mcp-configuration) for detailed configuration options.
|
|
18
|
+
### Install as Library
|
|
86
19
|
|
|
87
|
-
|
|
20
|
+
Install this library in your website project:
|
|
88
21
|
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
22
|
+
```bash
|
|
23
|
+
npm install pabal-web-mcp
|
|
24
|
+
# or
|
|
25
|
+
yarn add pabal-web-mcp
|
|
26
|
+
# or
|
|
27
|
+
pnpm add pabal-web-mcp
|
|
98
28
|
```
|
|
99
29
|
|
|
100
|
-
|
|
30
|
+
## 🔐 Configure Credentials
|
|
101
31
|
|
|
102
|
-
|
|
103
|
-
{
|
|
104
|
-
"mcpServers": {
|
|
105
|
-
"pabal-web-mcp": {
|
|
106
|
-
"command": "pabal-web-mcp"
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
```
|
|
32
|
+
pabal-web-mcp uses the configuration file from `pabal-mcp`. For detailed credential setup instructions (App Store Connect API keys, Google Play service accounts, etc.), please refer to the [pabal-mcp README](https://github.com/quartz-labs-dev/pabal-mcp?tab=readme-ov-file#-configure-credentials).
|
|
111
33
|
|
|
112
|
-
|
|
34
|
+
### ⚠️ Important: Set dataDir Path
|
|
113
35
|
|
|
114
|
-
|
|
115
|
-
<summary><b>Install in Windsurf</b></summary>
|
|
36
|
+
**You must set `dataDir` in `~/.config/pabal-mcp/config.json` to the absolute path where your `pabal-web` project is stored on your local machine.**
|
|
116
37
|
|
|
117
38
|
```json
|
|
118
39
|
{
|
|
119
|
-
"
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
40
|
+
"dataDir": "/ABSOLUTE/PATH/TO/pabal-web",
|
|
41
|
+
"appStore": {
|
|
42
|
+
"issuerId": "xxxx",
|
|
43
|
+
"keyId": "xxxx",
|
|
44
|
+
"privateKeyPath": "./app-store-key.p8"
|
|
45
|
+
},
|
|
46
|
+
"googlePlay": {
|
|
47
|
+
"serviceAccountKeyPath": "./google-play-service-account.json"
|
|
124
48
|
}
|
|
125
49
|
}
|
|
126
50
|
```
|
|
127
51
|
|
|
128
|
-
|
|
52
|
+
Examples:
|
|
129
53
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
"pabal-web-mcp": {
|
|
134
|
-
"command": "pabal-web-mcp"
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
</details>
|
|
54
|
+
- macOS: `"/Users/username/projects/pabal-web"`
|
|
55
|
+
- Linux: `"/home/username/projects/pabal-web"`
|
|
56
|
+
- Windows: `"C:\\Users\\username\\projects\\pabal-web"`
|
|
141
57
|
|
|
142
58
|
## MCP Server
|
|
143
59
|
|
|
@@ -145,87 +61,34 @@ This package includes an MCP server for managing ASO data through Claude or othe
|
|
|
145
61
|
|
|
146
62
|
### Available Tools
|
|
147
63
|
|
|
148
|
-
| Tool
|
|
149
|
-
|
|
|
150
|
-
| `aso-to-public`
|
|
151
|
-
| `public-to-aso`
|
|
152
|
-
| `
|
|
153
|
-
| `
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
###
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
### Importing Utilities
|
|
179
|
-
|
|
180
|
-
```typescript
|
|
181
|
-
import {
|
|
182
|
-
// ASO Converter
|
|
183
|
-
loadAsoFromConfig,
|
|
184
|
-
|
|
185
|
-
// Locale Constants
|
|
186
|
-
DEFAULT_LOCALE,
|
|
187
|
-
UNIFIED_LOCALES,
|
|
188
|
-
|
|
189
|
-
// Locale Converters
|
|
190
|
-
unifiedToAppStore,
|
|
191
|
-
unifiedToGooglePlay,
|
|
192
|
-
appStoreToUnified,
|
|
193
|
-
googlePlayToUnified,
|
|
194
|
-
} from "pabal-web-mcp";
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
### Example: Loading ASO Data
|
|
198
|
-
|
|
199
|
-
```typescript
|
|
200
|
-
import { loadAsoFromConfig } from "pabal-web-mcp";
|
|
201
|
-
|
|
202
|
-
const asoData = loadAsoFromConfig("my-app");
|
|
203
|
-
console.log(asoData.appStore?.name);
|
|
204
|
-
console.log(asoData.googlePlay?.title);
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
## Types Reference
|
|
208
|
-
|
|
209
|
-
### ASO Types
|
|
210
|
-
|
|
211
|
-
- `AsoData` - Combined ASO data for both stores
|
|
212
|
-
- `AppStoreAsoData` - App Store specific ASO data
|
|
213
|
-
- `GooglePlayAsoData` - Google Play specific ASO data
|
|
214
|
-
- `AppStoreMultilingualAsoData` - Multilingual App Store data
|
|
215
|
-
- `GooglePlayMultilingualAsoData` - Multilingual Google Play data
|
|
216
|
-
|
|
217
|
-
### Product Types
|
|
218
|
-
|
|
219
|
-
- `ProductConfig` - Product configuration
|
|
220
|
-
- `ProductLocale` - Localized product content
|
|
221
|
-
- `LandingPage` - Landing page structure
|
|
222
|
-
- `AppPageData` - Complete app page data
|
|
223
|
-
|
|
224
|
-
### Locale Types
|
|
225
|
-
|
|
226
|
-
- `UnifiedLocale` - Unified locale code (e.g., "en-US", "ko-KR")
|
|
227
|
-
|
|
228
|
-
## Supported Locales
|
|
64
|
+
| Tool | Description |
|
|
65
|
+
| ------------------ | -------------------------------------------------------- |
|
|
66
|
+
| `aso-to-public` | Convert ASO data to public config format |
|
|
67
|
+
| `public-to-aso` | Convert public config to ASO data format |
|
|
68
|
+
| `keyword-research` | Plan/persist ASO keyword research (.aso/keywordResearch) |
|
|
69
|
+
| `improve-public` | Improve product locale content with AI suggestions |
|
|
70
|
+
| `init-project` | Initialize a new product project structure |
|
|
71
|
+
| `create-blog-html` | Generate static HTML blog posts with BLOG_META headers |
|
|
72
|
+
|
|
73
|
+
### Using external keyword MCP ([appreply-co/mcp-appstore](https://github.com/appreply-co/mcp-appstore))
|
|
74
|
+
|
|
75
|
+
1. Install deps in the existing clone: `cd external-tools/mcp-appstore && npm install`
|
|
76
|
+
2. Run server: `node server.js` (same cwd; `npm start` also works). If your MCP client allows, let the LLM start this process before keyword research and stop it after; otherwise start/stop it manually.
|
|
77
|
+
3. Register in your MCP client (example):
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"mcpServers": {
|
|
81
|
+
"mcp-appstore": {
|
|
82
|
+
"command": "node",
|
|
83
|
+
"args": ["/ABSOLUTE/PATH/TO/pabal-web-mcp/external-tools/mcp-appstore/server.js"],
|
|
84
|
+
"cwd": "/ABSOLUTE/PATH/TO/pabal-web-mcp/external-tools/mcp-appstore"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
4. Use it with `keyword-research` (saves to `.aso/keywordResearch/...`) before `improve-public` to supply keyword data.
|
|
90
|
+
|
|
91
|
+
### Supported Locales
|
|
229
92
|
|
|
230
93
|
Supports all languages supported by each store.
|
|
231
94
|
|
package/dist/bin/mcp-server.js
CHANGED
|
@@ -3,13 +3,14 @@ import {
|
|
|
3
3
|
DEFAULT_APP_SLUG
|
|
4
4
|
} from "../chunk-DLCIXAUB.js";
|
|
5
5
|
import {
|
|
6
|
+
getKeywordResearchDir,
|
|
6
7
|
getProductsDir,
|
|
7
8
|
getPublicDir,
|
|
8
9
|
getPullDataDir,
|
|
9
10
|
getPushDataDir,
|
|
10
11
|
loadAsoFromConfig,
|
|
11
12
|
saveAsoToAsoDir
|
|
12
|
-
} from "../chunk-
|
|
13
|
+
} from "../chunk-W62HB2ZL.js";
|
|
13
14
|
import {
|
|
14
15
|
DEFAULT_LOCALE,
|
|
15
16
|
appStoreToUnified,
|
|
@@ -1194,7 +1195,14 @@ ${json}
|
|
|
1194
1195
|
// src/tools/utils/improve-public/generate-aso-prompt.util.ts
|
|
1195
1196
|
var FIELD_LIMITS_DOC_PATH2 = "docs/aso/ASO_FIELD_LIMITS.md";
|
|
1196
1197
|
function generatePrimaryOptimizationPrompt(args) {
|
|
1197
|
-
const {
|
|
1198
|
+
const {
|
|
1199
|
+
slug,
|
|
1200
|
+
category,
|
|
1201
|
+
primaryLocale,
|
|
1202
|
+
localeSections,
|
|
1203
|
+
keywordResearchByLocale,
|
|
1204
|
+
keywordResearchDirByLocale
|
|
1205
|
+
} = args;
|
|
1198
1206
|
let prompt = `# ASO Optimization - Stage 1: Primary Locale
|
|
1199
1207
|
|
|
1200
1208
|
`;
|
|
@@ -1204,32 +1212,33 @@ function generatePrimaryOptimizationPrompt(args) {
|
|
|
1204
1212
|
prompt += `## Task
|
|
1205
1213
|
|
|
1206
1214
|
`;
|
|
1207
|
-
prompt += `Optimize the PRIMARY locale (${primaryLocale})
|
|
1215
|
+
prompt += `Optimize the PRIMARY locale (${primaryLocale}) using **saved keyword research** + full ASO field optimization.
|
|
1208
1216
|
|
|
1209
1217
|
`;
|
|
1210
1218
|
prompt += `## Step 1: Keyword Research (${primaryLocale})
|
|
1211
1219
|
|
|
1212
1220
|
`;
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
`;
|
|
1219
|
-
prompt += `3. **Feature-Based**: Identify 2-3 core features \u2192 search feature-specific keywords
|
|
1220
|
-
`;
|
|
1221
|
-
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
|
+
|
|
1222
1226
|
`;
|
|
1223
|
-
|
|
1227
|
+
prompt += `Saved research:
|
|
1228
|
+
${researchSections.join("\n")}
|
|
1224
1229
|
|
|
1225
1230
|
`;
|
|
1226
|
-
|
|
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.
|
|
1227
1235
|
|
|
1228
1236
|
`;
|
|
1237
|
+
}
|
|
1229
1238
|
prompt += `## Step 2: Optimize All Fields (${primaryLocale})
|
|
1230
1239
|
|
|
1231
1240
|
`;
|
|
1232
|
-
prompt += `Apply the
|
|
1241
|
+
prompt += `Apply the selected keywords to ALL fields:
|
|
1233
1242
|
`;
|
|
1234
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)
|
|
1235
1244
|
`;
|
|
@@ -1284,11 +1293,11 @@ function generatePrimaryOptimizationPrompt(args) {
|
|
|
1284
1293
|
prompt += `## Output Format
|
|
1285
1294
|
|
|
1286
1295
|
`;
|
|
1287
|
-
prompt += `**1. Keyword Research**
|
|
1296
|
+
prompt += `**1. Keyword Research (from saved data)**
|
|
1288
1297
|
`;
|
|
1289
|
-
prompt += ` -
|
|
1298
|
+
prompt += ` - Cite file(s) used and list the selected top 10 keywords (no new research)
|
|
1290
1299
|
`;
|
|
1291
|
-
prompt += ` -
|
|
1300
|
+
prompt += ` - Rationale: why these 10 were chosen from saved research
|
|
1292
1301
|
|
|
1293
1302
|
`;
|
|
1294
1303
|
prompt += `**2. Optimized JSON** (complete ${primaryLocale} locale structure)
|
|
@@ -1336,6 +1345,8 @@ function generateKeywordLocalizationPrompt(args) {
|
|
|
1336
1345
|
targetLocales,
|
|
1337
1346
|
localeSections,
|
|
1338
1347
|
optimizedPrimary,
|
|
1348
|
+
keywordResearchByLocale,
|
|
1349
|
+
keywordResearchDirByLocale,
|
|
1339
1350
|
batchLocales,
|
|
1340
1351
|
batchIndex,
|
|
1341
1352
|
totalBatches,
|
|
@@ -1379,7 +1390,7 @@ function generateKeywordLocalizationPrompt(args) {
|
|
|
1379
1390
|
`;
|
|
1380
1391
|
prompt += `For EACH target locale in this batch:
|
|
1381
1392
|
`;
|
|
1382
|
-
prompt += `1.
|
|
1393
|
+
prompt += `1. Use SAVED keyword research (see per-locale data below). Do NOT invent keywords.
|
|
1383
1394
|
`;
|
|
1384
1395
|
prompt += `2. Replace keywords in translated content (preserve structure/tone/context)
|
|
1385
1396
|
`;
|
|
@@ -1400,18 +1411,22 @@ ${optimizedPrimary}
|
|
|
1400
1411
|
prompt += `## Keyword Research (Per Locale)
|
|
1401
1412
|
|
|
1402
1413
|
`;
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
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
|
+
)}
|
|
1410
1422
|
|
|
1411
1423
|
`;
|
|
1412
|
-
|
|
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.
|
|
1413
1426
|
|
|
1414
1427
|
`;
|
|
1428
|
+
}
|
|
1429
|
+
});
|
|
1415
1430
|
prompt += `## Keyword Replacement Strategy
|
|
1416
1431
|
|
|
1417
1432
|
`;
|
|
@@ -1492,7 +1507,7 @@ ${optimizedPrimary}
|
|
|
1492
1507
|
`;
|
|
1493
1508
|
prompt += `Process EACH locale in this batch sequentially:
|
|
1494
1509
|
`;
|
|
1495
|
-
prompt += `1.
|
|
1510
|
+
prompt += `1. Use saved keyword research (or pause if missing and request keyword-research run)
|
|
1496
1511
|
`;
|
|
1497
1512
|
prompt += `2. Replace keywords in ALL fields:
|
|
1498
1513
|
`;
|
|
@@ -1548,11 +1563,11 @@ ${optimizedPrimary}
|
|
|
1548
1563
|
prompt += `### Locale [locale-code]:
|
|
1549
1564
|
|
|
1550
1565
|
`;
|
|
1551
|
-
prompt += `**1. Keyword Research**
|
|
1566
|
+
prompt += `**1. Keyword Research (saved)**
|
|
1552
1567
|
`;
|
|
1553
|
-
prompt += ` -
|
|
1568
|
+
prompt += ` - Cite file(s) used; list selected top 10 keywords (no new research)
|
|
1554
1569
|
`;
|
|
1555
|
-
prompt += ` -
|
|
1570
|
+
prompt += ` - Rationale: why these were chosen from saved research
|
|
1556
1571
|
|
|
1557
1572
|
`;
|
|
1558
1573
|
prompt += `**2. Updated JSON** (complete locale structure with keyword replacements)
|
|
@@ -1596,6 +1611,92 @@ ${optimizedPrimary}
|
|
|
1596
1611
|
return prompt;
|
|
1597
1612
|
}
|
|
1598
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
|
+
|
|
1599
1700
|
// src/tools/improve-public.ts
|
|
1600
1701
|
var FIELD_LIMITS_DOC_PATH3 = "docs/aso/ASO_FIELD_LIMITS.md";
|
|
1601
1702
|
var toJsonSchema3 = zodToJsonSchema3;
|
|
@@ -1712,12 +1813,21 @@ async function handleImprovePublic(input) {
|
|
|
1712
1813
|
})
|
|
1713
1814
|
);
|
|
1714
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
|
+
}
|
|
1715
1823
|
const baseArgs = {
|
|
1716
1824
|
slug,
|
|
1717
1825
|
category,
|
|
1718
1826
|
primaryLocale,
|
|
1719
1827
|
targetLocales,
|
|
1720
|
-
localeSections
|
|
1828
|
+
localeSections,
|
|
1829
|
+
keywordResearchByLocale,
|
|
1830
|
+
keywordResearchDirByLocale
|
|
1721
1831
|
};
|
|
1722
1832
|
if (stage === "1" || stage === "both") {
|
|
1723
1833
|
const prompt = generatePrimaryOptimizationPrompt(baseArgs);
|
|
@@ -1763,6 +1873,8 @@ async function handleImprovePublic(input) {
|
|
|
1763
1873
|
primaryLocale: baseArgs.primaryLocale,
|
|
1764
1874
|
targetLocales: baseArgs.targetLocales,
|
|
1765
1875
|
localeSections: baseArgs.localeSections,
|
|
1876
|
+
keywordResearchByLocale: baseArgs.keywordResearchByLocale,
|
|
1877
|
+
keywordResearchDirByLocale: baseArgs.keywordResearchDirByLocale,
|
|
1766
1878
|
optimizedPrimary,
|
|
1767
1879
|
batchLocales,
|
|
1768
1880
|
batchIndex: currentBatchIndex,
|
|
@@ -1785,13 +1897,13 @@ async function handleImprovePublic(input) {
|
|
|
1785
1897
|
}
|
|
1786
1898
|
|
|
1787
1899
|
// src/tools/init-project.ts
|
|
1788
|
-
import
|
|
1789
|
-
import
|
|
1900
|
+
import fs7 from "fs";
|
|
1901
|
+
import path7 from "path";
|
|
1790
1902
|
import { z as z4 } from "zod";
|
|
1791
1903
|
import { zodToJsonSchema as zodToJsonSchema4 } from "zod-to-json-schema";
|
|
1792
1904
|
var listSlugDirs = (dir) => {
|
|
1793
|
-
if (!
|
|
1794
|
-
return
|
|
1905
|
+
if (!fs7.existsSync(dir)) return [];
|
|
1906
|
+
return fs7.readdirSync(dir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
|
|
1795
1907
|
};
|
|
1796
1908
|
var initProjectInputSchema = z4.object({
|
|
1797
1909
|
slug: z4.string().trim().optional().describe(
|
|
@@ -1816,7 +1928,7 @@ Steps:
|
|
|
1816
1928
|
inputSchema: inputSchema4
|
|
1817
1929
|
};
|
|
1818
1930
|
async function handleInitProject(input) {
|
|
1819
|
-
const pullDataDir =
|
|
1931
|
+
const pullDataDir = path7.join(getPullDataDir(), "products");
|
|
1820
1932
|
const publicDir = getProductsDir();
|
|
1821
1933
|
const pullDataSlugs = listSlugDirs(pullDataDir);
|
|
1822
1934
|
const publicSlugs = listSlugDirs(publicDir);
|
|
@@ -1894,14 +2006,14 @@ async function handleInitProject(input) {
|
|
|
1894
2006
|
}
|
|
1895
2007
|
|
|
1896
2008
|
// src/tools/create-blog-html.ts
|
|
1897
|
-
import
|
|
1898
|
-
import
|
|
2009
|
+
import fs9 from "fs";
|
|
2010
|
+
import path9 from "path";
|
|
1899
2011
|
import { z as z5 } from "zod";
|
|
1900
2012
|
import { zodToJsonSchema as zodToJsonSchema5 } from "zod-to-json-schema";
|
|
1901
2013
|
|
|
1902
2014
|
// src/utils/blog.util.ts
|
|
1903
|
-
import
|
|
1904
|
-
import
|
|
2015
|
+
import fs8 from "fs";
|
|
2016
|
+
import path8 from "path";
|
|
1905
2017
|
var DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
|
|
1906
2018
|
var BLOG_ROOT = "blogs";
|
|
1907
2019
|
var removeDiacritics = (value) => value.normalize("NFKD").replace(/[\u0300-\u036f]/g, "");
|
|
@@ -1989,13 +2101,13 @@ function resolveTargetLocales(input) {
|
|
|
1989
2101
|
return fallback ? [fallback] : [];
|
|
1990
2102
|
}
|
|
1991
2103
|
function getBlogOutputPaths(options) {
|
|
1992
|
-
const baseDir =
|
|
2104
|
+
const baseDir = path8.join(
|
|
1993
2105
|
options.publicDir,
|
|
1994
2106
|
BLOG_ROOT,
|
|
1995
2107
|
options.appSlug,
|
|
1996
2108
|
options.slug
|
|
1997
2109
|
);
|
|
1998
|
-
const filePath =
|
|
2110
|
+
const filePath = path8.join(baseDir, `${options.locale}.html`);
|
|
1999
2111
|
const publicBasePath = toPublicBlogBase(options.appSlug, options.slug);
|
|
2000
2112
|
return { baseDir, filePath, publicBasePath };
|
|
2001
2113
|
}
|
|
@@ -2020,18 +2132,18 @@ function findExistingBlogPosts({
|
|
|
2020
2132
|
publicDir,
|
|
2021
2133
|
limit = 2
|
|
2022
2134
|
}) {
|
|
2023
|
-
const blogAppDir =
|
|
2024
|
-
if (!
|
|
2135
|
+
const blogAppDir = path8.join(publicDir, BLOG_ROOT, appSlug);
|
|
2136
|
+
if (!fs8.existsSync(blogAppDir)) {
|
|
2025
2137
|
return [];
|
|
2026
2138
|
}
|
|
2027
2139
|
const posts = [];
|
|
2028
|
-
const subdirs =
|
|
2140
|
+
const subdirs = fs8.readdirSync(blogAppDir, { withFileTypes: true });
|
|
2029
2141
|
for (const subdir of subdirs) {
|
|
2030
2142
|
if (!subdir.isDirectory()) continue;
|
|
2031
|
-
const localeFile =
|
|
2032
|
-
if (!
|
|
2143
|
+
const localeFile = path8.join(blogAppDir, subdir.name, `${locale}.html`);
|
|
2144
|
+
if (!fs8.existsSync(localeFile)) continue;
|
|
2033
2145
|
try {
|
|
2034
|
-
const htmlContent =
|
|
2146
|
+
const htmlContent = fs8.readFileSync(localeFile, "utf-8");
|
|
2035
2147
|
const { meta, body } = parseBlogHtml(htmlContent);
|
|
2036
2148
|
if (meta && meta.locale === locale) {
|
|
2037
2149
|
posts.push({
|
|
@@ -2174,7 +2286,7 @@ async function handleCreateBlogHtml(input) {
|
|
|
2174
2286
|
}
|
|
2175
2287
|
const output = {
|
|
2176
2288
|
slug,
|
|
2177
|
-
baseDir:
|
|
2289
|
+
baseDir: path9.join(publicDir, "blogs", appSlug, slug),
|
|
2178
2290
|
files: [],
|
|
2179
2291
|
coverImage: coverImage && coverImage.trim().length > 0 ? coverImage.trim() : `/products/${appSlug}/og-image.png`,
|
|
2180
2292
|
metaByLocale: {}
|
|
@@ -2188,7 +2300,7 @@ async function handleCreateBlogHtml(input) {
|
|
|
2188
2300
|
})
|
|
2189
2301
|
);
|
|
2190
2302
|
const existing = plannedFiles.filter(
|
|
2191
|
-
({ filePath }) =>
|
|
2303
|
+
({ filePath }) => fs9.existsSync(filePath)
|
|
2192
2304
|
);
|
|
2193
2305
|
if (existing.length > 0 && !overwrite) {
|
|
2194
2306
|
const existingList = existing.map((f) => f.filePath).join("\n- ");
|
|
@@ -2197,7 +2309,7 @@ async function handleCreateBlogHtml(input) {
|
|
|
2197
2309
|
- ${existingList}`
|
|
2198
2310
|
);
|
|
2199
2311
|
}
|
|
2200
|
-
|
|
2312
|
+
fs9.mkdirSync(output.baseDir, { recursive: true });
|
|
2201
2313
|
for (const locale of targetLocales) {
|
|
2202
2314
|
const { filePath } = getBlogOutputPaths({
|
|
2203
2315
|
appSlug,
|
|
@@ -2223,7 +2335,7 @@ async function handleCreateBlogHtml(input) {
|
|
|
2223
2335
|
meta,
|
|
2224
2336
|
content
|
|
2225
2337
|
});
|
|
2226
|
-
|
|
2338
|
+
fs9.writeFileSync(filePath, html, "utf-8");
|
|
2227
2339
|
output.files.push({ locale, path: filePath });
|
|
2228
2340
|
}
|
|
2229
2341
|
const summaryLines = [
|
|
@@ -2259,6 +2371,243 @@ Writing style reference for ${locale}: Found ${posts.length} existing post(s) us
|
|
|
2259
2371
|
};
|
|
2260
2372
|
}
|
|
2261
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
|
+
|
|
2262
2611
|
// src/tools/index.ts
|
|
2263
2612
|
var tools = [
|
|
2264
2613
|
{
|
|
@@ -2300,6 +2649,14 @@ var tools = [
|
|
|
2300
2649
|
zodSchema: createBlogHtmlInputSchema,
|
|
2301
2650
|
handler: handleCreateBlogHtml,
|
|
2302
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"
|
|
2303
2660
|
}
|
|
2304
2661
|
];
|
|
2305
2662
|
function getToolDefinitions() {
|
|
@@ -2308,7 +2665,8 @@ function getToolDefinitions() {
|
|
|
2308
2665
|
publicToAsoTool,
|
|
2309
2666
|
improvePublicTool,
|
|
2310
2667
|
initProjectTool,
|
|
2311
|
-
createBlogHtmlTool
|
|
2668
|
+
createBlogHtmlTool,
|
|
2669
|
+
keywordResearchTool
|
|
2312
2670
|
];
|
|
2313
2671
|
}
|
|
2314
2672
|
function getToolHandler(name) {
|
|
@@ -0,0 +1,355 @@
|
|
|
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
|
+
|
|
61
|
+
// src/utils/aso-converter.ts
|
|
62
|
+
import fs2 from "fs";
|
|
63
|
+
import path2 from "path";
|
|
64
|
+
function generateFullDescription(localeData, metadata = {}) {
|
|
65
|
+
const { aso, landing } = localeData;
|
|
66
|
+
const template = aso?.template;
|
|
67
|
+
if (!template) {
|
|
68
|
+
return "";
|
|
69
|
+
}
|
|
70
|
+
const landingFeatures = landing?.features?.items || [];
|
|
71
|
+
const landingScreenshots = landing?.screenshots?.images || [];
|
|
72
|
+
const keyHeading = template.keyFeaturesHeading || "Key Features";
|
|
73
|
+
const featuresHeading = template.featuresHeading || "Additional Features";
|
|
74
|
+
const parts = [template.intro];
|
|
75
|
+
if (landingFeatures.length > 0) {
|
|
76
|
+
parts.push(
|
|
77
|
+
"",
|
|
78
|
+
keyHeading,
|
|
79
|
+
"",
|
|
80
|
+
...landingFeatures.map(
|
|
81
|
+
(feature) => [`\u25B6\uFE0E ${feature.title}`, feature.body || ""].filter(Boolean).join("\n")
|
|
82
|
+
)
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (landingScreenshots.length > 0) {
|
|
86
|
+
parts.push("", featuresHeading, "");
|
|
87
|
+
parts.push(
|
|
88
|
+
...landingScreenshots.map(
|
|
89
|
+
(screenshot) => [`\u25B6\uFE0E ${screenshot.title}`, screenshot.description || ""].filter(Boolean).join("\n")
|
|
90
|
+
)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
parts.push("", template.outro);
|
|
94
|
+
const includeSupport = template.includeSupportLinks ?? true;
|
|
95
|
+
if (includeSupport) {
|
|
96
|
+
const contactLines = [
|
|
97
|
+
metadata.instagram ? `Instagram: ${metadata.instagram}` : null,
|
|
98
|
+
metadata.contactEmail ? `Email: ${metadata.contactEmail}` : null,
|
|
99
|
+
metadata.termsUrl ? `- Terms of Use: ${metadata.termsUrl}` : null,
|
|
100
|
+
metadata.privacyUrl ? `- Privacy Policy: ${metadata.privacyUrl}` : null
|
|
101
|
+
].filter((line) => line !== null);
|
|
102
|
+
if (contactLines.length > 0) {
|
|
103
|
+
parts.push("", "[Contact & Support]", "", ...contactLines);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return parts.join("\n");
|
|
107
|
+
}
|
|
108
|
+
function loadAsoFromConfig(slug) {
|
|
109
|
+
const productsDir = getProductsDir();
|
|
110
|
+
const configPath = path2.join(productsDir, slug, "config.json");
|
|
111
|
+
console.debug(`[loadAsoFromConfig] Looking for ${slug}:`);
|
|
112
|
+
console.debug(` - productsDir: ${productsDir}`);
|
|
113
|
+
console.debug(` - configPath: ${configPath}`);
|
|
114
|
+
console.debug(` - configPath exists: ${fs2.existsSync(configPath)}`);
|
|
115
|
+
if (!fs2.existsSync(configPath)) {
|
|
116
|
+
console.warn(`[loadAsoFromConfig] Config file not found at ${configPath}`);
|
|
117
|
+
return {};
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const configContent = fs2.readFileSync(configPath, "utf-8");
|
|
121
|
+
const config = JSON.parse(configContent);
|
|
122
|
+
const localesDir = path2.join(productsDir, slug, "locales");
|
|
123
|
+
console.debug(` - localesDir: ${localesDir}`);
|
|
124
|
+
console.debug(` - localesDir exists: ${fs2.existsSync(localesDir)}`);
|
|
125
|
+
if (!fs2.existsSync(localesDir)) {
|
|
126
|
+
console.warn(
|
|
127
|
+
`[loadAsoFromConfig] Locales directory not found at ${localesDir}`
|
|
128
|
+
);
|
|
129
|
+
return {};
|
|
130
|
+
}
|
|
131
|
+
const localeFiles = fs2.readdirSync(localesDir).filter((f) => f.endsWith(".json"));
|
|
132
|
+
const locales = {};
|
|
133
|
+
for (const file of localeFiles) {
|
|
134
|
+
const localeCode = file.replace(".json", "");
|
|
135
|
+
const localePath = path2.join(localesDir, file);
|
|
136
|
+
const localeContent = fs2.readFileSync(localePath, "utf-8");
|
|
137
|
+
locales[localeCode] = JSON.parse(localeContent);
|
|
138
|
+
}
|
|
139
|
+
console.debug(
|
|
140
|
+
` - Found ${Object.keys(locales).length} locale file(s): ${Object.keys(
|
|
141
|
+
locales
|
|
142
|
+
).join(", ")}`
|
|
143
|
+
);
|
|
144
|
+
if (Object.keys(locales).length === 0) {
|
|
145
|
+
console.warn(
|
|
146
|
+
`[loadAsoFromConfig] No locale files found in ${localesDir}`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
const defaultLocale = config.content?.defaultLocale || DEFAULT_LOCALE;
|
|
150
|
+
const asoData = {};
|
|
151
|
+
if (config.packageName) {
|
|
152
|
+
const googlePlayLocales = {};
|
|
153
|
+
const metadata = config.metadata || {};
|
|
154
|
+
const screenshots = metadata.screenshots || {};
|
|
155
|
+
for (const [locale, localeData] of Object.entries(locales)) {
|
|
156
|
+
if (!isSupportedLocale(locale)) {
|
|
157
|
+
console.debug(
|
|
158
|
+
`Skipping locale ${locale} - not a valid unified locale`
|
|
159
|
+
);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (!isGooglePlayLocale(locale)) {
|
|
163
|
+
console.debug(
|
|
164
|
+
`Skipping locale ${locale} - not supported by Google Play`
|
|
165
|
+
);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const aso = localeData.aso || {};
|
|
169
|
+
if (!aso || !aso.title && !aso.shortDescription) {
|
|
170
|
+
console.warn(
|
|
171
|
+
`Locale ${locale} has no ASO data (title or shortDescription)`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
const localeScreenshots = {
|
|
175
|
+
phone: screenshots.phone?.map(
|
|
176
|
+
(p) => p.replace("/screenshots/", `/screenshots/${locale}/`)
|
|
177
|
+
),
|
|
178
|
+
tablet: screenshots.tablet?.map(
|
|
179
|
+
(p) => p.replace("/screenshots/", `/screenshots/${locale}/`)
|
|
180
|
+
)
|
|
181
|
+
};
|
|
182
|
+
googlePlayLocales[locale] = {
|
|
183
|
+
title: aso.title || "",
|
|
184
|
+
shortDescription: aso.shortDescription || "",
|
|
185
|
+
fullDescription: generateFullDescription(localeData, metadata),
|
|
186
|
+
packageName: config.packageName,
|
|
187
|
+
defaultLanguage: locale,
|
|
188
|
+
screenshots: {
|
|
189
|
+
phone: localeScreenshots.phone || [],
|
|
190
|
+
tablet: localeScreenshots.tablet
|
|
191
|
+
},
|
|
192
|
+
contactEmail: metadata.contactEmail
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
const googleLocaleKeys = Object.keys(googlePlayLocales);
|
|
196
|
+
if (googleLocaleKeys.length > 0) {
|
|
197
|
+
const hasConfigDefault = isGooglePlayLocale(defaultLocale) && Boolean(googlePlayLocales[defaultLocale]);
|
|
198
|
+
const resolvedDefault = hasConfigDefault ? defaultLocale : googlePlayLocales[DEFAULT_LOCALE] ? DEFAULT_LOCALE : googleLocaleKeys[0];
|
|
199
|
+
asoData.googlePlay = {
|
|
200
|
+
locales: googlePlayLocales,
|
|
201
|
+
defaultLocale: resolvedDefault
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (config.bundleId) {
|
|
206
|
+
const appStoreLocales = {};
|
|
207
|
+
const metadata = config.metadata || {};
|
|
208
|
+
const screenshots = metadata.screenshots || {};
|
|
209
|
+
for (const [locale, localeData] of Object.entries(locales)) {
|
|
210
|
+
if (!isSupportedLocale(locale)) {
|
|
211
|
+
console.debug(
|
|
212
|
+
`Skipping locale ${locale} - not a valid unified locale`
|
|
213
|
+
);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (!isAppStoreLocale(locale)) {
|
|
217
|
+
console.debug(
|
|
218
|
+
`Skipping locale ${locale} - not supported by App Store`
|
|
219
|
+
);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const aso = localeData.aso || {};
|
|
223
|
+
if (!aso || !aso.title && !aso.shortDescription) {
|
|
224
|
+
console.warn(
|
|
225
|
+
`Locale ${locale} has no ASO data (title or shortDescription)`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
const localeScreenshots = {
|
|
229
|
+
phone: screenshots.phone?.map(
|
|
230
|
+
(p) => p.replace("/screenshots/", `/screenshots/${locale}/`)
|
|
231
|
+
),
|
|
232
|
+
tablet: screenshots.tablet?.map(
|
|
233
|
+
(p) => p.replace("/screenshots/", `/screenshots/${locale}/`)
|
|
234
|
+
)
|
|
235
|
+
};
|
|
236
|
+
appStoreLocales[locale] = {
|
|
237
|
+
name: aso.title || "",
|
|
238
|
+
subtitle: aso.subtitle,
|
|
239
|
+
description: generateFullDescription(localeData, metadata),
|
|
240
|
+
keywords: Array.isArray(aso.keywords) ? aso.keywords.join(", ") : aso.keywords,
|
|
241
|
+
promotionalText: void 0,
|
|
242
|
+
bundleId: config.bundleId,
|
|
243
|
+
locale,
|
|
244
|
+
supportUrl: metadata.supportUrl,
|
|
245
|
+
marketingUrl: metadata.marketingUrl,
|
|
246
|
+
privacyPolicyUrl: metadata.privacyUrl,
|
|
247
|
+
screenshots: {
|
|
248
|
+
// 폰 스크린샷을 iphone65로 매핑
|
|
249
|
+
iphone65: localeScreenshots.phone || [],
|
|
250
|
+
// 태블릿 스크린샷을 ipadPro129로 매핑
|
|
251
|
+
ipadPro129: localeScreenshots.tablet
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
const appStoreLocaleKeys = Object.keys(appStoreLocales);
|
|
256
|
+
if (appStoreLocaleKeys.length > 0) {
|
|
257
|
+
const hasConfigDefault = isAppStoreLocale(defaultLocale) && Boolean(appStoreLocales[defaultLocale]);
|
|
258
|
+
const resolvedDefault = hasConfigDefault ? defaultLocale : appStoreLocales[DEFAULT_LOCALE] ? DEFAULT_LOCALE : appStoreLocaleKeys[0];
|
|
259
|
+
asoData.appStore = {
|
|
260
|
+
locales: appStoreLocales,
|
|
261
|
+
defaultLocale: resolvedDefault
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const hasGooglePlay = !!asoData.googlePlay;
|
|
266
|
+
const hasAppStore = !!asoData.appStore;
|
|
267
|
+
console.debug(`[loadAsoFromConfig] Result for ${slug}:`);
|
|
268
|
+
console.debug(
|
|
269
|
+
` - Google Play data: ${hasGooglePlay ? "found" : "not found"}`
|
|
270
|
+
);
|
|
271
|
+
console.debug(` - App Store data: ${hasAppStore ? "found" : "not found"}`);
|
|
272
|
+
if (!hasGooglePlay && !hasAppStore) {
|
|
273
|
+
console.warn(`[loadAsoFromConfig] No ASO data generated for ${slug}`);
|
|
274
|
+
}
|
|
275
|
+
return asoData;
|
|
276
|
+
} catch (error) {
|
|
277
|
+
console.error(
|
|
278
|
+
`[loadAsoFromConfig] Failed to load ASO data from config for ${slug}:`,
|
|
279
|
+
error
|
|
280
|
+
);
|
|
281
|
+
return {};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function saveAsoToConfig(slug, config) {
|
|
285
|
+
const productsDir = getProductsDir();
|
|
286
|
+
const configPath = path2.join(productsDir, slug, "config.json");
|
|
287
|
+
fs2.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
288
|
+
}
|
|
289
|
+
function saveAsoToAsoDir(slug, asoData) {
|
|
290
|
+
const rootDir = getPushDataDir();
|
|
291
|
+
if (asoData.googlePlay) {
|
|
292
|
+
const asoPath = path2.join(
|
|
293
|
+
rootDir,
|
|
294
|
+
"products",
|
|
295
|
+
slug,
|
|
296
|
+
"store",
|
|
297
|
+
"google-play",
|
|
298
|
+
"aso-data.json"
|
|
299
|
+
);
|
|
300
|
+
const dir = path2.dirname(asoPath);
|
|
301
|
+
if (!fs2.existsSync(dir)) {
|
|
302
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
303
|
+
}
|
|
304
|
+
const googlePlayData = asoData.googlePlay;
|
|
305
|
+
const multilingualData = "locales" in googlePlayData ? googlePlayData : {
|
|
306
|
+
locales: {
|
|
307
|
+
[googlePlayData.defaultLanguage || DEFAULT_LOCALE]: googlePlayData
|
|
308
|
+
},
|
|
309
|
+
defaultLocale: googlePlayData.defaultLanguage || DEFAULT_LOCALE
|
|
310
|
+
};
|
|
311
|
+
fs2.writeFileSync(
|
|
312
|
+
asoPath,
|
|
313
|
+
JSON.stringify({ googlePlay: multilingualData }, null, 2) + "\n",
|
|
314
|
+
"utf-8"
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
if (asoData.appStore) {
|
|
318
|
+
const asoPath = path2.join(
|
|
319
|
+
rootDir,
|
|
320
|
+
"products",
|
|
321
|
+
slug,
|
|
322
|
+
"store",
|
|
323
|
+
"app-store",
|
|
324
|
+
"aso-data.json"
|
|
325
|
+
);
|
|
326
|
+
const dir = path2.dirname(asoPath);
|
|
327
|
+
if (!fs2.existsSync(dir)) {
|
|
328
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
329
|
+
}
|
|
330
|
+
const appStoreData = asoData.appStore;
|
|
331
|
+
const multilingualData = "locales" in appStoreData ? appStoreData : {
|
|
332
|
+
locales: {
|
|
333
|
+
[appStoreData.locale || DEFAULT_LOCALE]: appStoreData
|
|
334
|
+
},
|
|
335
|
+
defaultLocale: appStoreData.locale || DEFAULT_LOCALE
|
|
336
|
+
};
|
|
337
|
+
fs2.writeFileSync(
|
|
338
|
+
asoPath,
|
|
339
|
+
JSON.stringify({ appStore: multilingualData }, null, 2) + "\n",
|
|
340
|
+
"utf-8"
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export {
|
|
346
|
+
getAsoDataDir,
|
|
347
|
+
getPullDataDir,
|
|
348
|
+
getPushDataDir,
|
|
349
|
+
getPublicDir,
|
|
350
|
+
getKeywordResearchDir,
|
|
351
|
+
getProductsDir,
|
|
352
|
+
loadAsoFromConfig,
|
|
353
|
+
saveAsoToConfig,
|
|
354
|
+
saveAsoToAsoDir
|
|
355
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -40,9 +40,13 @@ declare function getPushDataDir(): string;
|
|
|
40
40
|
* Get the public directory path (dataDir/public)
|
|
41
41
|
*/
|
|
42
42
|
declare function getPublicDir(): string;
|
|
43
|
+
/**
|
|
44
|
+
* Get the keywordResearch directory path (dataDir/.aso/keywordResearch)
|
|
45
|
+
*/
|
|
46
|
+
declare function getKeywordResearchDir(): string;
|
|
43
47
|
/**
|
|
44
48
|
* Get the products directory path (dataDir/public/products)
|
|
45
49
|
*/
|
|
46
50
|
declare function getProductsDir(): string;
|
|
47
51
|
|
|
48
|
-
export { AsoData, ProductConfig, getAsoDataDir, getProductsDir, getPublicDir, getPullDataDir, getPushDataDir, loadAsoFromConfig, saveAsoToAsoDir, saveAsoToConfig };
|
|
52
|
+
export { AsoData, ProductConfig, getAsoDataDir, getKeywordResearchDir, getProductsDir, getPublicDir, getPullDataDir, getPushDataDir, loadAsoFromConfig, saveAsoToAsoDir, saveAsoToConfig };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getAsoDataDir,
|
|
3
|
+
getKeywordResearchDir,
|
|
3
4
|
getProductsDir,
|
|
4
5
|
getPublicDir,
|
|
5
6
|
getPullDataDir,
|
|
@@ -7,7 +8,7 @@ import {
|
|
|
7
8
|
loadAsoFromConfig,
|
|
8
9
|
saveAsoToAsoDir,
|
|
9
10
|
saveAsoToConfig
|
|
10
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-W62HB2ZL.js";
|
|
11
12
|
import {
|
|
12
13
|
APP_STORE_TO_UNIFIED,
|
|
13
14
|
DEFAULT_LOCALE,
|
|
@@ -51,6 +52,7 @@ export {
|
|
|
51
52
|
convertObjectToAppStore,
|
|
52
53
|
convertObjectToGooglePlay,
|
|
53
54
|
getAsoDataDir,
|
|
55
|
+
getKeywordResearchDir,
|
|
54
56
|
getProductsDir,
|
|
55
57
|
getPublicDir,
|
|
56
58
|
getPullDataDir,
|