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.
@@ -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
+ });