pabal-web-mcp 0.1.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 +133 -0
- package/dist/bin/mcp-server.d.ts +1 -0
- package/dist/bin/mcp-server.js +1906 -0
- package/dist/chunk-YJWGBO7W.js +952 -0
- package/dist/index.d.ts +592 -0
- package/dist/index.js +62 -0
- package/package.json +52 -0
|
@@ -0,0 +1,1906 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_LOCALE,
|
|
4
|
+
appStoreToUnified,
|
|
5
|
+
googlePlayToUnified,
|
|
6
|
+
isAppStoreMultilingual,
|
|
7
|
+
isGooglePlayMultilingual,
|
|
8
|
+
loadAsoFromConfig,
|
|
9
|
+
saveAsoToAsoDir,
|
|
10
|
+
unifiedToAppStore,
|
|
11
|
+
unifiedToGooglePlay
|
|
12
|
+
} from "../chunk-YJWGBO7W.js";
|
|
13
|
+
|
|
14
|
+
// src/bin/mcp-server.ts
|
|
15
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
16
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
17
|
+
import {
|
|
18
|
+
CallToolRequestSchema,
|
|
19
|
+
ListToolsRequestSchema
|
|
20
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
21
|
+
|
|
22
|
+
// src/tools/aso-to-public.ts
|
|
23
|
+
import { z } from "zod";
|
|
24
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
25
|
+
|
|
26
|
+
// src/tools/utils/aso-to-public/load-pull-data.util.ts
|
|
27
|
+
import fs from "fs";
|
|
28
|
+
import path from "path";
|
|
29
|
+
function loadPullData(slug) {
|
|
30
|
+
const asoData = {};
|
|
31
|
+
const googlePlayPath = path.join(
|
|
32
|
+
process.cwd(),
|
|
33
|
+
".aso",
|
|
34
|
+
"pullData",
|
|
35
|
+
"products",
|
|
36
|
+
slug,
|
|
37
|
+
"store",
|
|
38
|
+
"google-play",
|
|
39
|
+
"aso-data.json"
|
|
40
|
+
);
|
|
41
|
+
if (fs.existsSync(googlePlayPath)) {
|
|
42
|
+
try {
|
|
43
|
+
const content = fs.readFileSync(googlePlayPath, "utf-8");
|
|
44
|
+
const data = JSON.parse(content);
|
|
45
|
+
if (data.googlePlay) {
|
|
46
|
+
asoData.googlePlay = data.googlePlay;
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
throw new Error(`Failed to read Google Play data: ${error}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const appStorePath = path.join(
|
|
53
|
+
process.cwd(),
|
|
54
|
+
".aso",
|
|
55
|
+
"pullData",
|
|
56
|
+
"products",
|
|
57
|
+
slug,
|
|
58
|
+
"store",
|
|
59
|
+
"app-store",
|
|
60
|
+
"aso-data.json"
|
|
61
|
+
);
|
|
62
|
+
if (fs.existsSync(appStorePath)) {
|
|
63
|
+
try {
|
|
64
|
+
const content = fs.readFileSync(appStorePath, "utf-8");
|
|
65
|
+
const data = JSON.parse(content);
|
|
66
|
+
if (data.appStore) {
|
|
67
|
+
asoData.appStore = data.appStore;
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
throw new Error(`Failed to read App Store data: ${error}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return asoData;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/tools/utils/aso-to-public/generate-conversion-prompt.util.ts
|
|
77
|
+
function generateConversionPrompt(title, shortDescription, fullDescription, locale, keywords, promotionalText, googlePlayData, appStoreData, screenshotPaths) {
|
|
78
|
+
const inputData = {
|
|
79
|
+
locale
|
|
80
|
+
};
|
|
81
|
+
if (googlePlayData && appStoreData) {
|
|
82
|
+
inputData.mergedFrom = ["Google Play", "App Store"];
|
|
83
|
+
inputData.googlePlay = {
|
|
84
|
+
title: googlePlayData.title,
|
|
85
|
+
shortDescription: googlePlayData.shortDescription || null,
|
|
86
|
+
fullDescription: googlePlayData.fullDescription
|
|
87
|
+
};
|
|
88
|
+
inputData.appStore = {
|
|
89
|
+
name: appStoreData.name,
|
|
90
|
+
subtitle: appStoreData.subtitle || null,
|
|
91
|
+
description: appStoreData.description,
|
|
92
|
+
keywords: appStoreData.keywords || null,
|
|
93
|
+
promotionalText: appStoreData.promotionalText || null
|
|
94
|
+
};
|
|
95
|
+
} else if (googlePlayData) {
|
|
96
|
+
inputData.source = "Google Play";
|
|
97
|
+
inputData.title = googlePlayData.title;
|
|
98
|
+
inputData.shortDescription = googlePlayData.shortDescription || null;
|
|
99
|
+
inputData.fullDescription = googlePlayData.fullDescription;
|
|
100
|
+
} else if (appStoreData) {
|
|
101
|
+
inputData.source = "App Store";
|
|
102
|
+
inputData.title = appStoreData.name;
|
|
103
|
+
inputData.subtitle = appStoreData.subtitle || null;
|
|
104
|
+
inputData.fullDescription = appStoreData.description;
|
|
105
|
+
if (appStoreData.keywords) inputData.keywords = appStoreData.keywords;
|
|
106
|
+
if (appStoreData.promotionalText) inputData.promotionalText = appStoreData.promotionalText;
|
|
107
|
+
} else {
|
|
108
|
+
inputData.title = title;
|
|
109
|
+
inputData.shortDescription = shortDescription || null;
|
|
110
|
+
inputData.fullDescription = fullDescription;
|
|
111
|
+
if (keywords) inputData.keywords = keywords;
|
|
112
|
+
if (promotionalText) inputData.promotionalText = promotionalText;
|
|
113
|
+
}
|
|
114
|
+
const inputJson = JSON.stringify(inputData, null, 2);
|
|
115
|
+
const outputExample = {
|
|
116
|
+
aso: {
|
|
117
|
+
title: "App title",
|
|
118
|
+
subtitle: "Subtitle (App Store only, optional)",
|
|
119
|
+
shortDescription: "Short description (Google Play only, optional)",
|
|
120
|
+
keywords: ["keyword1", "keyword2"],
|
|
121
|
+
template: {
|
|
122
|
+
intro: "Brief app introduction paragraph (2-3 sentences)",
|
|
123
|
+
outro: "Closing paragraph and CTA (1-2 sentences)"
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
landing: {
|
|
127
|
+
screenshots: {
|
|
128
|
+
images: [
|
|
129
|
+
{ title: "Caption title", description: "Caption body" }
|
|
130
|
+
]
|
|
131
|
+
},
|
|
132
|
+
features: {
|
|
133
|
+
items: [{ title: "Feature title", body: "Feature description" }]
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const outputFormatJson = JSON.stringify(outputExample, null, 2);
|
|
138
|
+
const hasBothPlatforms = googlePlayData && appStoreData;
|
|
139
|
+
return `Please convert the following store ASO data into config.json format.
|
|
140
|
+
|
|
141
|
+
**Input Data:**
|
|
142
|
+
\`\`\`json
|
|
143
|
+
${inputJson}
|
|
144
|
+
\`\`\`
|
|
145
|
+
|
|
146
|
+
**Output Format:**
|
|
147
|
+
Convert according to the following JSON schema:
|
|
148
|
+
|
|
149
|
+
\`\`\`json
|
|
150
|
+
${outputFormatJson}
|
|
151
|
+
\`\`\`
|
|
152
|
+
|
|
153
|
+
**Conversion Rules:**
|
|
154
|
+
${hasBothPlatforms ? `
|
|
155
|
+
**IMPORTANT - Data Merge Strategy:**
|
|
156
|
+
This locale has data from BOTH Google Play and App Store. Use the following merge strategy:
|
|
157
|
+
- **Title/Name**: Prefer App Store name if available, otherwise use Google Play title
|
|
158
|
+
- **Subtitle/Short Description**: Use App Store subtitle if available, otherwise use Google Play shortDescription
|
|
159
|
+
- **Full Description**: Merge both descriptions intelligently:
|
|
160
|
+
- If descriptions are similar, use the more detailed version
|
|
161
|
+
- If descriptions are different, combine unique information from both
|
|
162
|
+
- Prioritize App Store description for intro/outro (usually more polished)
|
|
163
|
+
- Incorporate unique features from Google Play description
|
|
164
|
+
- **Keywords**: Use App Store keywords (Google Play doesn't have explicit keywords field)
|
|
165
|
+
- **Promotional Text**: Use App Store promotionalText if available
|
|
166
|
+
|
|
167
|
+
**Content Extraction:**
|
|
168
|
+
` : ""}1. Use the first paragraph(s) of fullDescription as template.intro (2-3 sentences).
|
|
169
|
+
2. Use the final paragraph as template.outro (1-2 sentences, keep CTA tone).
|
|
170
|
+
3. Extract the strongest bullets/sections from the body into landing.features.items (3-5 items). Each item should have title + 1-2 sentence body.
|
|
171
|
+
4. Extract any explicit bullet lists or section highlights into landing.screenshots.images captions (keep order; 3-5 entries). Each needs title + short description.
|
|
172
|
+
5. Convert comma-separated keywords string to array.
|
|
173
|
+
6. Include Contact & Support info only if present in the original text; otherwise omit.
|
|
174
|
+
|
|
175
|
+
**Translation Guidelines (if converting to non-English locales):**
|
|
176
|
+
- Keep proper nouns and technical terms in English when translating
|
|
177
|
+
- Keep terms like "Always-On Display", "E-Ink", "Android", brand names (e.g., "Boox", "Meebook"), and other technical/proper nouns in English
|
|
178
|
+
- Only translate descriptive text, not technical terminology or proper nouns
|
|
179
|
+
- Example: "Always-On Display" should remain "Always-On Display" in Korean, not translated to "\uD56D\uC0C1 \uCF1C\uC9C4 \uD654\uBA74"
|
|
180
|
+
|
|
181
|
+
${screenshotPaths ? `
|
|
182
|
+
**Screenshot Paths:**
|
|
183
|
+
Screenshots for this locale can be found at:
|
|
184
|
+
${screenshotPaths.googlePlay ? `- Google Play: ${screenshotPaths.googlePlay}` : ""}
|
|
185
|
+
${screenshotPaths.appStore ? `- App Store: ${screenshotPaths.appStore}` : ""}
|
|
186
|
+
|
|
187
|
+
Copy screenshots from these directories to public/products/[slug]/screenshots/ as needed.
|
|
188
|
+
` : ""}
|
|
189
|
+
**Important:** Return only valid JSON, without any additional explanation.`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/tools/aso-to-public.ts
|
|
193
|
+
var toJsonSchema = zodToJsonSchema;
|
|
194
|
+
var asoToPublicInputSchema = z.object({
|
|
195
|
+
slug: z.string().describe("Product slug")
|
|
196
|
+
});
|
|
197
|
+
var jsonSchema = toJsonSchema(asoToPublicInputSchema, {
|
|
198
|
+
name: "AsoToPublicInput",
|
|
199
|
+
target: "openApi3",
|
|
200
|
+
$refStrategy: "none"
|
|
201
|
+
});
|
|
202
|
+
var inputSchema = jsonSchema.definitions?.AsoToPublicInput || jsonSchema;
|
|
203
|
+
var asoToPublicTool = {
|
|
204
|
+
name: "aso-to-public",
|
|
205
|
+
description: `Converts ASO data from .aso/pullData to public/products/[slug]/ structure.
|
|
206
|
+
|
|
207
|
+
**IMPORTANT:** The 'slug' parameter is REQUIRED. If the user does not provide a slug, you MUST ask them to provide it. This tool processes only ONE product at a time.
|
|
208
|
+
|
|
209
|
+
This tool:
|
|
210
|
+
1. Loads ASO data from .aso/pullData/products/[slug]/store/
|
|
211
|
+
2. Generates per-locale conversion prompts to map fullDescription into structured locale JSON (template intro/outro + landing features/screenshots captions)
|
|
212
|
+
3. Next steps (manual): paste converted JSON into public/products/[slug]/locales/[locale].json and copy screenshots from .aso/pullData if needed
|
|
213
|
+
|
|
214
|
+
The conversion from unstructured to structured format is performed by Claude based on the conversion prompt.`,
|
|
215
|
+
inputSchema
|
|
216
|
+
};
|
|
217
|
+
async function handleAsoToPublic(input) {
|
|
218
|
+
const { slug } = input;
|
|
219
|
+
const asoData = loadPullData(slug);
|
|
220
|
+
if (!asoData.googlePlay && !asoData.appStore) {
|
|
221
|
+
throw new Error(`No ASO data found in .aso/pullData for ${slug}`);
|
|
222
|
+
}
|
|
223
|
+
const mergedDataByLocale = /* @__PURE__ */ new Map();
|
|
224
|
+
if (asoData.googlePlay) {
|
|
225
|
+
const googlePlayData = asoData.googlePlay;
|
|
226
|
+
const locales = isGooglePlayMultilingual(googlePlayData) ? googlePlayData.locales : {
|
|
227
|
+
[googlePlayData.defaultLanguage || DEFAULT_LOCALE]: googlePlayData
|
|
228
|
+
};
|
|
229
|
+
for (const [platformLocale, localeData] of Object.entries(locales)) {
|
|
230
|
+
const unifiedLocale = googlePlayToUnified(platformLocale);
|
|
231
|
+
if (!mergedDataByLocale.has(unifiedLocale)) {
|
|
232
|
+
mergedDataByLocale.set(unifiedLocale, { unifiedLocale });
|
|
233
|
+
}
|
|
234
|
+
const merged = mergedDataByLocale.get(unifiedLocale);
|
|
235
|
+
merged.googlePlay = {
|
|
236
|
+
title: localeData.title,
|
|
237
|
+
shortDescription: localeData.shortDescription,
|
|
238
|
+
fullDescription: localeData.fullDescription
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (asoData.appStore) {
|
|
243
|
+
const appStoreData = asoData.appStore;
|
|
244
|
+
const locales = isAppStoreMultilingual(appStoreData) ? appStoreData.locales : { [appStoreData.locale || DEFAULT_LOCALE]: appStoreData };
|
|
245
|
+
for (const [platformLocale, localeData] of Object.entries(locales)) {
|
|
246
|
+
const unifiedLocale = appStoreToUnified(platformLocale);
|
|
247
|
+
if (!mergedDataByLocale.has(unifiedLocale)) {
|
|
248
|
+
mergedDataByLocale.set(unifiedLocale, { unifiedLocale });
|
|
249
|
+
}
|
|
250
|
+
const merged = mergedDataByLocale.get(unifiedLocale);
|
|
251
|
+
merged.appStore = {
|
|
252
|
+
name: localeData.name,
|
|
253
|
+
subtitle: localeData.subtitle,
|
|
254
|
+
description: localeData.description,
|
|
255
|
+
keywords: localeData.keywords,
|
|
256
|
+
promotionalText: localeData.promotionalText
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const conversionPrompts = [];
|
|
261
|
+
const conversionTasks = [];
|
|
262
|
+
for (const [unifiedLocale, mergedData] of mergedDataByLocale.entries()) {
|
|
263
|
+
const sources = [];
|
|
264
|
+
if (mergedData.googlePlay) sources.push("Google Play");
|
|
265
|
+
if (mergedData.appStore) sources.push("App Store");
|
|
266
|
+
let screenshotPaths;
|
|
267
|
+
if (mergedData.googlePlay) {
|
|
268
|
+
const googlePlayLocale = Object.keys(
|
|
269
|
+
isGooglePlayMultilingual(asoData.googlePlay) ? asoData.googlePlay.locales : { [asoData.googlePlay.defaultLanguage || DEFAULT_LOCALE]: {} }
|
|
270
|
+
).find((loc) => googlePlayToUnified(loc) === unifiedLocale);
|
|
271
|
+
if (googlePlayLocale) {
|
|
272
|
+
if (!screenshotPaths) screenshotPaths = {};
|
|
273
|
+
screenshotPaths.googlePlay = `.aso/pullData/products/${slug}/store/google-play/screenshots/${googlePlayLocale}/`;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (mergedData.appStore) {
|
|
277
|
+
const appStoreLocale = Object.keys(
|
|
278
|
+
isAppStoreMultilingual(asoData.appStore) ? asoData.appStore.locales : { [asoData.appStore.locale || DEFAULT_LOCALE]: {} }
|
|
279
|
+
).find((loc) => appStoreToUnified(loc) === unifiedLocale);
|
|
280
|
+
if (appStoreLocale) {
|
|
281
|
+
if (!screenshotPaths) screenshotPaths = {};
|
|
282
|
+
screenshotPaths.appStore = `.aso/pullData/products/${slug}/store/app-store/screenshots/${appStoreLocale}/`;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const prompt = generateConversionPrompt(
|
|
286
|
+
mergedData.googlePlay?.title || mergedData.appStore?.name || "",
|
|
287
|
+
mergedData.googlePlay?.shortDescription || mergedData.appStore?.subtitle,
|
|
288
|
+
mergedData.googlePlay?.fullDescription || mergedData.appStore?.description || "",
|
|
289
|
+
unifiedLocale,
|
|
290
|
+
mergedData.appStore?.keywords,
|
|
291
|
+
mergedData.appStore?.promotionalText,
|
|
292
|
+
mergedData.googlePlay,
|
|
293
|
+
mergedData.appStore,
|
|
294
|
+
screenshotPaths
|
|
295
|
+
);
|
|
296
|
+
conversionTasks.push({
|
|
297
|
+
locale: unifiedLocale,
|
|
298
|
+
sources,
|
|
299
|
+
prompt
|
|
300
|
+
});
|
|
301
|
+
const sourcesText = sources.join(" + ");
|
|
302
|
+
conversionPrompts.push(`
|
|
303
|
+
--- ${unifiedLocale} (${sourcesText}) ---
|
|
304
|
+
${prompt}`);
|
|
305
|
+
}
|
|
306
|
+
let responseText = `Converting ASO data from .aso/pullData to public/products/${slug}/ structure.
|
|
307
|
+
|
|
308
|
+
`;
|
|
309
|
+
responseText += `Found ${conversionTasks.length} unified locale(s) to convert.
|
|
310
|
+
`;
|
|
311
|
+
responseText += `Data sources: Google Play (${asoData.googlePlay ? "\u2713" : "\u2717"}), App Store (${asoData.appStore ? "\u2713" : "\u2717"})
|
|
312
|
+
|
|
313
|
+
`;
|
|
314
|
+
responseText += "Please convert each locale's ASO data using the prompts below.\n";
|
|
315
|
+
responseText += "When both platforms have data for the same locale, they are merged into a single conversion.\n\n";
|
|
316
|
+
responseText += conversionPrompts.join("\n\n");
|
|
317
|
+
responseText += `
|
|
318
|
+
|
|
319
|
+
Next steps (manual):
|
|
320
|
+
`;
|
|
321
|
+
responseText += `1. Save converted JSON to public/products/${slug}/locales/[locale].json
|
|
322
|
+
`;
|
|
323
|
+
responseText += ` Example: public/products/${slug}/locales/ar.json (not ar-SA.json)
|
|
324
|
+
`;
|
|
325
|
+
responseText += `2. Copy screenshots from .aso/pullData/products/${slug}/store/ to public/products/${slug}/screenshots/
|
|
326
|
+
`;
|
|
327
|
+
return {
|
|
328
|
+
content: [
|
|
329
|
+
{
|
|
330
|
+
type: "text",
|
|
331
|
+
text: responseText
|
|
332
|
+
}
|
|
333
|
+
]
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// src/tools/public-to-aso.ts
|
|
338
|
+
import { z as z2 } from "zod";
|
|
339
|
+
import { zodToJsonSchema as zodToJsonSchema2 } from "zod-to-json-schema";
|
|
340
|
+
import path4 from "path";
|
|
341
|
+
|
|
342
|
+
// src/tools/utils/public-to-aso/prepare-aso-data-for-push.util.ts
|
|
343
|
+
function prepareAsoDataForPush(slug, configData) {
|
|
344
|
+
const storeData = {};
|
|
345
|
+
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL ?? "https://labs.quartz.best";
|
|
346
|
+
const detailPageUrl = `${baseUrl}/${slug}`;
|
|
347
|
+
if (configData.googlePlay) {
|
|
348
|
+
const googlePlayData = configData.googlePlay;
|
|
349
|
+
const locales = isGooglePlayMultilingual(googlePlayData) ? googlePlayData.locales : {
|
|
350
|
+
[googlePlayData.defaultLanguage || DEFAULT_LOCALE]: googlePlayData
|
|
351
|
+
};
|
|
352
|
+
const cleanedLocales = {};
|
|
353
|
+
for (const [locale, localeData] of Object.entries(locales)) {
|
|
354
|
+
const { screenshots, ...rest } = localeData;
|
|
355
|
+
cleanedLocales[locale] = {
|
|
356
|
+
...rest,
|
|
357
|
+
featureGraphic: void 0,
|
|
358
|
+
// Images excluded
|
|
359
|
+
contactWebsite: detailPageUrl
|
|
360
|
+
// Set detail page URL
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
const convertedLocales = {};
|
|
364
|
+
for (const [unifiedLocale, localeData] of Object.entries(cleanedLocales)) {
|
|
365
|
+
const googlePlayLocale = unifiedToGooglePlay(
|
|
366
|
+
unifiedLocale
|
|
367
|
+
);
|
|
368
|
+
if (googlePlayLocale !== null) {
|
|
369
|
+
convertedLocales[googlePlayLocale] = {
|
|
370
|
+
...localeData,
|
|
371
|
+
defaultLanguage: googlePlayLocale
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const googleDefaultLocale = isGooglePlayMultilingual(googlePlayData) ? googlePlayData.defaultLocale || DEFAULT_LOCALE : googlePlayData.defaultLanguage || DEFAULT_LOCALE;
|
|
376
|
+
const convertedDefaultLocale = unifiedToGooglePlay(googleDefaultLocale) || googleDefaultLocale;
|
|
377
|
+
storeData.googlePlay = {
|
|
378
|
+
locales: convertedLocales,
|
|
379
|
+
defaultLocale: convertedDefaultLocale
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
if (configData.appStore) {
|
|
383
|
+
const appStoreData = configData.appStore;
|
|
384
|
+
const locales = isAppStoreMultilingual(appStoreData) ? appStoreData.locales : { [appStoreData.locale || DEFAULT_LOCALE]: appStoreData };
|
|
385
|
+
const cleanedLocales = {};
|
|
386
|
+
for (const [locale, localeData] of Object.entries(locales)) {
|
|
387
|
+
const { screenshots, ...rest } = localeData;
|
|
388
|
+
cleanedLocales[locale] = {
|
|
389
|
+
...rest,
|
|
390
|
+
marketingUrl: detailPageUrl
|
|
391
|
+
// Set detail page URL
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
const convertedLocales = {};
|
|
395
|
+
for (const [unifiedLocale, localeData] of Object.entries(cleanedLocales)) {
|
|
396
|
+
const appStoreLocale = unifiedToAppStore(unifiedLocale);
|
|
397
|
+
if (appStoreLocale !== null) {
|
|
398
|
+
convertedLocales[appStoreLocale] = {
|
|
399
|
+
...localeData,
|
|
400
|
+
locale: appStoreLocale
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const appStoreDefaultLocale = isAppStoreMultilingual(appStoreData) ? appStoreData.defaultLocale || DEFAULT_LOCALE : appStoreData.locale || DEFAULT_LOCALE;
|
|
405
|
+
const convertedDefaultLocale = unifiedToAppStore(appStoreDefaultLocale) || appStoreDefaultLocale;
|
|
406
|
+
storeData.appStore = {
|
|
407
|
+
locales: convertedLocales,
|
|
408
|
+
defaultLocale: convertedDefaultLocale
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
return storeData;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// src/tools/utils/public-to-aso/save-raw-aso-data.util.ts
|
|
415
|
+
function saveRawAsoData(slug, asoData, options) {
|
|
416
|
+
const rootDir = options?.rootDir ?? ".aso/pushData";
|
|
417
|
+
saveAsoToAsoDir(slug, asoData, { rootDir });
|
|
418
|
+
const localeCounts = {};
|
|
419
|
+
if (asoData.googlePlay) {
|
|
420
|
+
const googlePlayData = asoData.googlePlay;
|
|
421
|
+
const locales = isGooglePlayMultilingual(googlePlayData) ? googlePlayData.locales : { [googlePlayData.defaultLanguage || DEFAULT_LOCALE]: googlePlayData };
|
|
422
|
+
localeCounts.googlePlay = Object.keys(locales).length;
|
|
423
|
+
}
|
|
424
|
+
if (asoData.appStore) {
|
|
425
|
+
const appStoreData = asoData.appStore;
|
|
426
|
+
const locales = isAppStoreMultilingual(appStoreData) ? appStoreData.locales : { [appStoreData.locale || DEFAULT_LOCALE]: appStoreData };
|
|
427
|
+
localeCounts.appStore = Object.keys(locales).length;
|
|
428
|
+
}
|
|
429
|
+
if (localeCounts.googlePlay) {
|
|
430
|
+
console.log(
|
|
431
|
+
`\u{1F4BE} Google Play raw data saved to ${rootDir} (${localeCounts.googlePlay} locale${localeCounts.googlePlay > 1 ? "s" : ""})`
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
if (localeCounts.appStore) {
|
|
435
|
+
console.log(
|
|
436
|
+
`\u{1F4BE} App Store raw data saved to ${rootDir} (${localeCounts.appStore} locale${localeCounts.appStore > 1 ? "s" : ""})`
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// src/tools/utils/public-to-aso/download-image.util.ts
|
|
442
|
+
import fs2 from "fs";
|
|
443
|
+
import path2 from "path";
|
|
444
|
+
async function downloadImage(url, outputPath) {
|
|
445
|
+
try {
|
|
446
|
+
const response = await fetch(url);
|
|
447
|
+
if (!response.ok) {
|
|
448
|
+
throw new Error(
|
|
449
|
+
`Image download failed: ${response.status} ${response.statusText}`
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
453
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
454
|
+
const dir = path2.dirname(outputPath);
|
|
455
|
+
if (!fs2.existsSync(dir)) {
|
|
456
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
457
|
+
}
|
|
458
|
+
fs2.writeFileSync(outputPath, buffer);
|
|
459
|
+
} catch (error) {
|
|
460
|
+
console.error(`\u274C Image download failed (${url}):`, error);
|
|
461
|
+
throw error;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// src/tools/utils/public-to-aso/is-local-asset-path.util.ts
|
|
466
|
+
function isLocalAssetPath(assetPath) {
|
|
467
|
+
if (!assetPath) {
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
const trimmedPath = assetPath.trim();
|
|
471
|
+
return !/^([a-z]+:)?\/\//i.test(trimmedPath);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// src/tools/utils/public-to-aso/copy-local-asset-to-aso-dir.util.ts
|
|
475
|
+
import fs3 from "fs";
|
|
476
|
+
import path3 from "path";
|
|
477
|
+
function copyLocalAssetToAsoDir(assetPath, outputPath) {
|
|
478
|
+
const trimmedPath = assetPath.replace(/^\.\//, "").replace(/^public\//, "").replace(/^\/+/, "");
|
|
479
|
+
const sourcePath = path3.join(process.cwd(), "public", trimmedPath);
|
|
480
|
+
if (!fs3.existsSync(sourcePath)) {
|
|
481
|
+
console.warn(`\u26A0\uFE0F Local image not found: ${sourcePath}`);
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
const dir = path3.dirname(outputPath);
|
|
485
|
+
if (!fs3.existsSync(dir)) {
|
|
486
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
487
|
+
}
|
|
488
|
+
fs3.copyFileSync(sourcePath, outputPath);
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// src/tools/utils/shared/convert-to-multilingual.util.ts
|
|
493
|
+
function convertToMultilingual(data, locale) {
|
|
494
|
+
const detectedLocale = locale || data.locale || data.defaultLanguage || DEFAULT_LOCALE;
|
|
495
|
+
return {
|
|
496
|
+
locales: {
|
|
497
|
+
[detectedLocale]: data
|
|
498
|
+
},
|
|
499
|
+
defaultLocale: detectedLocale
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// src/tools/public-to-aso.ts
|
|
504
|
+
var FIELD_LIMITS_DOC_PATH = "docs/aso/ASO_FIELD_LIMITS.md";
|
|
505
|
+
var toJsonSchema2 = zodToJsonSchema2;
|
|
506
|
+
var publicToAsoInputSchema = z2.object({
|
|
507
|
+
slug: z2.string().describe("Product slug"),
|
|
508
|
+
dryRun: z2.boolean().optional().default(false).describe("Preview mode (no changes)")
|
|
509
|
+
});
|
|
510
|
+
var jsonSchema2 = toJsonSchema2(publicToAsoInputSchema, {
|
|
511
|
+
name: "PublicToAsoInput",
|
|
512
|
+
target: "openApi3",
|
|
513
|
+
$refStrategy: "none"
|
|
514
|
+
});
|
|
515
|
+
var inputSchema2 = jsonSchema2.definitions?.PublicToAsoInput || jsonSchema2;
|
|
516
|
+
async function downloadScreenshotsToAsoDir(slug, asoData, options) {
|
|
517
|
+
const rootDir = options?.rootDir ?? ".aso/pushData";
|
|
518
|
+
const productStoreRoot = path4.join(
|
|
519
|
+
process.cwd(),
|
|
520
|
+
rootDir,
|
|
521
|
+
"products",
|
|
522
|
+
slug,
|
|
523
|
+
"store"
|
|
524
|
+
);
|
|
525
|
+
if (asoData.googlePlay) {
|
|
526
|
+
let googlePlayData = asoData.googlePlay;
|
|
527
|
+
if (!isGooglePlayMultilingual(googlePlayData)) {
|
|
528
|
+
googlePlayData = convertToMultilingual(
|
|
529
|
+
googlePlayData,
|
|
530
|
+
googlePlayData.defaultLanguage
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
for (const unifiedLocale of Object.keys(googlePlayData.locales)) {
|
|
534
|
+
const googlePlayLocale = unifiedToGooglePlay(
|
|
535
|
+
unifiedLocale
|
|
536
|
+
);
|
|
537
|
+
if (!googlePlayLocale) {
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
const localeData = googlePlayData.locales[unifiedLocale];
|
|
541
|
+
const asoDir = path4.join(
|
|
542
|
+
productStoreRoot,
|
|
543
|
+
"google-play",
|
|
544
|
+
"screenshots",
|
|
545
|
+
googlePlayLocale
|
|
546
|
+
);
|
|
547
|
+
if (localeData.screenshots?.phone?.length > 0) {
|
|
548
|
+
for (let i = 0; i < localeData.screenshots.phone.length; i++) {
|
|
549
|
+
const url = localeData.screenshots.phone[i];
|
|
550
|
+
const filename = `phone-${i + 1}.png`;
|
|
551
|
+
const outputPath = path4.join(asoDir, filename);
|
|
552
|
+
if (isLocalAssetPath(url)) {
|
|
553
|
+
copyLocalAssetToAsoDir(url, outputPath);
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
await downloadImage(url, outputPath);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
const tablet7Screenshots = localeData.screenshots?.tablet7;
|
|
560
|
+
if (tablet7Screenshots && tablet7Screenshots.length > 0) {
|
|
561
|
+
for (let i = 0; i < tablet7Screenshots.length; i++) {
|
|
562
|
+
const url = tablet7Screenshots[i];
|
|
563
|
+
const filename = `tablet7-${i + 1}.png`;
|
|
564
|
+
const outputPath = path4.join(asoDir, filename);
|
|
565
|
+
if (isLocalAssetPath(url)) {
|
|
566
|
+
copyLocalAssetToAsoDir(url, outputPath);
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
await downloadImage(url, outputPath);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
const tablet10Screenshots = localeData.screenshots?.tablet10;
|
|
573
|
+
if (tablet10Screenshots && tablet10Screenshots.length > 0) {
|
|
574
|
+
for (let i = 0; i < tablet10Screenshots.length; i++) {
|
|
575
|
+
const url = tablet10Screenshots[i];
|
|
576
|
+
const filename = `tablet10-${i + 1}.png`;
|
|
577
|
+
const outputPath = path4.join(asoDir, filename);
|
|
578
|
+
if (isLocalAssetPath(url)) {
|
|
579
|
+
copyLocalAssetToAsoDir(url, outputPath);
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
await downloadImage(url, outputPath);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
const tabletScreenshots = localeData.screenshots?.tablet;
|
|
586
|
+
if (tabletScreenshots && tabletScreenshots.length > 0) {
|
|
587
|
+
for (let i = 0; i < tabletScreenshots.length; i++) {
|
|
588
|
+
const url = tabletScreenshots[i];
|
|
589
|
+
const filename = `tablet-${i + 1}.png`;
|
|
590
|
+
const outputPath = path4.join(asoDir, filename);
|
|
591
|
+
if (isLocalAssetPath(url)) {
|
|
592
|
+
copyLocalAssetToAsoDir(url, outputPath);
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
await downloadImage(url, outputPath);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (localeData.featureGraphic) {
|
|
599
|
+
const featureGraphicUrl = localeData.featureGraphic;
|
|
600
|
+
const outputPath = path4.join(asoDir, "feature-graphic.png");
|
|
601
|
+
if (isLocalAssetPath(featureGraphicUrl)) {
|
|
602
|
+
copyLocalAssetToAsoDir(featureGraphicUrl, outputPath);
|
|
603
|
+
} else {
|
|
604
|
+
await downloadImage(featureGraphicUrl, outputPath);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (asoData.appStore) {
|
|
610
|
+
let appStoreData = asoData.appStore;
|
|
611
|
+
if (!isAppStoreMultilingual(appStoreData)) {
|
|
612
|
+
appStoreData = convertToMultilingual(appStoreData, appStoreData.locale);
|
|
613
|
+
}
|
|
614
|
+
for (const unifiedLocale of Object.keys(appStoreData.locales)) {
|
|
615
|
+
const appStoreLocale = unifiedToAppStore(unifiedLocale);
|
|
616
|
+
if (!appStoreLocale) {
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
const localeData = appStoreData.locales[unifiedLocale];
|
|
620
|
+
const asoDir = path4.join(
|
|
621
|
+
productStoreRoot,
|
|
622
|
+
"app-store",
|
|
623
|
+
"screenshots",
|
|
624
|
+
appStoreLocale
|
|
625
|
+
);
|
|
626
|
+
const iphone65Screenshots = localeData.screenshots?.iphone65;
|
|
627
|
+
if (iphone65Screenshots && iphone65Screenshots.length > 0) {
|
|
628
|
+
for (let i = 0; i < iphone65Screenshots.length; i++) {
|
|
629
|
+
const url = iphone65Screenshots[i];
|
|
630
|
+
const filename = `iphone65-${i + 1}.png`;
|
|
631
|
+
const outputPath = path4.join(asoDir, filename);
|
|
632
|
+
if (isLocalAssetPath(url)) {
|
|
633
|
+
copyLocalAssetToAsoDir(url, outputPath);
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
await downloadImage(url, outputPath);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
const ipadPro129Screenshots = localeData.screenshots?.ipadPro129;
|
|
640
|
+
if (ipadPro129Screenshots && ipadPro129Screenshots.length > 0) {
|
|
641
|
+
for (let i = 0; i < ipadPro129Screenshots.length; i++) {
|
|
642
|
+
const url = ipadPro129Screenshots[i];
|
|
643
|
+
const filename = `ipadPro129-${i + 1}.png`;
|
|
644
|
+
const outputPath = path4.join(asoDir, filename);
|
|
645
|
+
if (isLocalAssetPath(url)) {
|
|
646
|
+
copyLocalAssetToAsoDir(url, outputPath);
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
await downloadImage(url, outputPath);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
var publicToAsoTool = {
|
|
656
|
+
name: "public-to-aso",
|
|
657
|
+
description: `Prepares ASO data from public/products/[slug]/ to .aso/pushData format.
|
|
658
|
+
|
|
659
|
+
**IMPORTANT:** The 'slug' parameter is REQUIRED. If the user does not provide a slug, you MUST ask them to provide it. This tool processes only ONE product at a time.
|
|
660
|
+
|
|
661
|
+
This tool:
|
|
662
|
+
1. Loads ASO data from public/products/[slug]/config.json + locales/
|
|
663
|
+
2. Converts to store-compatible format (removes screenshots from metadata, sets contactWebsite/marketingUrl)
|
|
664
|
+
3. Saves metadata to .aso/pushData/products/[slug]/store/
|
|
665
|
+
4. Copies/downloads screenshots to .aso/pushData/products/[slug]/store/screenshots/
|
|
666
|
+
|
|
667
|
+
Before running, review ${FIELD_LIMITS_DOC_PATH} for per-store limits. This prepares data for pushing to stores without actually uploading.`,
|
|
668
|
+
inputSchema: inputSchema2
|
|
669
|
+
};
|
|
670
|
+
async function handlePublicToAso(input) {
|
|
671
|
+
const { slug, dryRun } = input;
|
|
672
|
+
const configData = loadAsoFromConfig(slug);
|
|
673
|
+
if (!configData.googlePlay && !configData.appStore) {
|
|
674
|
+
throw new Error(`No ASO data found in config.json + locales/ for ${slug}`);
|
|
675
|
+
}
|
|
676
|
+
const storeData = prepareAsoDataForPush(slug, configData);
|
|
677
|
+
if (dryRun) {
|
|
678
|
+
return {
|
|
679
|
+
content: [
|
|
680
|
+
{
|
|
681
|
+
type: "text",
|
|
682
|
+
text: `Preview mode - Data that would be saved to .aso/pushData:
|
|
683
|
+
|
|
684
|
+
${JSON.stringify(
|
|
685
|
+
storeData,
|
|
686
|
+
null,
|
|
687
|
+
2
|
|
688
|
+
)}`
|
|
689
|
+
}
|
|
690
|
+
]
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
const pushDataRoot = ".aso/pushData";
|
|
694
|
+
saveRawAsoData(slug, storeData, { rootDir: pushDataRoot });
|
|
695
|
+
await downloadScreenshotsToAsoDir(slug, configData, {
|
|
696
|
+
rootDir: pushDataRoot
|
|
697
|
+
});
|
|
698
|
+
const localeCounts = {};
|
|
699
|
+
if (storeData.googlePlay) {
|
|
700
|
+
const googlePlayData = storeData.googlePlay;
|
|
701
|
+
const locales = isGooglePlayMultilingual(googlePlayData) ? googlePlayData.locales : {
|
|
702
|
+
[googlePlayData.defaultLanguage || DEFAULT_LOCALE]: googlePlayData
|
|
703
|
+
};
|
|
704
|
+
localeCounts.googlePlay = Object.keys(locales).length;
|
|
705
|
+
}
|
|
706
|
+
if (storeData.appStore) {
|
|
707
|
+
const appStoreData = storeData.appStore;
|
|
708
|
+
const locales = isAppStoreMultilingual(appStoreData) ? appStoreData.locales : { [appStoreData.locale || DEFAULT_LOCALE]: appStoreData };
|
|
709
|
+
localeCounts.appStore = Object.keys(locales).length;
|
|
710
|
+
}
|
|
711
|
+
let responseText = `\u2705 ${slug} .aso/pushData files prepared from config.json + locales/ (images + metadata synced)
|
|
712
|
+
|
|
713
|
+
`;
|
|
714
|
+
if (localeCounts.googlePlay) {
|
|
715
|
+
responseText += `Google Play: ${localeCounts.googlePlay} locale(s)
|
|
716
|
+
`;
|
|
717
|
+
}
|
|
718
|
+
if (localeCounts.appStore) {
|
|
719
|
+
responseText += `App Store: ${localeCounts.appStore} locale(s)
|
|
720
|
+
`;
|
|
721
|
+
}
|
|
722
|
+
responseText += `
|
|
723
|
+
Next step: Push to stores using pabal-mcp's aso-push tool`;
|
|
724
|
+
responseText += `
|
|
725
|
+
Reference: ${FIELD_LIMITS_DOC_PATH}`;
|
|
726
|
+
return {
|
|
727
|
+
content: [
|
|
728
|
+
{
|
|
729
|
+
type: "text",
|
|
730
|
+
text: responseText
|
|
731
|
+
}
|
|
732
|
+
]
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// src/tools/improve-public.ts
|
|
737
|
+
import { z as z3 } from "zod";
|
|
738
|
+
import { zodToJsonSchema as zodToJsonSchema3 } from "zod-to-json-schema";
|
|
739
|
+
|
|
740
|
+
// src/tools/utils/improve-public/load-product-locales.util.ts
|
|
741
|
+
import fs4 from "fs";
|
|
742
|
+
import path5 from "path";
|
|
743
|
+
function loadProductLocales(slug) {
|
|
744
|
+
const productDir = path5.join(process.cwd(), "public", "products", slug);
|
|
745
|
+
const configPath = path5.join(productDir, "config.json");
|
|
746
|
+
const localesDir = path5.join(productDir, "locales");
|
|
747
|
+
let config = null;
|
|
748
|
+
if (fs4.existsSync(configPath)) {
|
|
749
|
+
const raw = fs4.readFileSync(configPath, "utf-8");
|
|
750
|
+
config = JSON.parse(raw);
|
|
751
|
+
}
|
|
752
|
+
if (!fs4.existsSync(localesDir)) {
|
|
753
|
+
throw new Error(`No locales directory found for ${slug}`);
|
|
754
|
+
}
|
|
755
|
+
const locales = {};
|
|
756
|
+
const localeFiles = fs4.readdirSync(localesDir).filter((file) => file.endsWith(".json"));
|
|
757
|
+
if (localeFiles.length === 0) {
|
|
758
|
+
throw new Error(`No locale files found for ${slug}`);
|
|
759
|
+
}
|
|
760
|
+
for (const file of localeFiles) {
|
|
761
|
+
const localeCode = file.replace(".json", "");
|
|
762
|
+
const localePath = path5.join(localesDir, file);
|
|
763
|
+
const content = fs4.readFileSync(localePath, "utf-8");
|
|
764
|
+
locales[localeCode] = JSON.parse(content);
|
|
765
|
+
}
|
|
766
|
+
return { config, locales };
|
|
767
|
+
}
|
|
768
|
+
function resolvePrimaryLocale(config, locales) {
|
|
769
|
+
const localeKeys = Object.keys(locales);
|
|
770
|
+
if (localeKeys.length === 0) {
|
|
771
|
+
return DEFAULT_LOCALE;
|
|
772
|
+
}
|
|
773
|
+
const configuredDefault = config?.content?.defaultLocale;
|
|
774
|
+
if (configuredDefault && locales[configuredDefault]) {
|
|
775
|
+
return configuredDefault;
|
|
776
|
+
}
|
|
777
|
+
if (locales[DEFAULT_LOCALE]) {
|
|
778
|
+
return DEFAULT_LOCALE;
|
|
779
|
+
}
|
|
780
|
+
return localeKeys[0];
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// src/tools/utils/improve-public/get-full-description.util.ts
|
|
784
|
+
function getFullDescriptionForLocale(asoData, locale) {
|
|
785
|
+
if (asoData.googlePlay) {
|
|
786
|
+
const googlePlayData = asoData.googlePlay;
|
|
787
|
+
if (isGooglePlayMultilingual(googlePlayData)) {
|
|
788
|
+
const localeData = googlePlayData.locales[locale];
|
|
789
|
+
if (localeData?.fullDescription) {
|
|
790
|
+
return localeData.fullDescription;
|
|
791
|
+
}
|
|
792
|
+
} else if (googlePlayData.defaultLanguage === locale) {
|
|
793
|
+
return googlePlayData.fullDescription;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
if (asoData.appStore) {
|
|
797
|
+
const appStoreData = asoData.appStore;
|
|
798
|
+
if (isAppStoreMultilingual(appStoreData)) {
|
|
799
|
+
const localeData = appStoreData.locales[locale];
|
|
800
|
+
if (localeData?.description) {
|
|
801
|
+
return localeData.description;
|
|
802
|
+
}
|
|
803
|
+
} else if (appStoreData.locale === locale) {
|
|
804
|
+
return appStoreData.description;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return void 0;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// src/tools/utils/improve-public/keyword-analysis.util.ts
|
|
811
|
+
function analyzeKeywords(content) {
|
|
812
|
+
const words = content.toLowerCase().replace(/[^\w\s]/g, " ").split(/\s+/).filter((word) => word.length > 2);
|
|
813
|
+
const frequency = {};
|
|
814
|
+
for (const word of words) {
|
|
815
|
+
frequency[word] = (frequency[word] || 0) + 1;
|
|
816
|
+
}
|
|
817
|
+
const totalWords = words.length;
|
|
818
|
+
const density = {};
|
|
819
|
+
for (const [word, count] of Object.entries(frequency)) {
|
|
820
|
+
density[word] = count / totalWords * 100;
|
|
821
|
+
}
|
|
822
|
+
return { keywordFrequency: frequency, keywordDensity: density, totalWords };
|
|
823
|
+
}
|
|
824
|
+
function extractKeywordsFromContent(localeData) {
|
|
825
|
+
const keywords = /* @__PURE__ */ new Set();
|
|
826
|
+
const aso = localeData.aso || {};
|
|
827
|
+
const landing = localeData.landing || {};
|
|
828
|
+
if (Array.isArray(aso.keywords)) {
|
|
829
|
+
aso.keywords.forEach((kw) => keywords.add(kw.toLowerCase()));
|
|
830
|
+
} else if (typeof aso.keywords === "string") {
|
|
831
|
+
aso.keywords.split(",").map((kw) => kw.trim().toLowerCase()).forEach((kw) => keywords.add(kw));
|
|
832
|
+
}
|
|
833
|
+
[aso.title, aso.subtitle, aso.shortDescription].forEach((text) => {
|
|
834
|
+
if (text) {
|
|
835
|
+
text.toLowerCase().split(/\s+/).filter((word) => word.length > 2).forEach((word) => keywords.add(word));
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
if (aso.template?.intro) {
|
|
839
|
+
aso.template.intro.toLowerCase().replace(/[^\w\s]/g, " ").split(/\s+/).filter((word) => word.length > 3).forEach((word) => keywords.add(word));
|
|
840
|
+
}
|
|
841
|
+
if (aso.template?.outro) {
|
|
842
|
+
aso.template.outro.toLowerCase().replace(/[^\w\s]/g, " ").split(/\s+/).filter((word) => word.length > 3).forEach((word) => keywords.add(word));
|
|
843
|
+
}
|
|
844
|
+
const hero = landing.hero || {};
|
|
845
|
+
[hero.title, hero.description].forEach((text) => {
|
|
846
|
+
if (text) {
|
|
847
|
+
text.toLowerCase().replace(/[^\w\s]/g, " ").split(/\s+/).filter((word) => word.length > 3).forEach((word) => keywords.add(word));
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
const features = landing.features?.items || [];
|
|
851
|
+
features.forEach((feature) => {
|
|
852
|
+
if (feature.title) {
|
|
853
|
+
feature.title.toLowerCase().replace(/[^\w\s]/g, " ").split(/\s+/).filter((word) => word.length > 3).forEach((word) => keywords.add(word));
|
|
854
|
+
}
|
|
855
|
+
if (feature.body) {
|
|
856
|
+
feature.body.toLowerCase().replace(/[^\w\s]/g, " ").split(/\s+/).filter((word) => word.length > 3).forEach((word) => keywords.add(word));
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
const screenshots = landing.screenshots?.images || [];
|
|
860
|
+
screenshots.forEach((screenshot) => {
|
|
861
|
+
if (screenshot.title) {
|
|
862
|
+
screenshot.title.toLowerCase().replace(/[^\w\s]/g, " ").split(/\s+/).filter((word) => word.length > 3).forEach((word) => keywords.add(word));
|
|
863
|
+
}
|
|
864
|
+
if (screenshot.description) {
|
|
865
|
+
screenshot.description.toLowerCase().replace(/[^\w\s]/g, " ").split(/\s+/).filter((word) => word.length > 3).forEach((word) => keywords.add(word));
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
return Array.from(keywords);
|
|
869
|
+
}
|
|
870
|
+
function generateKeywordResearchQueries(args) {
|
|
871
|
+
const { currentKeywords, category, title, features = [], screenshots = [] } = args;
|
|
872
|
+
const queries = [];
|
|
873
|
+
if (category) {
|
|
874
|
+
const categoryName = category.toLowerCase().replace(/_/g, " ");
|
|
875
|
+
queries.push(`ASO keywords ${categoryName} app store optimization`);
|
|
876
|
+
queries.push(`best keywords for ${categoryName} apps`);
|
|
877
|
+
queries.push(`top ${categoryName} apps keywords`);
|
|
878
|
+
}
|
|
879
|
+
if (title) {
|
|
880
|
+
const titleWords = title.toLowerCase().split(/\s+/).filter((w) => w.length > 3);
|
|
881
|
+
titleWords.forEach((word) => {
|
|
882
|
+
queries.push(`${word} app keywords ASO`);
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
const featureTerms = /* @__PURE__ */ new Set();
|
|
886
|
+
features.forEach((feature) => {
|
|
887
|
+
if (feature.title) {
|
|
888
|
+
const words = feature.title.toLowerCase().replace(/[^\w\s]/g, " ").split(/\s+/).filter((w) => w.length > 4);
|
|
889
|
+
words.forEach((word) => featureTerms.add(word));
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
Array.from(featureTerms).slice(0, 3).forEach((term) => {
|
|
893
|
+
queries.push(`${term} app store keywords`);
|
|
894
|
+
});
|
|
895
|
+
const screenshotTerms = /* @__PURE__ */ new Set();
|
|
896
|
+
screenshots.forEach((screenshot) => {
|
|
897
|
+
if (screenshot.title) {
|
|
898
|
+
const words = screenshot.title.toLowerCase().replace(/[^\w\s]/g, " ").split(/\s+/).filter((w) => w.length > 4);
|
|
899
|
+
words.forEach((word) => screenshotTerms.add(word));
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
Array.from(screenshotTerms).slice(0, 3).forEach((term) => {
|
|
903
|
+
queries.push(`${term} app keywords`);
|
|
904
|
+
});
|
|
905
|
+
currentKeywords.slice(0, 5).forEach((keyword) => {
|
|
906
|
+
if (keyword.length > 3) {
|
|
907
|
+
queries.push(`${keyword} app store keywords`);
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
return queries;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// src/tools/utils/improve-public/format-locale-section.util.ts
|
|
914
|
+
function generateKeywordSuggestions(args) {
|
|
915
|
+
const { currentKeywords, category, title, description, locale, features = [], screenshots = [] } = args;
|
|
916
|
+
const analysis = analyzeKeywords(description);
|
|
917
|
+
const topKeywords = Object.entries(analysis.keywordDensity).sort(([, a], [, b]) => b - a).slice(0, 10).map(([word]) => word);
|
|
918
|
+
const researchQueries = generateKeywordResearchQueries({
|
|
919
|
+
currentKeywords,
|
|
920
|
+
category,
|
|
921
|
+
title,
|
|
922
|
+
locale,
|
|
923
|
+
features,
|
|
924
|
+
screenshots
|
|
925
|
+
});
|
|
926
|
+
let suggestions = `## Keyword Analysis & Research (${locale})
|
|
927
|
+
|
|
928
|
+
`;
|
|
929
|
+
suggestions += `### Current Keywords:
|
|
930
|
+
`;
|
|
931
|
+
suggestions += `- ${currentKeywords.join(", ") || "(none)"}
|
|
932
|
+
|
|
933
|
+
`;
|
|
934
|
+
suggestions += `### Top Keywords Found in Content (by frequency):
|
|
935
|
+
`;
|
|
936
|
+
topKeywords.slice(0, 10).forEach((keyword, idx) => {
|
|
937
|
+
const density = analysis.keywordDensity[keyword]?.toFixed(2) || "0";
|
|
938
|
+
suggestions += `${idx + 1}. "${keyword}" (density: ${density}%)
|
|
939
|
+
`;
|
|
940
|
+
});
|
|
941
|
+
suggestions += `
|
|
942
|
+
`;
|
|
943
|
+
suggestions += `### Keyword Research Queries (MULTIPLE SEARCH STRATEGIES REQUIRED):
|
|
944
|
+
`;
|
|
945
|
+
suggestions += `**IMPORTANT**: Use ALL 5 search strategies below. Don't rely on just one approach.
|
|
946
|
+
|
|
947
|
+
`;
|
|
948
|
+
suggestions += `#### Strategy 1: Direct Store Search
|
|
949
|
+
`;
|
|
950
|
+
suggestions += `- Search App Store/Play Store: "${category ? category.toLowerCase().replace(/_/g, " ") : "category"} apps"
|
|
951
|
+
`;
|
|
952
|
+
suggestions += `- Visit top 5-10 apps, extract keywords from their names, descriptions, screenshots
|
|
953
|
+
|
|
954
|
+
`;
|
|
955
|
+
suggestions += `#### Strategy 2: ASO Guides & Tools
|
|
956
|
+
`;
|
|
957
|
+
researchQueries.slice(0, 4).forEach((query, idx) => {
|
|
958
|
+
suggestions += `${idx + 1}. "${query}"
|
|
959
|
+
`;
|
|
960
|
+
});
|
|
961
|
+
suggestions += `
|
|
962
|
+
`;
|
|
963
|
+
suggestions += `#### Strategy 3: User Intent Searches
|
|
964
|
+
`;
|
|
965
|
+
if (title) {
|
|
966
|
+
const titleWords = title.toLowerCase().split(/\s+/).filter((w) => w.length > 3);
|
|
967
|
+
titleWords.slice(0, 3).forEach((word, idx) => {
|
|
968
|
+
suggestions += `${idx + 1}. "${word} app"
|
|
969
|
+
`;
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
suggestions += `- Search: "[primary feature] app", "[problem solved] app"
|
|
973
|
+
|
|
974
|
+
`;
|
|
975
|
+
suggestions += `#### Strategy 4: Competitor Analysis
|
|
976
|
+
`;
|
|
977
|
+
suggestions += `- Search: "top ${category ? category.toLowerCase().replace(/_/g, " ") : "category"} apps"
|
|
978
|
+
`;
|
|
979
|
+
suggestions += `- "best ${category ? category.toLowerCase().replace(/_/g, " ") : "category"} apps 2024"
|
|
980
|
+
|
|
981
|
+
`;
|
|
982
|
+
suggestions += `#### Strategy 5: Long-tail Keywords
|
|
983
|
+
`;
|
|
984
|
+
currentKeywords.slice(0, 3).forEach((keyword, idx) => {
|
|
985
|
+
if (keyword.length > 3) {
|
|
986
|
+
suggestions += `${idx + 1}. "${keyword} app store keywords"
|
|
987
|
+
`;
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
suggestions += `
|
|
991
|
+
`;
|
|
992
|
+
suggestions += `**What to Extract**:
|
|
993
|
+
`;
|
|
994
|
+
suggestions += `- Exact keyword phrases (2-4 words) from top apps
|
|
995
|
+
`;
|
|
996
|
+
suggestions += `- Feature terms, benefit terms, category jargon
|
|
997
|
+
`;
|
|
998
|
+
suggestions += `- Action verbs and technical terms
|
|
999
|
+
`;
|
|
1000
|
+
suggestions += `- Minimum 15-20 keywords, prioritize those used by 3+ top apps
|
|
1001
|
+
|
|
1002
|
+
`;
|
|
1003
|
+
suggestions += `### Keyword Optimization Guidelines:
|
|
1004
|
+
`;
|
|
1005
|
+
suggestions += `1. **Keywords Array (CRITICAL)**: Update aso.keywords array with researched keywords. This directly impacts App Store search visibility
|
|
1006
|
+
`;
|
|
1007
|
+
suggestions += `2. **Keyword Density**: Maintain 2.5-3% density for primary keywords, distributed naturally
|
|
1008
|
+
`;
|
|
1009
|
+
suggestions += `3. **Title/Subtitle**: Include 1-2 core keywords (30 char limit)
|
|
1010
|
+
`;
|
|
1011
|
+
suggestions += `4. **Short Description**: Include searchable keywords (80 char limit)
|
|
1012
|
+
`;
|
|
1013
|
+
suggestions += `5. **Full Description**: Place primary keywords in first 2-3 lines, distribute naturally throughout
|
|
1014
|
+
`;
|
|
1015
|
+
suggestions += `6. **template.intro**: Use up to 300 chars to naturally incorporate more keywords and provide richer context
|
|
1016
|
+
`;
|
|
1017
|
+
suggestions += `7. **App Store Keywords**: 100 char limit, comma-separated, avoid duplicates from name/subtitle
|
|
1018
|
+
|
|
1019
|
+
`;
|
|
1020
|
+
if (category) {
|
|
1021
|
+
suggestions += `### Category-Based Keyword Strategy:
|
|
1022
|
+
`;
|
|
1023
|
+
suggestions += `Category: ${category}
|
|
1024
|
+
`;
|
|
1025
|
+
suggestions += `- Research top-ranking apps in "${category}" category
|
|
1026
|
+
`;
|
|
1027
|
+
suggestions += `- Identify common keywords used by successful competitors
|
|
1028
|
+
`;
|
|
1029
|
+
suggestions += `- Focus on user search intent and specific terms users actually search for
|
|
1030
|
+
`;
|
|
1031
|
+
suggestions += `- Avoid brand names and competitor names
|
|
1032
|
+
|
|
1033
|
+
`;
|
|
1034
|
+
}
|
|
1035
|
+
return suggestions;
|
|
1036
|
+
}
|
|
1037
|
+
function formatLocaleSection(args) {
|
|
1038
|
+
const {
|
|
1039
|
+
slug,
|
|
1040
|
+
locale,
|
|
1041
|
+
localeData,
|
|
1042
|
+
fullDescription,
|
|
1043
|
+
primaryLocale,
|
|
1044
|
+
category
|
|
1045
|
+
} = args;
|
|
1046
|
+
const aso = localeData.aso || {};
|
|
1047
|
+
const template = aso.template;
|
|
1048
|
+
const landing = localeData.landing || {};
|
|
1049
|
+
const hero = landing.hero || {};
|
|
1050
|
+
const screenshots = landing.screenshots?.images || [];
|
|
1051
|
+
const features = landing.features?.items || [];
|
|
1052
|
+
const lengthOf = (value) => value ? value.length : 0;
|
|
1053
|
+
const keywordsLength = Array.isArray(aso.keywords) ? aso.keywords.join(", ").length : lengthOf(typeof aso.keywords === "string" ? aso.keywords : void 0);
|
|
1054
|
+
const header = `--- ${locale}${locale === primaryLocale ? " (primary)" : ""} ---`;
|
|
1055
|
+
const stats = [
|
|
1056
|
+
`Path: public/products/${slug}/locales/${locale}.json`,
|
|
1057
|
+
`- aso.title: ${lengthOf(aso.title)} chars`,
|
|
1058
|
+
`- aso.subtitle: ${lengthOf(aso.subtitle)} chars`,
|
|
1059
|
+
`- aso.shortDescription: ${lengthOf(aso.shortDescription)} chars`,
|
|
1060
|
+
`- aso.keywords: ${keywordsLength} chars total`,
|
|
1061
|
+
`- template.intro: ${lengthOf(template?.intro)} chars (limit: 300)`,
|
|
1062
|
+
`- template.outro: ${lengthOf(template?.outro)} chars (limit: 200)`,
|
|
1063
|
+
`- landing.hero.title: ${lengthOf(hero.title)} chars`,
|
|
1064
|
+
`- landing.hero.description: ${lengthOf(hero.description)} chars`,
|
|
1065
|
+
`- features: ${features.length} items`,
|
|
1066
|
+
`- screenshots: ${screenshots.length} captions`,
|
|
1067
|
+
`- fullDescription (derived): ${fullDescription?.length ?? 0} chars`
|
|
1068
|
+
].join("\n");
|
|
1069
|
+
const currentKeywords = extractKeywordsFromContent(localeData);
|
|
1070
|
+
const landingText = [
|
|
1071
|
+
hero.title,
|
|
1072
|
+
hero.description,
|
|
1073
|
+
...screenshots.map((img) => `${img.title} ${img.description || ""}`),
|
|
1074
|
+
...features.map((item) => `${item.title} ${item.body || ""}`),
|
|
1075
|
+
landing.reviews?.title,
|
|
1076
|
+
landing.reviews?.description,
|
|
1077
|
+
landing.cta?.headline,
|
|
1078
|
+
landing.cta?.description
|
|
1079
|
+
].filter(Boolean).join(" ");
|
|
1080
|
+
const fullText = [
|
|
1081
|
+
aso.title,
|
|
1082
|
+
aso.subtitle,
|
|
1083
|
+
aso.shortDescription,
|
|
1084
|
+
template?.intro,
|
|
1085
|
+
template?.outro,
|
|
1086
|
+
fullDescription,
|
|
1087
|
+
landingText
|
|
1088
|
+
].filter(Boolean).join(" ");
|
|
1089
|
+
const keywordAnalysis = generateKeywordSuggestions({
|
|
1090
|
+
currentKeywords,
|
|
1091
|
+
category,
|
|
1092
|
+
title: aso.title,
|
|
1093
|
+
description: fullText,
|
|
1094
|
+
locale,
|
|
1095
|
+
features,
|
|
1096
|
+
screenshots
|
|
1097
|
+
});
|
|
1098
|
+
const json = JSON.stringify(localeData, null, 2);
|
|
1099
|
+
return `${header}
|
|
1100
|
+
${stats}
|
|
1101
|
+
|
|
1102
|
+
${keywordAnalysis}
|
|
1103
|
+
\`\`\`json
|
|
1104
|
+
${json}
|
|
1105
|
+
\`\`\`
|
|
1106
|
+
`;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// src/tools/utils/improve-public/generate-aso-prompt.util.ts
|
|
1110
|
+
var FIELD_LIMITS_DOC_PATH2 = "docs/aso/ASO_FIELD_LIMITS.md";
|
|
1111
|
+
function generatePrimaryOptimizationPrompt(args) {
|
|
1112
|
+
const { slug, category, primaryLocale, localeSections } = args;
|
|
1113
|
+
let prompt = `# ASO Optimization - Stage 1: Primary Locale
|
|
1114
|
+
|
|
1115
|
+
`;
|
|
1116
|
+
prompt += `Product: ${slug} | Category: ${category || "N/A"} | Primary: ${primaryLocale}
|
|
1117
|
+
|
|
1118
|
+
`;
|
|
1119
|
+
prompt += `## Task
|
|
1120
|
+
|
|
1121
|
+
`;
|
|
1122
|
+
prompt += `Optimize the PRIMARY locale (${primaryLocale}) with keyword research + full ASO field optimization.
|
|
1123
|
+
|
|
1124
|
+
`;
|
|
1125
|
+
prompt += `## Step 1: Keyword Research (${primaryLocale})
|
|
1126
|
+
|
|
1127
|
+
`;
|
|
1128
|
+
prompt += `**Strategies** (apply all 5):
|
|
1129
|
+
`;
|
|
1130
|
+
prompt += `1. **App Store**: Search top apps in category \u2192 extract keywords from titles/descriptions
|
|
1131
|
+
`;
|
|
1132
|
+
prompt += `2. **Description-Based**: Use current shortDescription \u2192 find related ASO keywords
|
|
1133
|
+
`;
|
|
1134
|
+
prompt += `3. **Feature-Based**: Identify 2-3 core features \u2192 search feature-specific keywords
|
|
1135
|
+
`;
|
|
1136
|
+
prompt += `4. **User Intent**: Research user search patterns for the app's use case
|
|
1137
|
+
`;
|
|
1138
|
+
prompt += `5. **Competitor**: Analyze 3+ successful apps \u2192 find common keyword patterns
|
|
1139
|
+
|
|
1140
|
+
`;
|
|
1141
|
+
prompt += `**Output**: 10 high-performing keywords
|
|
1142
|
+
|
|
1143
|
+
`;
|
|
1144
|
+
prompt += `## Step 2: Optimize All Fields (${primaryLocale})
|
|
1145
|
+
|
|
1146
|
+
`;
|
|
1147
|
+
prompt += `Apply the 10 keywords to ALL fields:
|
|
1148
|
+
`;
|
|
1149
|
+
prompt += `- \`aso.title\` (\u226430): **"App Name: Primary Keyword"** format (app name in English, keyword in target language, keyword starts with uppercase after the colon)
|
|
1150
|
+
`;
|
|
1151
|
+
prompt += `- \`aso.subtitle\` (\u226430): Complementary keywords
|
|
1152
|
+
`;
|
|
1153
|
+
prompt += `- \`aso.shortDescription\` (\u226480): Primary keywords (no emojis/CAPS)
|
|
1154
|
+
`;
|
|
1155
|
+
prompt += `- \`aso.keywords\` (\u2264100): Comma-separated 10 keywords
|
|
1156
|
+
`;
|
|
1157
|
+
prompt += `- \`aso.template.intro\` (\u2264300): Keyword-rich, use full length
|
|
1158
|
+
`;
|
|
1159
|
+
prompt += `- \`aso.template.outro\` (\u2264200): Natural keyword integration
|
|
1160
|
+
`;
|
|
1161
|
+
prompt += `- \`landing.hero.title\`: Primary keywords
|
|
1162
|
+
`;
|
|
1163
|
+
prompt += `- \`landing.hero.description\`: Keywords if present
|
|
1164
|
+
`;
|
|
1165
|
+
prompt += `- \`landing.screenshots.images[].title\`: Keywords in screenshot titles
|
|
1166
|
+
`;
|
|
1167
|
+
prompt += `- \`landing.screenshots.images[].description\`: Keywords in screenshot descriptions
|
|
1168
|
+
`;
|
|
1169
|
+
prompt += `- \`landing.features.items[].title\`: Keywords in feature titles
|
|
1170
|
+
`;
|
|
1171
|
+
prompt += `- \`landing.features.items[].body\`: Keywords in feature descriptions
|
|
1172
|
+
`;
|
|
1173
|
+
prompt += `- \`landing.reviews.title\`: Keywords if applicable
|
|
1174
|
+
`;
|
|
1175
|
+
prompt += `- \`landing.reviews.description\`: Keywords if applicable
|
|
1176
|
+
`;
|
|
1177
|
+
prompt += `- \`landing.cta.headline\`: Keywords if applicable
|
|
1178
|
+
`;
|
|
1179
|
+
prompt += `- \`landing.cta.description\`: Keywords if applicable
|
|
1180
|
+
|
|
1181
|
+
`;
|
|
1182
|
+
prompt += `**Guidelines**: 2.5-3% keyword density, natural flow, cultural appropriateness
|
|
1183
|
+
`;
|
|
1184
|
+
prompt += `**CRITICAL**: You MUST include the complete \`landing\` object in your optimized JSON output, with all screenshots, features, reviews, and cta sections properly translated and keyword-optimized.
|
|
1185
|
+
|
|
1186
|
+
`;
|
|
1187
|
+
prompt += `## Step 3: Validate
|
|
1188
|
+
|
|
1189
|
+
`;
|
|
1190
|
+
prompt += `Check all limits: title \u226430, subtitle \u226430, shortDescription \u226480, keywords \u2264100, intro \u2264300, outro \u2264200
|
|
1191
|
+
|
|
1192
|
+
`;
|
|
1193
|
+
prompt += `## Current Data
|
|
1194
|
+
|
|
1195
|
+
`;
|
|
1196
|
+
prompt += `${localeSections.find((s) => s.includes(`[${primaryLocale}]`)) || localeSections[0]}
|
|
1197
|
+
|
|
1198
|
+
`;
|
|
1199
|
+
prompt += `## Output Format
|
|
1200
|
+
|
|
1201
|
+
`;
|
|
1202
|
+
prompt += `**1. Keyword Research**
|
|
1203
|
+
`;
|
|
1204
|
+
prompt += ` - Query: "[query]" \u2192 Found: [apps] \u2192 Keywords: [list]
|
|
1205
|
+
`;
|
|
1206
|
+
prompt += ` - Final 10 keywords: [list] with rationale
|
|
1207
|
+
|
|
1208
|
+
`;
|
|
1209
|
+
prompt += `**2. Optimized JSON** (complete ${primaryLocale} locale structure)
|
|
1210
|
+
`;
|
|
1211
|
+
prompt += ` - MUST include complete \`aso\` object with all fields
|
|
1212
|
+
`;
|
|
1213
|
+
prompt += ` - MUST include complete \`landing\` object with:
|
|
1214
|
+
`;
|
|
1215
|
+
prompt += ` * \`landing.hero\` (title, description, titleHighlight)
|
|
1216
|
+
`;
|
|
1217
|
+
prompt += ` * \`landing.screenshots.images[]\` (all items with title and description)
|
|
1218
|
+
`;
|
|
1219
|
+
prompt += ` * \`landing.features.items[]\` (all items with title and body)
|
|
1220
|
+
`;
|
|
1221
|
+
prompt += ` * \`landing.reviews\` (title, description, icons, rating, testimonials)
|
|
1222
|
+
`;
|
|
1223
|
+
prompt += ` * \`landing.cta\` (headline, icons, rating, description)
|
|
1224
|
+
|
|
1225
|
+
`;
|
|
1226
|
+
prompt += `**3. Validation**
|
|
1227
|
+
`;
|
|
1228
|
+
prompt += ` - title: X/30 \u2713/\u2717
|
|
1229
|
+
`;
|
|
1230
|
+
prompt += ` - subtitle: X/30 \u2713/\u2717
|
|
1231
|
+
`;
|
|
1232
|
+
prompt += ` - shortDescription: X/80 \u2713/\u2717
|
|
1233
|
+
`;
|
|
1234
|
+
prompt += ` - keywords: X/100 \u2713/\u2717
|
|
1235
|
+
`;
|
|
1236
|
+
prompt += ` - intro: X/300 \u2713/\u2717
|
|
1237
|
+
`;
|
|
1238
|
+
prompt += ` - outro: X/200 \u2713/\u2717
|
|
1239
|
+
`;
|
|
1240
|
+
prompt += ` - Density: X% (2.5-3%) \u2713/\u2717
|
|
1241
|
+
|
|
1242
|
+
`;
|
|
1243
|
+
prompt += `**Reference**: ${FIELD_LIMITS_DOC_PATH2}
|
|
1244
|
+
`;
|
|
1245
|
+
return prompt;
|
|
1246
|
+
}
|
|
1247
|
+
function generateKeywordLocalizationPrompt(args) {
|
|
1248
|
+
const {
|
|
1249
|
+
slug,
|
|
1250
|
+
primaryLocale,
|
|
1251
|
+
targetLocales,
|
|
1252
|
+
localeSections,
|
|
1253
|
+
optimizedPrimary,
|
|
1254
|
+
batchLocales,
|
|
1255
|
+
batchIndex,
|
|
1256
|
+
totalBatches,
|
|
1257
|
+
batchLocaleSections
|
|
1258
|
+
} = args;
|
|
1259
|
+
const nonPrimaryLocales = batchLocales || targetLocales.filter((l) => l !== primaryLocale);
|
|
1260
|
+
const sectionsToUse = batchLocaleSections || localeSections;
|
|
1261
|
+
let prompt = `# ASO Optimization - Stage 2: Keyword Localization`;
|
|
1262
|
+
if (batchIndex !== void 0 && totalBatches !== void 0) {
|
|
1263
|
+
prompt += ` (Batch ${batchIndex + 1}/${totalBatches})`;
|
|
1264
|
+
}
|
|
1265
|
+
prompt += `
|
|
1266
|
+
|
|
1267
|
+
`;
|
|
1268
|
+
prompt += `Product: ${slug} | Primary: ${primaryLocale} | Batch Locales: ${nonPrimaryLocales.join(
|
|
1269
|
+
", "
|
|
1270
|
+
)}
|
|
1271
|
+
|
|
1272
|
+
`;
|
|
1273
|
+
if (batchIndex !== void 0 && totalBatches !== void 0) {
|
|
1274
|
+
prompt += `**\u26A0\uFE0F BATCH PROCESSING MODE**
|
|
1275
|
+
|
|
1276
|
+
`;
|
|
1277
|
+
prompt += `This is batch ${batchIndex + 1} of ${totalBatches}.
|
|
1278
|
+
`;
|
|
1279
|
+
prompt += `Process ONLY the locales in this batch: ${nonPrimaryLocales.join(
|
|
1280
|
+
", "
|
|
1281
|
+
)}
|
|
1282
|
+
`;
|
|
1283
|
+
prompt += `After completing this batch, save the files and proceed to the next batch.
|
|
1284
|
+
|
|
1285
|
+
`;
|
|
1286
|
+
}
|
|
1287
|
+
prompt += `## Task
|
|
1288
|
+
|
|
1289
|
+
`;
|
|
1290
|
+
prompt += `**CRITICAL: Only process locales that already exist in public/products/${slug}/locales/.**
|
|
1291
|
+
`;
|
|
1292
|
+
prompt += `**Do NOT create new locale files - only improve existing ones.**
|
|
1293
|
+
|
|
1294
|
+
`;
|
|
1295
|
+
prompt += `For EACH target locale in this batch:
|
|
1296
|
+
`;
|
|
1297
|
+
prompt += `1. Research 10 language-specific keywords
|
|
1298
|
+
`;
|
|
1299
|
+
prompt += `2. Replace keywords in translated content (preserve structure/tone/context)
|
|
1300
|
+
`;
|
|
1301
|
+
prompt += `3. Validate character limits
|
|
1302
|
+
`;
|
|
1303
|
+
prompt += `4. **SAVE the updated JSON to file** using the save-locale-file tool (only if file exists)
|
|
1304
|
+
|
|
1305
|
+
`;
|
|
1306
|
+
prompt += `## Optimized Primary (Reference)
|
|
1307
|
+
|
|
1308
|
+
`;
|
|
1309
|
+
prompt += `Use this as the base structure/messaging:
|
|
1310
|
+
\`\`\`json
|
|
1311
|
+
${optimizedPrimary}
|
|
1312
|
+
\`\`\`
|
|
1313
|
+
|
|
1314
|
+
`;
|
|
1315
|
+
prompt += `## Keyword Research (Per Locale)
|
|
1316
|
+
|
|
1317
|
+
`;
|
|
1318
|
+
prompt += `For EACH locale, perform lightweight keyword research:
|
|
1319
|
+
`;
|
|
1320
|
+
prompt += `1. **App Store Search**: "[core feature] [lang] \uC571/app" \u2192 top 5 apps
|
|
1321
|
+
`;
|
|
1322
|
+
prompt += `2. **Competitor Keywords**: Extract keywords from successful apps in that language
|
|
1323
|
+
`;
|
|
1324
|
+
prompt += `3. **Search Trends**: Check what users actually search in that language
|
|
1325
|
+
|
|
1326
|
+
`;
|
|
1327
|
+
prompt += `**Output**: 10 keywords per locale
|
|
1328
|
+
|
|
1329
|
+
`;
|
|
1330
|
+
prompt += `## Keyword Replacement Strategy
|
|
1331
|
+
|
|
1332
|
+
`;
|
|
1333
|
+
prompt += `For EACH locale:
|
|
1334
|
+
`;
|
|
1335
|
+
prompt += `1. Take the TRANSLATED content (below)
|
|
1336
|
+
`;
|
|
1337
|
+
prompt += `2. Replace \`aso.keywords\` array with new 10 keywords
|
|
1338
|
+
`;
|
|
1339
|
+
prompt += `3. **TITLE FORMAT**: \`aso.title\` must follow **"App Name: Primary Keyword"** format:
|
|
1340
|
+
`;
|
|
1341
|
+
prompt += ` - App name: **ALWAYS in English** (e.g., "Aurora EOS", "Timeline", "Recaply")
|
|
1342
|
+
`;
|
|
1343
|
+
prompt += ` - Primary keyword: **In target language** (e.g., "\uC624\uB85C\uB77C \uC608\uBCF4" for Korean, "\u30AA\u30FC\u30ED\u30E9\u4E88\u5831" for Japanese)
|
|
1344
|
+
`;
|
|
1345
|
+
prompt += ` - Example: "Aurora EOS: \uC624\uB85C\uB77C \uC608\uBCF4" (Korean), "Aurora EOS: \u30AA\u30FC\u30ED\u30E9\u4E88\u5831" (Japanese)
|
|
1346
|
+
`;
|
|
1347
|
+
prompt += ` - The keyword after the colon must start with an uppercase letter
|
|
1348
|
+
`;
|
|
1349
|
+
prompt += `4. Swap keywords in sentences while keeping:
|
|
1350
|
+
`;
|
|
1351
|
+
prompt += ` - Original sentence structure
|
|
1352
|
+
`;
|
|
1353
|
+
prompt += ` - Tone and messaging
|
|
1354
|
+
`;
|
|
1355
|
+
prompt += ` - Context and flow
|
|
1356
|
+
`;
|
|
1357
|
+
prompt += ` - Character limits
|
|
1358
|
+
|
|
1359
|
+
`;
|
|
1360
|
+
prompt += `4. **CRITICAL**: Update ALL \`landing\` sections:
|
|
1361
|
+
`;
|
|
1362
|
+
prompt += ` - \`landing.hero.title\` and \`landing.hero.description\`: Include keywords naturally
|
|
1363
|
+
`;
|
|
1364
|
+
prompt += ` - \`landing.screenshots.images[].title\`: Incorporate keywords in all screenshot titles
|
|
1365
|
+
`;
|
|
1366
|
+
prompt += ` - \`landing.screenshots.images[].description\`: Include keywords in all screenshot descriptions
|
|
1367
|
+
`;
|
|
1368
|
+
prompt += ` - \`landing.features.items[].title\`: Add keywords to feature titles where natural
|
|
1369
|
+
`;
|
|
1370
|
+
prompt += ` - \`landing.features.items[].body\`: Weave keywords into feature descriptions
|
|
1371
|
+
`;
|
|
1372
|
+
prompt += ` - \`landing.reviews.title\` and \`landing.reviews.description\`: Include keywords if applicable
|
|
1373
|
+
`;
|
|
1374
|
+
prompt += ` - \`landing.cta.headline\` and \`landing.cta.description\`: Include keywords if applicable
|
|
1375
|
+
`;
|
|
1376
|
+
prompt += ` - Maintain original context and meaning
|
|
1377
|
+
`;
|
|
1378
|
+
prompt += ` - Use language-specific terms that users actually search for
|
|
1379
|
+
`;
|
|
1380
|
+
prompt += ` - **DO NOT leave any landing fields in English** - all must be translated
|
|
1381
|
+
|
|
1382
|
+
`;
|
|
1383
|
+
prompt += `**Example**:
|
|
1384
|
+
`;
|
|
1385
|
+
prompt += `- Original: "Track aurora with real-time forecasts"
|
|
1386
|
+
`;
|
|
1387
|
+
prompt += `- Korean keywords: \uC624\uB85C\uB77C, \uC608\uBCF4, \uC2E4\uC2DC\uAC04
|
|
1388
|
+
`;
|
|
1389
|
+
prompt += `- Result: "\uC2E4\uC2DC\uAC04 \uC608\uBCF4\uB85C \uC624\uB85C\uB77C \uCD94\uC801"
|
|
1390
|
+
`;
|
|
1391
|
+
prompt += ` (structure changed for Korean, but context preserved)
|
|
1392
|
+
|
|
1393
|
+
`;
|
|
1394
|
+
prompt += `## Current Translated Locales (This Batch)
|
|
1395
|
+
|
|
1396
|
+
`;
|
|
1397
|
+
nonPrimaryLocales.forEach((loc) => {
|
|
1398
|
+
const section = sectionsToUse.find((s) => s.includes(`[${loc}]`));
|
|
1399
|
+
if (section) {
|
|
1400
|
+
prompt += `${section}
|
|
1401
|
+
|
|
1402
|
+
`;
|
|
1403
|
+
}
|
|
1404
|
+
});
|
|
1405
|
+
prompt += `## Workflow
|
|
1406
|
+
|
|
1407
|
+
`;
|
|
1408
|
+
prompt += `Process EACH locale in this batch sequentially:
|
|
1409
|
+
`;
|
|
1410
|
+
prompt += `1. Research 10 keywords for locale
|
|
1411
|
+
`;
|
|
1412
|
+
prompt += `2. Replace keywords in ALL fields:
|
|
1413
|
+
`;
|
|
1414
|
+
prompt += ` - \`aso.keywords\` array
|
|
1415
|
+
`;
|
|
1416
|
+
prompt += ` - \`aso.title\`, \`aso.subtitle\`, \`aso.shortDescription\`
|
|
1417
|
+
`;
|
|
1418
|
+
prompt += ` - \`aso.template.intro\`, \`aso.template.outro\`
|
|
1419
|
+
`;
|
|
1420
|
+
prompt += ` - \`landing.hero.title\` and \`landing.hero.description\`
|
|
1421
|
+
`;
|
|
1422
|
+
prompt += ` - \`landing.screenshots.images[].title\` and \`description\` (ALL items)
|
|
1423
|
+
`;
|
|
1424
|
+
prompt += ` - \`landing.features.items[].title\` and \`body\` (ALL items)
|
|
1425
|
+
`;
|
|
1426
|
+
prompt += ` - \`landing.reviews.title\` and \`landing.reviews.description\`
|
|
1427
|
+
`;
|
|
1428
|
+
prompt += ` - \`landing.cta.headline\` and \`landing.cta.description\`
|
|
1429
|
+
`;
|
|
1430
|
+
prompt += `3. **CRITICAL**: Ensure ALL landing fields are translated (not English)
|
|
1431
|
+
`;
|
|
1432
|
+
prompt += `4. Validate limits
|
|
1433
|
+
`;
|
|
1434
|
+
prompt += `5. **SAVE the updated JSON to file** using save-locale-file tool
|
|
1435
|
+
`;
|
|
1436
|
+
prompt += `6. Move to next locale in batch
|
|
1437
|
+
|
|
1438
|
+
`;
|
|
1439
|
+
if (batchIndex !== void 0 && totalBatches !== void 0) {
|
|
1440
|
+
prompt += `## After Completing This Batch
|
|
1441
|
+
|
|
1442
|
+
`;
|
|
1443
|
+
prompt += `1. Verify all locales in this batch have been saved to files
|
|
1444
|
+
`;
|
|
1445
|
+
if (batchIndex + 1 < totalBatches) {
|
|
1446
|
+
prompt += `2. Proceed to next batch (batch ${batchIndex + 2}/${totalBatches})
|
|
1447
|
+
`;
|
|
1448
|
+
prompt += `3. Use the same optimizedPrimary JSON as reference
|
|
1449
|
+
|
|
1450
|
+
`;
|
|
1451
|
+
} else {
|
|
1452
|
+
prompt += `2. All batches completed! \u2705
|
|
1453
|
+
|
|
1454
|
+
`;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
prompt += `## Output Format (Per Locale)
|
|
1458
|
+
|
|
1459
|
+
`;
|
|
1460
|
+
prompt += `For EACH locale, provide:
|
|
1461
|
+
|
|
1462
|
+
`;
|
|
1463
|
+
prompt += `### Locale [locale-code]:
|
|
1464
|
+
|
|
1465
|
+
`;
|
|
1466
|
+
prompt += `**1. Keyword Research**
|
|
1467
|
+
`;
|
|
1468
|
+
prompt += ` - Query: "[query]" \u2192 Keywords: [list]
|
|
1469
|
+
`;
|
|
1470
|
+
prompt += ` - Final 10: [list] with rationale
|
|
1471
|
+
|
|
1472
|
+
`;
|
|
1473
|
+
prompt += `**2. Updated JSON** (complete locale structure with keyword replacements)
|
|
1474
|
+
`;
|
|
1475
|
+
prompt += ` - MUST include complete \`aso\` object
|
|
1476
|
+
`;
|
|
1477
|
+
prompt += ` - MUST include complete \`landing\` object with ALL sections:
|
|
1478
|
+
`;
|
|
1479
|
+
prompt += ` * hero (title, description, titleHighlight)
|
|
1480
|
+
`;
|
|
1481
|
+
prompt += ` * screenshots.images[] (all items with translated title and description)
|
|
1482
|
+
`;
|
|
1483
|
+
prompt += ` * features.items[] (all items with translated title and body)
|
|
1484
|
+
`;
|
|
1485
|
+
prompt += ` * reviews (title, description, icons, rating, testimonials)
|
|
1486
|
+
`;
|
|
1487
|
+
prompt += ` * cta (headline, icons, rating, description)
|
|
1488
|
+
`;
|
|
1489
|
+
prompt += ` - **NO English text in landing sections** - everything must be translated
|
|
1490
|
+
|
|
1491
|
+
`;
|
|
1492
|
+
prompt += `**3. Validation**
|
|
1493
|
+
`;
|
|
1494
|
+
prompt += ` - All fields within limits \u2713/\u2717
|
|
1495
|
+
|
|
1496
|
+
`;
|
|
1497
|
+
prompt += `**4. File Save Confirmation**
|
|
1498
|
+
`;
|
|
1499
|
+
prompt += ` - Confirm file saved: public/products/${slug}/locales/[locale-code].json
|
|
1500
|
+
`;
|
|
1501
|
+
prompt += ` - **Only save if the file already exists** - do not create new files
|
|
1502
|
+
|
|
1503
|
+
`;
|
|
1504
|
+
prompt += `---
|
|
1505
|
+
|
|
1506
|
+
`;
|
|
1507
|
+
prompt += `Repeat for all locales in this batch: ${nonPrimaryLocales.join(
|
|
1508
|
+
", "
|
|
1509
|
+
)}
|
|
1510
|
+
`;
|
|
1511
|
+
return prompt;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
// src/tools/improve-public.ts
|
|
1515
|
+
var FIELD_LIMITS_DOC_PATH3 = "docs/aso/ASO_FIELD_LIMITS.md";
|
|
1516
|
+
var toJsonSchema3 = zodToJsonSchema3;
|
|
1517
|
+
var improvePublicInputSchema = z3.object({
|
|
1518
|
+
slug: z3.string().describe("Product slug"),
|
|
1519
|
+
locale: z3.string().optional().describe("Locale to improve (default: all locales)"),
|
|
1520
|
+
stage: z3.enum(["1", "2", "both"]).optional().describe(
|
|
1521
|
+
"Stage to execute: 1 (primary only), 2 (keyword localization), both (default)"
|
|
1522
|
+
),
|
|
1523
|
+
optimizedPrimary: z3.string().optional().describe("Optimized primary locale JSON (required for stage 2)"),
|
|
1524
|
+
batchSize: z3.number().int().positive().optional().default(5).describe(
|
|
1525
|
+
"Number of locales to process per batch (default: 5, for stage 2 only)"
|
|
1526
|
+
),
|
|
1527
|
+
batchIndex: z3.number().int().nonnegative().optional().describe(
|
|
1528
|
+
"Batch index to process (0-based, for stage 2 only). If not provided, processes all batches sequentially"
|
|
1529
|
+
)
|
|
1530
|
+
});
|
|
1531
|
+
var jsonSchema3 = toJsonSchema3(improvePublicInputSchema, {
|
|
1532
|
+
name: "ImprovePublicInput",
|
|
1533
|
+
target: "openApi3",
|
|
1534
|
+
$refStrategy: "none"
|
|
1535
|
+
});
|
|
1536
|
+
var inputSchema3 = jsonSchema3.definitions?.ImprovePublicInput || jsonSchema3;
|
|
1537
|
+
var improvePublicTool = {
|
|
1538
|
+
name: "improve-public",
|
|
1539
|
+
description: `Optimizes locale JSON in public/products/[slug]/locales for ASO.
|
|
1540
|
+
|
|
1541
|
+
**IMPORTANT:** The 'slug' parameter is REQUIRED. If the user does not provide a slug, you MUST ask them to provide it. This tool processes only ONE product at a time.
|
|
1542
|
+
|
|
1543
|
+
**CRITICAL: Only processes existing locale files. Does NOT create new locale files.**
|
|
1544
|
+
- Only improves locales that already exist in public/products/[slug]/locales/
|
|
1545
|
+
- If a locale file doesn't exist, it will be skipped (not created)
|
|
1546
|
+
- Always work with existing files only
|
|
1547
|
+
|
|
1548
|
+
This tool follows a 2-stage workflow:
|
|
1549
|
+
|
|
1550
|
+
**Stage 1: Primary Locale Optimization** (${FIELD_LIMITS_DOC_PATH3})
|
|
1551
|
+
1. Load product config + locales (primary: en-US default)
|
|
1552
|
+
2. Keyword research for PRIMARY locale only (web search for 10 keywords)
|
|
1553
|
+
3. Optimize ALL ASO fields in primary locale with keywords
|
|
1554
|
+
4. Validate character limits (title \u226430, subtitle \u226430, shortDescription \u226480, keywords \u2264100, intro \u2264300, outro \u2264200)
|
|
1555
|
+
|
|
1556
|
+
**Stage 2: Keyword Localization** (Batch Processing)
|
|
1557
|
+
1. Translate optimized primary \u2192 target locales in batches (preserve structure/tone/context)
|
|
1558
|
+
2. For EACH locale in batch: lightweight keyword research (10 language-specific keywords)
|
|
1559
|
+
3. Replace keywords in translated content (swap keywords only, keep context)
|
|
1560
|
+
4. Validate character limits per locale
|
|
1561
|
+
5. Save each batch to files before proceeding to next batch
|
|
1562
|
+
6. **Only processes locales that already exist - does NOT create new files**
|
|
1563
|
+
|
|
1564
|
+
**Batch Processing:**
|
|
1565
|
+
- Languages are processed in batches (default: 5 locales per batch)
|
|
1566
|
+
- Each batch is translated, optimized, and saved before moving to the next
|
|
1567
|
+
- Use \`batchIndex\` to process a specific batch (0-based)
|
|
1568
|
+
- If \`batchIndex\` is not provided, process all batches sequentially
|
|
1569
|
+
- **Only existing locale files are processed - missing locales are skipped**
|
|
1570
|
+
|
|
1571
|
+
This approach ensures:
|
|
1572
|
+
- Efficient token usage (full optimization only once)
|
|
1573
|
+
- Consistent messaging across all languages
|
|
1574
|
+
- Language-specific keyword optimization for each market
|
|
1575
|
+
- Prevents content truncation by processing in manageable batches
|
|
1576
|
+
- **No new files are created - only existing locales are improved**
|
|
1577
|
+
|
|
1578
|
+
Optionally target a single locale; the primary locale is always included for reference.`,
|
|
1579
|
+
inputSchema: inputSchema3
|
|
1580
|
+
};
|
|
1581
|
+
async function handleImprovePublic(input) {
|
|
1582
|
+
const {
|
|
1583
|
+
slug,
|
|
1584
|
+
locale,
|
|
1585
|
+
stage = "both",
|
|
1586
|
+
optimizedPrimary,
|
|
1587
|
+
batchSize = 5,
|
|
1588
|
+
batchIndex
|
|
1589
|
+
} = input;
|
|
1590
|
+
const { config, locales } = loadProductLocales(slug);
|
|
1591
|
+
const primaryLocale = resolvePrimaryLocale(config, locales);
|
|
1592
|
+
if (locale && !locales[locale]) {
|
|
1593
|
+
throw new Error(
|
|
1594
|
+
`Locale "${locale}" not found in public/products/${slug}/locales/. Only existing locale files are processed - new files are not created.`
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1597
|
+
const requestedLocales = locale ? [locale] : Object.keys(locales);
|
|
1598
|
+
const existingRequestedLocales = requestedLocales.filter(
|
|
1599
|
+
(loc) => locales[loc]
|
|
1600
|
+
);
|
|
1601
|
+
if (existingRequestedLocales.length === 0) {
|
|
1602
|
+
throw new Error(
|
|
1603
|
+
`No existing locales found to process. Only existing locale files in public/products/${slug}/locales/ are processed.`
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1606
|
+
const localeSet = /* @__PURE__ */ new Set([
|
|
1607
|
+
...existingRequestedLocales,
|
|
1608
|
+
primaryLocale
|
|
1609
|
+
]);
|
|
1610
|
+
const targetLocales = [...localeSet].filter((loc) => locales[loc]);
|
|
1611
|
+
const asoData = loadAsoFromConfig(slug);
|
|
1612
|
+
const category = config?.metadata?.category;
|
|
1613
|
+
const localeSections = [];
|
|
1614
|
+
for (const loc of localeSet) {
|
|
1615
|
+
const localeData = locales[loc];
|
|
1616
|
+
if (!localeData) {
|
|
1617
|
+
continue;
|
|
1618
|
+
}
|
|
1619
|
+
const fullDescription = getFullDescriptionForLocale(asoData, loc);
|
|
1620
|
+
localeSections.push(
|
|
1621
|
+
formatLocaleSection({
|
|
1622
|
+
slug,
|
|
1623
|
+
locale: loc,
|
|
1624
|
+
localeData,
|
|
1625
|
+
fullDescription,
|
|
1626
|
+
primaryLocale,
|
|
1627
|
+
category
|
|
1628
|
+
})
|
|
1629
|
+
);
|
|
1630
|
+
}
|
|
1631
|
+
const baseArgs = {
|
|
1632
|
+
slug,
|
|
1633
|
+
category,
|
|
1634
|
+
primaryLocale,
|
|
1635
|
+
targetLocales,
|
|
1636
|
+
localeSections
|
|
1637
|
+
};
|
|
1638
|
+
if (stage === "1" || stage === "both") {
|
|
1639
|
+
const prompt = generatePrimaryOptimizationPrompt(baseArgs);
|
|
1640
|
+
return {
|
|
1641
|
+
content: [
|
|
1642
|
+
{
|
|
1643
|
+
type: "text",
|
|
1644
|
+
text: prompt
|
|
1645
|
+
}
|
|
1646
|
+
]
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
if (stage === "2") {
|
|
1650
|
+
if (!optimizedPrimary) {
|
|
1651
|
+
throw new Error(
|
|
1652
|
+
"Stage 2 requires optimizedPrimary parameter. Run stage 1 first or use stage='both'."
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
const nonPrimaryLocales = targetLocales.filter((l) => l !== primaryLocale);
|
|
1656
|
+
const totalBatches = Math.ceil(nonPrimaryLocales.length / batchSize);
|
|
1657
|
+
let batchesToProcess;
|
|
1658
|
+
if (batchIndex !== void 0) {
|
|
1659
|
+
if (batchIndex < 0 || batchIndex >= totalBatches) {
|
|
1660
|
+
throw new Error(
|
|
1661
|
+
`Batch index ${batchIndex} is out of range. Total batches: ${totalBatches} (0-${totalBatches - 1})`
|
|
1662
|
+
);
|
|
1663
|
+
}
|
|
1664
|
+
batchesToProcess = [batchIndex];
|
|
1665
|
+
} else {
|
|
1666
|
+
batchesToProcess = Array.from({ length: totalBatches }, (_, i) => i);
|
|
1667
|
+
}
|
|
1668
|
+
const batchPrompts = [];
|
|
1669
|
+
for (const currentBatchIndex of batchesToProcess) {
|
|
1670
|
+
const startIdx = currentBatchIndex * batchSize;
|
|
1671
|
+
const endIdx = Math.min(startIdx + batchSize, nonPrimaryLocales.length);
|
|
1672
|
+
const batchLocales = nonPrimaryLocales.slice(startIdx, endIdx);
|
|
1673
|
+
const batchLocaleSections = localeSections.filter((section) => {
|
|
1674
|
+
return batchLocales.some((loc) => section.includes(`[${loc}]`));
|
|
1675
|
+
});
|
|
1676
|
+
const promptArgs = {
|
|
1677
|
+
slug: baseArgs.slug,
|
|
1678
|
+
category: baseArgs.category,
|
|
1679
|
+
primaryLocale: baseArgs.primaryLocale,
|
|
1680
|
+
targetLocales: baseArgs.targetLocales,
|
|
1681
|
+
localeSections: baseArgs.localeSections,
|
|
1682
|
+
optimizedPrimary,
|
|
1683
|
+
batchLocales,
|
|
1684
|
+
batchIndex: currentBatchIndex,
|
|
1685
|
+
totalBatches,
|
|
1686
|
+
batchLocaleSections
|
|
1687
|
+
};
|
|
1688
|
+
const prompt = generateKeywordLocalizationPrompt(promptArgs);
|
|
1689
|
+
batchPrompts.push(prompt);
|
|
1690
|
+
}
|
|
1691
|
+
return {
|
|
1692
|
+
content: [
|
|
1693
|
+
{
|
|
1694
|
+
type: "text",
|
|
1695
|
+
text: batchPrompts.join("\n\n---\n\n")
|
|
1696
|
+
}
|
|
1697
|
+
]
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
throw new Error(`Invalid stage: ${stage}. Must be "1", "2", or "both".`);
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
// src/tools/init-project.ts
|
|
1704
|
+
import fs5 from "fs";
|
|
1705
|
+
import path6 from "path";
|
|
1706
|
+
import { z as z4 } from "zod";
|
|
1707
|
+
import { zodToJsonSchema as zodToJsonSchema4 } from "zod-to-json-schema";
|
|
1708
|
+
var listSlugDirs = (dir) => {
|
|
1709
|
+
if (!fs5.existsSync(dir)) return [];
|
|
1710
|
+
return fs5.readdirSync(dir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
|
|
1711
|
+
};
|
|
1712
|
+
var initProjectInputSchema = z4.object({
|
|
1713
|
+
slug: z4.string().trim().optional().describe(
|
|
1714
|
+
"Optional product slug to focus on. Defaults to all slugs in .aso/pullData/products/"
|
|
1715
|
+
)
|
|
1716
|
+
});
|
|
1717
|
+
var jsonSchema4 = zodToJsonSchema4(initProjectInputSchema, {
|
|
1718
|
+
name: "InitProjectInput",
|
|
1719
|
+
target: "openApi3",
|
|
1720
|
+
$refStrategy: "none"
|
|
1721
|
+
});
|
|
1722
|
+
var inputSchema4 = jsonSchema4.definitions?.InitProjectInput || jsonSchema4;
|
|
1723
|
+
var initProjectTool = {
|
|
1724
|
+
name: "init-project",
|
|
1725
|
+
description: `Guides the initialization flow: run pabal-mcp Init, then convert ASO pullData into public/products/[slug]/.
|
|
1726
|
+
|
|
1727
|
+
This tool is read-only and returns a checklist. It does not call pabal-mcp directly or write files.
|
|
1728
|
+
|
|
1729
|
+
Steps:
|
|
1730
|
+
1) Ensure pabal-mcp 'init' ran and .aso/pullData/products/[slug]/ exists
|
|
1731
|
+
2) Convert pulled ASO data -> public/products/[slug]/ using pabal-web-mcp tools (aso-to-public, public-to-aso dry run)
|
|
1732
|
+
3) Validate outputs and next actions`,
|
|
1733
|
+
inputSchema: inputSchema4
|
|
1734
|
+
};
|
|
1735
|
+
async function handleInitProject(input) {
|
|
1736
|
+
const pullDataDir = path6.join(process.cwd(), ".aso", "pullData", "products");
|
|
1737
|
+
const publicDir = path6.join(process.cwd(), "public", "products");
|
|
1738
|
+
const pullDataSlugs = listSlugDirs(pullDataDir);
|
|
1739
|
+
const publicSlugs = listSlugDirs(publicDir);
|
|
1740
|
+
const targetSlugs = input.slug?.length && input.slug.trim().length > 0 ? [input.slug.trim()] : pullDataSlugs.length > 0 ? pullDataSlugs : publicSlugs;
|
|
1741
|
+
const lines = [];
|
|
1742
|
+
lines.push("Init workflow (pabal-mcp -> pabal-web-mcp)");
|
|
1743
|
+
lines.push(
|
|
1744
|
+
`Target slugs: ${targetSlugs.length > 0 ? targetSlugs.join(", ") : "(none detected)"}`
|
|
1745
|
+
);
|
|
1746
|
+
lines.push(
|
|
1747
|
+
`pullData: ${pullDataSlugs.length > 0 ? "found" : "missing"} at ${pullDataDir}`
|
|
1748
|
+
);
|
|
1749
|
+
lines.push(
|
|
1750
|
+
`public/products: ${publicSlugs.length > 0 ? "found" : "missing"} at ${publicDir}`
|
|
1751
|
+
);
|
|
1752
|
+
lines.push("");
|
|
1753
|
+
if (targetSlugs.length === 0) {
|
|
1754
|
+
lines.push(
|
|
1755
|
+
"No products detected. Run pabal-mcp 'init' for your slug(s) to populate .aso/pullData/products/, then rerun this tool."
|
|
1756
|
+
);
|
|
1757
|
+
return {
|
|
1758
|
+
content: [
|
|
1759
|
+
{
|
|
1760
|
+
type: "text",
|
|
1761
|
+
text: lines.join("\n")
|
|
1762
|
+
}
|
|
1763
|
+
]
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
lines.push("Step 1: Fetch raw ASO data (pabal-mcp 'init')");
|
|
1767
|
+
for (const slug of targetSlugs) {
|
|
1768
|
+
const hasPull = pullDataSlugs.includes(slug);
|
|
1769
|
+
lines.push(`- ${slug}: ${hasPull ? "pullData ready" : "pullData missing"}`);
|
|
1770
|
+
}
|
|
1771
|
+
lines.push(
|
|
1772
|
+
"Action: In pabal-mcp, run the 'init' tool for each slug above that is missing pullData."
|
|
1773
|
+
);
|
|
1774
|
+
lines.push("");
|
|
1775
|
+
lines.push(
|
|
1776
|
+
"Step 2: Convert pullData to web assets (pabal-web-mcp 'aso-to-public')"
|
|
1777
|
+
);
|
|
1778
|
+
for (const slug of targetSlugs) {
|
|
1779
|
+
const hasPull = pullDataSlugs.includes(slug);
|
|
1780
|
+
const hasPublic = publicSlugs.includes(slug);
|
|
1781
|
+
const pullStatus = hasPull ? "pullData ready" : "pullData missing";
|
|
1782
|
+
const publicStatus = hasPublic ? "public/products ready" : "public/products missing";
|
|
1783
|
+
lines.push(
|
|
1784
|
+
`- ${slug}: ${pullStatus}, ${publicStatus}${hasPull && !hasPublic ? " (ready to convert)" : ""}`
|
|
1785
|
+
);
|
|
1786
|
+
}
|
|
1787
|
+
lines.push(
|
|
1788
|
+
"Action: After pullData exists, run pabal-web-mcp 'aso-to-public' per slug to generate locale prompts. Save results to public/products/[slug]/locales/, copy screenshots, and ensure config.json/icon/og-image are in place."
|
|
1789
|
+
);
|
|
1790
|
+
lines.push("");
|
|
1791
|
+
lines.push("Step 3: Verify and prepare for push (optional)");
|
|
1792
|
+
lines.push(
|
|
1793
|
+
"Use pabal-web-mcp 'public-to-aso' with dryRun=true to validate structure and build .aso/pushData before uploading via store tooling."
|
|
1794
|
+
);
|
|
1795
|
+
lines.push("");
|
|
1796
|
+
lines.push("Notes:");
|
|
1797
|
+
lines.push(
|
|
1798
|
+
"- This tool is read-only; it does not write files or call pabal-mcp."
|
|
1799
|
+
);
|
|
1800
|
+
lines.push(
|
|
1801
|
+
"- Extend this init checklist as new processes are added (e.g., asset generation or validations)."
|
|
1802
|
+
);
|
|
1803
|
+
return {
|
|
1804
|
+
content: [
|
|
1805
|
+
{
|
|
1806
|
+
type: "text",
|
|
1807
|
+
text: lines.join("\n")
|
|
1808
|
+
}
|
|
1809
|
+
]
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
// src/tools/index.ts
|
|
1814
|
+
var tools = [
|
|
1815
|
+
{
|
|
1816
|
+
name: asoToPublicTool.name,
|
|
1817
|
+
description: asoToPublicTool.description,
|
|
1818
|
+
inputSchema: asoToPublicTool.inputSchema,
|
|
1819
|
+
zodSchema: asoToPublicInputSchema,
|
|
1820
|
+
handler: handleAsoToPublic,
|
|
1821
|
+
category: "ASO Data Conversion"
|
|
1822
|
+
},
|
|
1823
|
+
{
|
|
1824
|
+
name: publicToAsoTool.name,
|
|
1825
|
+
description: publicToAsoTool.description,
|
|
1826
|
+
inputSchema: publicToAsoTool.inputSchema,
|
|
1827
|
+
zodSchema: publicToAsoInputSchema,
|
|
1828
|
+
handler: handlePublicToAso,
|
|
1829
|
+
category: "ASO Data Conversion"
|
|
1830
|
+
},
|
|
1831
|
+
{
|
|
1832
|
+
name: improvePublicTool.name,
|
|
1833
|
+
description: improvePublicTool.description,
|
|
1834
|
+
inputSchema: improvePublicTool.inputSchema,
|
|
1835
|
+
zodSchema: improvePublicInputSchema,
|
|
1836
|
+
handler: handleImprovePublic,
|
|
1837
|
+
category: "ASO Optimization"
|
|
1838
|
+
},
|
|
1839
|
+
{
|
|
1840
|
+
name: initProjectTool.name,
|
|
1841
|
+
description: initProjectTool.description,
|
|
1842
|
+
inputSchema: initProjectTool.inputSchema,
|
|
1843
|
+
zodSchema: initProjectInputSchema,
|
|
1844
|
+
handler: handleInitProject,
|
|
1845
|
+
category: "Setup"
|
|
1846
|
+
}
|
|
1847
|
+
];
|
|
1848
|
+
function getToolDefinitions() {
|
|
1849
|
+
return [
|
|
1850
|
+
asoToPublicTool,
|
|
1851
|
+
publicToAsoTool,
|
|
1852
|
+
improvePublicTool,
|
|
1853
|
+
initProjectTool
|
|
1854
|
+
];
|
|
1855
|
+
}
|
|
1856
|
+
function getToolHandler(name) {
|
|
1857
|
+
const tool = tools.find((t) => t.name === name);
|
|
1858
|
+
if (!tool) {
|
|
1859
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
1860
|
+
}
|
|
1861
|
+
return tool.handler;
|
|
1862
|
+
}
|
|
1863
|
+
function getToolZodSchema(name) {
|
|
1864
|
+
const tool = tools.find((t) => t.name === name);
|
|
1865
|
+
if (!tool) {
|
|
1866
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
1867
|
+
}
|
|
1868
|
+
return tool.zodSchema;
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
// src/bin/mcp-server.ts
|
|
1872
|
+
var server = new Server(
|
|
1873
|
+
{
|
|
1874
|
+
name: "pabal-web-mcp",
|
|
1875
|
+
version: "0.1.0"
|
|
1876
|
+
},
|
|
1877
|
+
{
|
|
1878
|
+
capabilities: {
|
|
1879
|
+
tools: {}
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
);
|
|
1883
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1884
|
+
return {
|
|
1885
|
+
tools: getToolDefinitions()
|
|
1886
|
+
};
|
|
1887
|
+
});
|
|
1888
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1889
|
+
const { name, arguments: args } = request.params;
|
|
1890
|
+
const zodSchema = getToolZodSchema(name);
|
|
1891
|
+
const handler = getToolHandler(name);
|
|
1892
|
+
if (!zodSchema) {
|
|
1893
|
+
throw new Error(`No schema found for tool: ${name}`);
|
|
1894
|
+
}
|
|
1895
|
+
const input = zodSchema.parse(args);
|
|
1896
|
+
return await handler(input);
|
|
1897
|
+
});
|
|
1898
|
+
async function main() {
|
|
1899
|
+
const transport = new StdioServerTransport();
|
|
1900
|
+
await server.connect(transport);
|
|
1901
|
+
console.error("pabal-web-mcp server running on stdio");
|
|
1902
|
+
}
|
|
1903
|
+
main().catch((error) => {
|
|
1904
|
+
console.error("Fatal error in main():", error);
|
|
1905
|
+
process.exit(1);
|
|
1906
|
+
});
|