pabal-resource-mcp 1.5.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/mcp-server.js +658 -47
- package/dist/chunk-YKUBCCJA.js +386 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +3 -1
- package/package.json +4 -1
package/dist/bin/mcp-server.js
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
DEFAULT_APP_SLUG
|
|
4
4
|
} from "../chunk-DLCIXAUB.js";
|
|
5
5
|
import {
|
|
6
|
+
getGeminiApiKey,
|
|
6
7
|
getKeywordResearchDir,
|
|
7
8
|
getProductsDir,
|
|
8
9
|
getPublicDir,
|
|
@@ -10,7 +11,7 @@ import {
|
|
|
10
11
|
getPushDataDir,
|
|
11
12
|
loadAsoFromConfig,
|
|
12
13
|
saveAsoToAsoDir
|
|
13
|
-
} from "../chunk-
|
|
14
|
+
} from "../chunk-YKUBCCJA.js";
|
|
14
15
|
import {
|
|
15
16
|
DEFAULT_LOCALE,
|
|
16
17
|
appStoreToUnified,
|
|
@@ -3384,25 +3385,626 @@ Context around ${pos}: ${context}`
|
|
|
3384
3385
|
};
|
|
3385
3386
|
}
|
|
3386
3387
|
|
|
3387
|
-
// src/tools/
|
|
3388
|
-
import fs9 from "fs";
|
|
3389
|
-
import path9 from "path";
|
|
3388
|
+
// src/tools/aso/localize-screenshots.ts
|
|
3390
3389
|
import { z as z7 } from "zod";
|
|
3391
3390
|
import { zodToJsonSchema as zodToJsonSchema7 } from "zod-to-json-schema";
|
|
3391
|
+
import fs12 from "fs";
|
|
3392
|
+
import path11 from "path";
|
|
3393
|
+
|
|
3394
|
+
// src/tools/aso/utils/localize-screenshots/scan-screenshots.util.ts
|
|
3395
|
+
import fs9 from "fs";
|
|
3396
|
+
import path9 from "path";
|
|
3397
|
+
function getScreenshotsDir(slug) {
|
|
3398
|
+
const productsDir = getProductsDir();
|
|
3399
|
+
return path9.join(productsDir, slug, "screenshots");
|
|
3400
|
+
}
|
|
3401
|
+
function scanLocaleScreenshots(slug, locale) {
|
|
3402
|
+
const screenshotsDir = getScreenshotsDir(slug);
|
|
3403
|
+
const localeDir = path9.join(screenshotsDir, locale);
|
|
3404
|
+
if (!fs9.existsSync(localeDir)) {
|
|
3405
|
+
return [];
|
|
3406
|
+
}
|
|
3407
|
+
const screenshots = [];
|
|
3408
|
+
const deviceTypes = ["phone", "tablet"];
|
|
3409
|
+
for (const deviceType of deviceTypes) {
|
|
3410
|
+
const deviceDir = path9.join(localeDir, deviceType);
|
|
3411
|
+
if (!fs9.existsSync(deviceDir)) {
|
|
3412
|
+
continue;
|
|
3413
|
+
}
|
|
3414
|
+
const files = fs9.readdirSync(deviceDir).filter((file) => /\.(png|jpg|jpeg|webp)$/i.test(file)).sort((a, b) => {
|
|
3415
|
+
const numA = parseInt(a.match(/\d+/)?.[0] || "0");
|
|
3416
|
+
const numB = parseInt(b.match(/\d+/)?.[0] || "0");
|
|
3417
|
+
return numA - numB;
|
|
3418
|
+
});
|
|
3419
|
+
for (const filename of files) {
|
|
3420
|
+
screenshots.push({
|
|
3421
|
+
type: deviceType,
|
|
3422
|
+
filename,
|
|
3423
|
+
fullPath: path9.join(deviceDir, filename)
|
|
3424
|
+
});
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
return screenshots;
|
|
3428
|
+
}
|
|
3429
|
+
|
|
3430
|
+
// src/tools/aso/utils/localize-screenshots/gemini-image-translator.util.ts
|
|
3431
|
+
import { GoogleGenAI, Modality } from "@google/genai";
|
|
3432
|
+
import fs10 from "fs";
|
|
3433
|
+
import path10 from "path";
|
|
3434
|
+
var LANGUAGE_NAMES = {
|
|
3435
|
+
"en-US": "English (US)",
|
|
3436
|
+
"en-GB": "English (UK)",
|
|
3437
|
+
"en-AU": "English (Australia)",
|
|
3438
|
+
"en-CA": "English (Canada)",
|
|
3439
|
+
"ko-KR": "Korean",
|
|
3440
|
+
"ja-JP": "Japanese",
|
|
3441
|
+
"zh-Hans": "Simplified Chinese",
|
|
3442
|
+
"zh-Hant": "Traditional Chinese",
|
|
3443
|
+
"zh-CN": "Simplified Chinese",
|
|
3444
|
+
"zh-TW": "Traditional Chinese",
|
|
3445
|
+
"fr-FR": "French",
|
|
3446
|
+
"fr-CA": "French (Canada)",
|
|
3447
|
+
"de-DE": "German",
|
|
3448
|
+
"es-ES": "Spanish (Spain)",
|
|
3449
|
+
"es-419": "Spanish (Latin America)",
|
|
3450
|
+
"es-MX": "Spanish (Mexico)",
|
|
3451
|
+
"pt-BR": "Portuguese (Brazil)",
|
|
3452
|
+
"pt-PT": "Portuguese (Portugal)",
|
|
3453
|
+
"it-IT": "Italian",
|
|
3454
|
+
"nl-NL": "Dutch",
|
|
3455
|
+
"ru-RU": "Russian",
|
|
3456
|
+
"ar": "Arabic",
|
|
3457
|
+
"ar-SA": "Arabic",
|
|
3458
|
+
"hi-IN": "Hindi",
|
|
3459
|
+
"th-TH": "Thai",
|
|
3460
|
+
"vi-VN": "Vietnamese",
|
|
3461
|
+
"id-ID": "Indonesian",
|
|
3462
|
+
"ms-MY": "Malay",
|
|
3463
|
+
"tr-TR": "Turkish",
|
|
3464
|
+
"pl-PL": "Polish",
|
|
3465
|
+
"uk-UA": "Ukrainian",
|
|
3466
|
+
"cs-CZ": "Czech",
|
|
3467
|
+
"el-GR": "Greek",
|
|
3468
|
+
"ro-RO": "Romanian",
|
|
3469
|
+
"hu-HU": "Hungarian",
|
|
3470
|
+
"sv-SE": "Swedish",
|
|
3471
|
+
"da-DK": "Danish",
|
|
3472
|
+
"fi-FI": "Finnish",
|
|
3473
|
+
"no-NO": "Norwegian",
|
|
3474
|
+
"he-IL": "Hebrew",
|
|
3475
|
+
"sk-SK": "Slovak",
|
|
3476
|
+
"bg-BG": "Bulgarian",
|
|
3477
|
+
"hr-HR": "Croatian",
|
|
3478
|
+
"ca-ES": "Catalan"
|
|
3479
|
+
};
|
|
3480
|
+
function getLanguageName(locale) {
|
|
3481
|
+
if (LANGUAGE_NAMES[locale]) {
|
|
3482
|
+
return LANGUAGE_NAMES[locale];
|
|
3483
|
+
}
|
|
3484
|
+
const baseCode = locale.split("-")[0];
|
|
3485
|
+
const matchingKey = Object.keys(LANGUAGE_NAMES).find(
|
|
3486
|
+
(key) => key.startsWith(baseCode + "-") || key === baseCode
|
|
3487
|
+
);
|
|
3488
|
+
if (matchingKey) {
|
|
3489
|
+
return LANGUAGE_NAMES[matchingKey];
|
|
3490
|
+
}
|
|
3491
|
+
return locale;
|
|
3492
|
+
}
|
|
3493
|
+
function getGeminiClient() {
|
|
3494
|
+
const apiKey = getGeminiApiKey();
|
|
3495
|
+
return new GoogleGenAI({ apiKey });
|
|
3496
|
+
}
|
|
3497
|
+
function readImageAsBase64(imagePath) {
|
|
3498
|
+
const buffer = fs10.readFileSync(imagePath);
|
|
3499
|
+
const base64 = buffer.toString("base64");
|
|
3500
|
+
const ext = path10.extname(imagePath).toLowerCase();
|
|
3501
|
+
let mimeType = "image/png";
|
|
3502
|
+
if (ext === ".jpg" || ext === ".jpeg") {
|
|
3503
|
+
mimeType = "image/jpeg";
|
|
3504
|
+
} else if (ext === ".webp") {
|
|
3505
|
+
mimeType = "image/webp";
|
|
3506
|
+
}
|
|
3507
|
+
return { data: base64, mimeType };
|
|
3508
|
+
}
|
|
3509
|
+
async function translateImage(sourcePath, sourceLocale, targetLocale, outputPath) {
|
|
3510
|
+
try {
|
|
3511
|
+
const client = getGeminiClient();
|
|
3512
|
+
const sourceLanguage = getLanguageName(sourceLocale);
|
|
3513
|
+
const targetLanguage = getLanguageName(targetLocale);
|
|
3514
|
+
const { data: imageData, mimeType } = readImageAsBase64(sourcePath);
|
|
3515
|
+
const prompt = `This is an app screenshot with text in ${sourceLanguage}.
|
|
3516
|
+
Please translate ONLY the text/words in this image to ${targetLanguage}.
|
|
3517
|
+
|
|
3518
|
+
IMPORTANT INSTRUCTIONS:
|
|
3519
|
+
- Keep the EXACT same layout, design, colors, and visual elements
|
|
3520
|
+
- Only translate the visible text content to ${targetLanguage}
|
|
3521
|
+
- Maintain the same font style and text positioning as much as possible
|
|
3522
|
+
- Do NOT add any new elements or remove existing design elements
|
|
3523
|
+
- The output should look identical except the text language is ${targetLanguage}
|
|
3524
|
+
- Preserve all icons, images, and graphical elements exactly as they are`;
|
|
3525
|
+
const response = await client.models.generateContent({
|
|
3526
|
+
model: "imagen-3.0-generate-002",
|
|
3527
|
+
contents: [
|
|
3528
|
+
{
|
|
3529
|
+
role: "user",
|
|
3530
|
+
parts: [
|
|
3531
|
+
{ text: prompt },
|
|
3532
|
+
{
|
|
3533
|
+
inlineData: {
|
|
3534
|
+
mimeType,
|
|
3535
|
+
data: imageData
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
]
|
|
3539
|
+
}
|
|
3540
|
+
],
|
|
3541
|
+
config: {
|
|
3542
|
+
responseModalities: [Modality.TEXT, Modality.IMAGE]
|
|
3543
|
+
}
|
|
3544
|
+
});
|
|
3545
|
+
const candidates = response.candidates;
|
|
3546
|
+
if (!candidates || candidates.length === 0) {
|
|
3547
|
+
return {
|
|
3548
|
+
success: false,
|
|
3549
|
+
error: "No response from Gemini API"
|
|
3550
|
+
};
|
|
3551
|
+
}
|
|
3552
|
+
const parts = candidates[0].content?.parts;
|
|
3553
|
+
if (!parts) {
|
|
3554
|
+
return {
|
|
3555
|
+
success: false,
|
|
3556
|
+
error: "No content parts in response"
|
|
3557
|
+
};
|
|
3558
|
+
}
|
|
3559
|
+
for (const part of parts) {
|
|
3560
|
+
if (part.inlineData?.data) {
|
|
3561
|
+
const imageBuffer = Buffer.from(part.inlineData.data, "base64");
|
|
3562
|
+
const outputDir = path10.dirname(outputPath);
|
|
3563
|
+
if (!fs10.existsSync(outputDir)) {
|
|
3564
|
+
fs10.mkdirSync(outputDir, { recursive: true });
|
|
3565
|
+
}
|
|
3566
|
+
fs10.writeFileSync(outputPath, imageBuffer);
|
|
3567
|
+
return {
|
|
3568
|
+
success: true,
|
|
3569
|
+
outputPath
|
|
3570
|
+
};
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
return {
|
|
3574
|
+
success: false,
|
|
3575
|
+
error: "No image data in Gemini response"
|
|
3576
|
+
};
|
|
3577
|
+
} catch (error) {
|
|
3578
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3579
|
+
return {
|
|
3580
|
+
success: false,
|
|
3581
|
+
error: message
|
|
3582
|
+
};
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
async function translateImagesWithProgress(translations, onProgress) {
|
|
3586
|
+
let successful = 0;
|
|
3587
|
+
let failed = 0;
|
|
3588
|
+
const errors = [];
|
|
3589
|
+
for (const translation of translations) {
|
|
3590
|
+
const progress = {
|
|
3591
|
+
sourceLocale: translation.sourceLocale,
|
|
3592
|
+
targetLocale: translation.targetLocale,
|
|
3593
|
+
deviceType: translation.deviceType,
|
|
3594
|
+
filename: translation.filename,
|
|
3595
|
+
status: "translating"
|
|
3596
|
+
};
|
|
3597
|
+
onProgress?.(progress);
|
|
3598
|
+
const result = await translateImage(
|
|
3599
|
+
translation.sourcePath,
|
|
3600
|
+
translation.sourceLocale,
|
|
3601
|
+
translation.targetLocale,
|
|
3602
|
+
translation.outputPath
|
|
3603
|
+
);
|
|
3604
|
+
if (result.success) {
|
|
3605
|
+
successful++;
|
|
3606
|
+
progress.status = "completed";
|
|
3607
|
+
} else {
|
|
3608
|
+
failed++;
|
|
3609
|
+
progress.status = "failed";
|
|
3610
|
+
progress.error = result.error;
|
|
3611
|
+
errors.push({ path: translation.sourcePath, error: result.error || "Unknown error" });
|
|
3612
|
+
}
|
|
3613
|
+
onProgress?.(progress);
|
|
3614
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
3615
|
+
}
|
|
3616
|
+
return { successful, failed, errors };
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3619
|
+
// src/tools/aso/utils/localize-screenshots/image-resizer.util.ts
|
|
3620
|
+
import sharp from "sharp";
|
|
3621
|
+
import fs11 from "fs";
|
|
3622
|
+
async function getImageDimensions(imagePath) {
|
|
3623
|
+
const metadata = await sharp(imagePath).metadata();
|
|
3624
|
+
if (!metadata.width || !metadata.height) {
|
|
3625
|
+
throw new Error(`Unable to read dimensions from ${imagePath}`);
|
|
3626
|
+
}
|
|
3627
|
+
return {
|
|
3628
|
+
width: metadata.width,
|
|
3629
|
+
height: metadata.height
|
|
3630
|
+
};
|
|
3631
|
+
}
|
|
3632
|
+
async function resizeImage(inputPath, outputPath, targetDimensions) {
|
|
3633
|
+
await sharp(inputPath).resize(targetDimensions.width, targetDimensions.height, {
|
|
3634
|
+
fit: "fill",
|
|
3635
|
+
// Exact resize to target dimensions
|
|
3636
|
+
withoutEnlargement: false
|
|
3637
|
+
// Allow enlargement if needed
|
|
3638
|
+
}).toFile(outputPath + ".tmp");
|
|
3639
|
+
fs11.renameSync(outputPath + ".tmp", outputPath);
|
|
3640
|
+
}
|
|
3641
|
+
async function validateAndResizeImage(sourcePath, translatedPath) {
|
|
3642
|
+
const sourceDimensions = await getImageDimensions(sourcePath);
|
|
3643
|
+
const translatedDimensions = await getImageDimensions(translatedPath);
|
|
3644
|
+
const needsResize = sourceDimensions.width !== translatedDimensions.width || sourceDimensions.height !== translatedDimensions.height;
|
|
3645
|
+
if (needsResize) {
|
|
3646
|
+
await resizeImage(translatedPath, translatedPath, sourceDimensions);
|
|
3647
|
+
}
|
|
3648
|
+
return {
|
|
3649
|
+
resized: needsResize,
|
|
3650
|
+
sourceDimensions,
|
|
3651
|
+
translatedDimensions,
|
|
3652
|
+
finalDimensions: sourceDimensions
|
|
3653
|
+
};
|
|
3654
|
+
}
|
|
3655
|
+
async function batchValidateAndResize(pairs) {
|
|
3656
|
+
let resizedCount = 0;
|
|
3657
|
+
const errors = [];
|
|
3658
|
+
for (const { sourcePath, translatedPath } of pairs) {
|
|
3659
|
+
try {
|
|
3660
|
+
if (!fs11.existsSync(translatedPath)) {
|
|
3661
|
+
continue;
|
|
3662
|
+
}
|
|
3663
|
+
const result = await validateAndResizeImage(sourcePath, translatedPath);
|
|
3664
|
+
if (result.resized) {
|
|
3665
|
+
resizedCount++;
|
|
3666
|
+
}
|
|
3667
|
+
} catch (error) {
|
|
3668
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3669
|
+
errors.push({ path: translatedPath, error: message });
|
|
3670
|
+
}
|
|
3671
|
+
}
|
|
3672
|
+
return {
|
|
3673
|
+
total: pairs.length,
|
|
3674
|
+
resized: resizedCount,
|
|
3675
|
+
errors
|
|
3676
|
+
};
|
|
3677
|
+
}
|
|
3678
|
+
|
|
3679
|
+
// src/tools/aso/localize-screenshots.ts
|
|
3680
|
+
var TOOL_NAME3 = "localize-screenshots";
|
|
3681
|
+
var localizeScreenshotsInputSchema = z7.object({
|
|
3682
|
+
appName: z7.string().describe(
|
|
3683
|
+
"App name, slug, bundleId, or packageName to search for. Will be validated using search-app."
|
|
3684
|
+
),
|
|
3685
|
+
targetLocales: z7.array(z7.string()).optional().describe(
|
|
3686
|
+
"Specific target locales to translate to. If not provided, all supported locales from the product will be used."
|
|
3687
|
+
),
|
|
3688
|
+
deviceTypes: z7.array(z7.enum(["phone", "tablet"])).optional().default(["phone", "tablet"]).describe("Device types to process (default: both phone and tablet)"),
|
|
3689
|
+
dryRun: z7.boolean().optional().default(false).describe("Preview mode - shows what would be translated without actually translating"),
|
|
3690
|
+
skipExisting: z7.boolean().optional().default(true).describe("Skip translation if target file already exists (default: true)")
|
|
3691
|
+
});
|
|
3692
|
+
var jsonSchema7 = zodToJsonSchema7(localizeScreenshotsInputSchema, {
|
|
3693
|
+
name: "LocalizeScreenshotsInput",
|
|
3694
|
+
$refStrategy: "none"
|
|
3695
|
+
});
|
|
3696
|
+
var inputSchema7 = jsonSchema7.definitions?.LocalizeScreenshotsInput || jsonSchema7;
|
|
3697
|
+
var localizeScreenshotsTool = {
|
|
3698
|
+
name: TOOL_NAME3,
|
|
3699
|
+
description: `Translate app screenshots to multiple languages using Gemini API.
|
|
3700
|
+
|
|
3701
|
+
**IMPORTANT:** This tool uses the search-app tool internally to validate the app. You can provide an approximate name, bundleId, or packageName.
|
|
3702
|
+
|
|
3703
|
+
This tool:
|
|
3704
|
+
1. Validates the app exists in registered-apps.json
|
|
3705
|
+
2. Reads supported locales from public/products/{slug}/locales/ directory
|
|
3706
|
+
3. Scans screenshots from the primary locale's screenshots folder
|
|
3707
|
+
4. Uses Gemini API (imagen-3.0-generate-002) to translate text in images
|
|
3708
|
+
5. Validates output image dimensions match source and resizes if needed
|
|
3709
|
+
|
|
3710
|
+
**Requirements:**
|
|
3711
|
+
- GEMINI_API_KEY or GOOGLE_API_KEY environment variable must be set
|
|
3712
|
+
- Screenshots must be in: public/products/{slug}/screenshots/{locale}/phone/ and /tablet/
|
|
3713
|
+
- Locale files must exist in: public/products/{slug}/locales/
|
|
3714
|
+
|
|
3715
|
+
**Example structure:**
|
|
3716
|
+
\`\`\`
|
|
3717
|
+
public/products/my-app/
|
|
3718
|
+
\u251C\u2500\u2500 config.json
|
|
3719
|
+
\u251C\u2500\u2500 locales/
|
|
3720
|
+
\u2502 \u251C\u2500\u2500 en-US.json (primary)
|
|
3721
|
+
\u2502 \u251C\u2500\u2500 ko-KR.json
|
|
3722
|
+
\u2502 \u2514\u2500\u2500 ja-JP.json
|
|
3723
|
+
\u2514\u2500\u2500 screenshots/
|
|
3724
|
+
\u2514\u2500\u2500 en-US/
|
|
3725
|
+
\u251C\u2500\u2500 phone/
|
|
3726
|
+
\u2502 \u251C\u2500\u2500 1.png
|
|
3727
|
+
\u2502 \u2514\u2500\u2500 2.png
|
|
3728
|
+
\u2514\u2500\u2500 tablet/
|
|
3729
|
+
\u2514\u2500\u2500 1.png
|
|
3730
|
+
\`\`\``,
|
|
3731
|
+
inputSchema: inputSchema7
|
|
3732
|
+
};
|
|
3733
|
+
function validateApp(appName) {
|
|
3734
|
+
const { app } = findRegisteredApp(appName);
|
|
3735
|
+
if (!app) {
|
|
3736
|
+
throw new Error(
|
|
3737
|
+
`App not found: "${appName}". Use search-app tool to find the correct app name.`
|
|
3738
|
+
);
|
|
3739
|
+
}
|
|
3740
|
+
return {
|
|
3741
|
+
slug: app.slug,
|
|
3742
|
+
name: app.name || app.slug
|
|
3743
|
+
};
|
|
3744
|
+
}
|
|
3745
|
+
function getSupportedLocales(slug) {
|
|
3746
|
+
const { config, locales } = loadProductLocales(slug);
|
|
3747
|
+
const allLocales = Object.keys(locales);
|
|
3748
|
+
if (allLocales.length === 0) {
|
|
3749
|
+
throw new Error(`No locale files found for ${slug}`);
|
|
3750
|
+
}
|
|
3751
|
+
const primaryLocale = resolvePrimaryLocale(config, locales);
|
|
3752
|
+
return {
|
|
3753
|
+
primaryLocale,
|
|
3754
|
+
allLocales
|
|
3755
|
+
};
|
|
3756
|
+
}
|
|
3757
|
+
function getTargetLocales(allLocales, primaryLocale, requestedTargets) {
|
|
3758
|
+
let targets = allLocales.filter((locale) => locale !== primaryLocale);
|
|
3759
|
+
if (requestedTargets && requestedTargets.length > 0) {
|
|
3760
|
+
const validTargets = requestedTargets.filter((t) => allLocales.includes(t));
|
|
3761
|
+
const invalidTargets = requestedTargets.filter(
|
|
3762
|
+
(t) => !allLocales.includes(t)
|
|
3763
|
+
);
|
|
3764
|
+
if (invalidTargets.length > 0) {
|
|
3765
|
+
console.warn(
|
|
3766
|
+
`Warning: Some requested locales are not supported: ${invalidTargets.join(", ")}`
|
|
3767
|
+
);
|
|
3768
|
+
}
|
|
3769
|
+
targets = validTargets.filter((t) => t !== primaryLocale);
|
|
3770
|
+
}
|
|
3771
|
+
return targets;
|
|
3772
|
+
}
|
|
3773
|
+
function buildTranslationTasks(slug, screenshots, primaryLocale, targetLocales, skipExisting) {
|
|
3774
|
+
const tasks = [];
|
|
3775
|
+
const screenshotsDir = getScreenshotsDir(slug);
|
|
3776
|
+
for (const targetLocale of targetLocales) {
|
|
3777
|
+
for (const screenshot of screenshots) {
|
|
3778
|
+
const outputPath = path11.join(
|
|
3779
|
+
screenshotsDir,
|
|
3780
|
+
targetLocale,
|
|
3781
|
+
screenshot.type,
|
|
3782
|
+
screenshot.filename
|
|
3783
|
+
);
|
|
3784
|
+
if (skipExisting && fs12.existsSync(outputPath)) {
|
|
3785
|
+
continue;
|
|
3786
|
+
}
|
|
3787
|
+
tasks.push({
|
|
3788
|
+
sourcePath: screenshot.fullPath,
|
|
3789
|
+
sourceLocale: primaryLocale,
|
|
3790
|
+
targetLocale,
|
|
3791
|
+
outputPath,
|
|
3792
|
+
deviceType: screenshot.type,
|
|
3793
|
+
filename: screenshot.filename
|
|
3794
|
+
});
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
return tasks;
|
|
3798
|
+
}
|
|
3799
|
+
async function handleLocalizeScreenshots(input) {
|
|
3800
|
+
const {
|
|
3801
|
+
appName,
|
|
3802
|
+
targetLocales: requestedTargetLocales,
|
|
3803
|
+
deviceTypes = ["phone", "tablet"],
|
|
3804
|
+
dryRun = false,
|
|
3805
|
+
skipExisting = true
|
|
3806
|
+
} = input;
|
|
3807
|
+
const results = [];
|
|
3808
|
+
let appInfo;
|
|
3809
|
+
try {
|
|
3810
|
+
appInfo = validateApp(appName);
|
|
3811
|
+
results.push(`\u2705 App found: ${appInfo.name} (${appInfo.slug})`);
|
|
3812
|
+
} catch (error) {
|
|
3813
|
+
return {
|
|
3814
|
+
content: [
|
|
3815
|
+
{
|
|
3816
|
+
type: "text",
|
|
3817
|
+
text: `\u274C ${error instanceof Error ? error.message : String(error)}`
|
|
3818
|
+
}
|
|
3819
|
+
]
|
|
3820
|
+
};
|
|
3821
|
+
}
|
|
3822
|
+
let primaryLocale;
|
|
3823
|
+
let allLocales;
|
|
3824
|
+
try {
|
|
3825
|
+
const localeInfo = getSupportedLocales(appInfo.slug);
|
|
3826
|
+
primaryLocale = localeInfo.primaryLocale;
|
|
3827
|
+
allLocales = localeInfo.allLocales;
|
|
3828
|
+
results.push(`\u{1F4CD} Primary locale: ${primaryLocale}`);
|
|
3829
|
+
results.push(`\u{1F310} Supported locales: ${allLocales.join(", ")}`);
|
|
3830
|
+
} catch (error) {
|
|
3831
|
+
return {
|
|
3832
|
+
content: [
|
|
3833
|
+
{
|
|
3834
|
+
type: "text",
|
|
3835
|
+
text: `\u274C ${error instanceof Error ? error.message : String(error)}`
|
|
3836
|
+
}
|
|
3837
|
+
]
|
|
3838
|
+
};
|
|
3839
|
+
}
|
|
3840
|
+
const targetLocales = getTargetLocales(
|
|
3841
|
+
allLocales,
|
|
3842
|
+
primaryLocale,
|
|
3843
|
+
requestedTargetLocales
|
|
3844
|
+
);
|
|
3845
|
+
if (targetLocales.length === 0) {
|
|
3846
|
+
return {
|
|
3847
|
+
content: [
|
|
3848
|
+
{
|
|
3849
|
+
type: "text",
|
|
3850
|
+
text: `\u274C No target locales to translate to. Primary locale: ${primaryLocale}, Available: ${allLocales.join(", ")}`
|
|
3851
|
+
}
|
|
3852
|
+
]
|
|
3853
|
+
};
|
|
3854
|
+
}
|
|
3855
|
+
results.push(`\u{1F3AF} Target locales: ${targetLocales.join(", ")}`);
|
|
3856
|
+
const sourceScreenshots = scanLocaleScreenshots(appInfo.slug, primaryLocale);
|
|
3857
|
+
const filteredScreenshots = sourceScreenshots.filter(
|
|
3858
|
+
(s) => deviceTypes.includes(s.type)
|
|
3859
|
+
);
|
|
3860
|
+
if (filteredScreenshots.length === 0) {
|
|
3861
|
+
const screenshotsDir2 = getScreenshotsDir(appInfo.slug);
|
|
3862
|
+
return {
|
|
3863
|
+
content: [
|
|
3864
|
+
{
|
|
3865
|
+
type: "text",
|
|
3866
|
+
text: `\u274C No screenshots found in ${screenshotsDir2}/${primaryLocale}/
|
|
3867
|
+
|
|
3868
|
+
Expected structure:
|
|
3869
|
+
${screenshotsDir2}/${primaryLocale}/phone/1.png, 2.png, ...
|
|
3870
|
+
${screenshotsDir2}/${primaryLocale}/tablet/1.png, 2.png, ...`
|
|
3871
|
+
}
|
|
3872
|
+
]
|
|
3873
|
+
};
|
|
3874
|
+
}
|
|
3875
|
+
const phoneCount = filteredScreenshots.filter((s) => s.type === "phone").length;
|
|
3876
|
+
const tabletCount = filteredScreenshots.filter((s) => s.type === "tablet").length;
|
|
3877
|
+
results.push(`\u{1F4F8} Source screenshots: ${phoneCount} phone, ${tabletCount} tablet`);
|
|
3878
|
+
const tasks = buildTranslationTasks(
|
|
3879
|
+
appInfo.slug,
|
|
3880
|
+
filteredScreenshots,
|
|
3881
|
+
primaryLocale,
|
|
3882
|
+
targetLocales,
|
|
3883
|
+
skipExisting
|
|
3884
|
+
);
|
|
3885
|
+
if (tasks.length === 0) {
|
|
3886
|
+
results.push(`
|
|
3887
|
+
\u2705 All screenshots already translated (skipExisting=true)`);
|
|
3888
|
+
return {
|
|
3889
|
+
content: [
|
|
3890
|
+
{
|
|
3891
|
+
type: "text",
|
|
3892
|
+
text: results.join("\n")
|
|
3893
|
+
}
|
|
3894
|
+
]
|
|
3895
|
+
};
|
|
3896
|
+
}
|
|
3897
|
+
results.push(`
|
|
3898
|
+
\u{1F4CB} Translation tasks: ${tasks.length} images to translate`);
|
|
3899
|
+
if (dryRun) {
|
|
3900
|
+
results.push(`
|
|
3901
|
+
\u{1F50D} DRY RUN - No actual translations will be performed
|
|
3902
|
+
`);
|
|
3903
|
+
const tasksByLocale = {};
|
|
3904
|
+
for (const task of tasks) {
|
|
3905
|
+
if (!tasksByLocale[task.targetLocale]) {
|
|
3906
|
+
tasksByLocale[task.targetLocale] = [];
|
|
3907
|
+
}
|
|
3908
|
+
tasksByLocale[task.targetLocale].push(task);
|
|
3909
|
+
}
|
|
3910
|
+
for (const [locale, localeTasks] of Object.entries(tasksByLocale)) {
|
|
3911
|
+
results.push(`
|
|
3912
|
+
\u{1F4C1} ${locale}:`);
|
|
3913
|
+
for (const task of localeTasks) {
|
|
3914
|
+
results.push(` - ${task.deviceType}/${task.filename}`);
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3917
|
+
return {
|
|
3918
|
+
content: [
|
|
3919
|
+
{
|
|
3920
|
+
type: "text",
|
|
3921
|
+
text: results.join("\n")
|
|
3922
|
+
}
|
|
3923
|
+
]
|
|
3924
|
+
};
|
|
3925
|
+
}
|
|
3926
|
+
results.push(`
|
|
3927
|
+
\u{1F680} Starting translations...`);
|
|
3928
|
+
const translationResult = await translateImagesWithProgress(
|
|
3929
|
+
tasks,
|
|
3930
|
+
(progress) => {
|
|
3931
|
+
if (progress.status === "completed") {
|
|
3932
|
+
console.log(
|
|
3933
|
+
`\u2705 ${progress.targetLocale}/${progress.deviceType}/${progress.filename}`
|
|
3934
|
+
);
|
|
3935
|
+
} else if (progress.status === "failed") {
|
|
3936
|
+
console.log(
|
|
3937
|
+
`\u274C ${progress.targetLocale}/${progress.deviceType}/${progress.filename}: ${progress.error}`
|
|
3938
|
+
);
|
|
3939
|
+
}
|
|
3940
|
+
}
|
|
3941
|
+
);
|
|
3942
|
+
results.push(`
|
|
3943
|
+
\u{1F4CA} Translation Results:`);
|
|
3944
|
+
results.push(` \u2705 Successful: ${translationResult.successful}`);
|
|
3945
|
+
results.push(` \u274C Failed: ${translationResult.failed}`);
|
|
3946
|
+
if (translationResult.errors.length > 0) {
|
|
3947
|
+
results.push(`
|
|
3948
|
+
\u26A0\uFE0F Errors:`);
|
|
3949
|
+
for (const err of translationResult.errors.slice(0, 5)) {
|
|
3950
|
+
results.push(` - ${path11.basename(err.path)}: ${err.error}`);
|
|
3951
|
+
}
|
|
3952
|
+
if (translationResult.errors.length > 5) {
|
|
3953
|
+
results.push(` ... and ${translationResult.errors.length - 5} more errors`);
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
3956
|
+
if (translationResult.successful > 0) {
|
|
3957
|
+
results.push(`
|
|
3958
|
+
\u{1F50D} Validating image dimensions...`);
|
|
3959
|
+
const successfulTasks = tasks.filter((t) => fs12.existsSync(t.outputPath));
|
|
3960
|
+
const resizePairs = successfulTasks.map((t) => ({
|
|
3961
|
+
sourcePath: t.sourcePath,
|
|
3962
|
+
translatedPath: t.outputPath
|
|
3963
|
+
}));
|
|
3964
|
+
const resizeResult = await batchValidateAndResize(resizePairs);
|
|
3965
|
+
if (resizeResult.resized > 0) {
|
|
3966
|
+
results.push(` \u{1F527} Resized ${resizeResult.resized} images to match source dimensions`);
|
|
3967
|
+
} else {
|
|
3968
|
+
results.push(` \u2705 All image dimensions match source`);
|
|
3969
|
+
}
|
|
3970
|
+
if (resizeResult.errors.length > 0) {
|
|
3971
|
+
results.push(` \u26A0\uFE0F Resize errors: ${resizeResult.errors.length}`);
|
|
3972
|
+
}
|
|
3973
|
+
}
|
|
3974
|
+
const screenshotsDir = getScreenshotsDir(appInfo.slug);
|
|
3975
|
+
results.push(`
|
|
3976
|
+
\u{1F4C1} Output location: ${screenshotsDir}/`);
|
|
3977
|
+
results.push(`
|
|
3978
|
+
\u2705 Screenshot localization complete!`);
|
|
3979
|
+
return {
|
|
3980
|
+
content: [
|
|
3981
|
+
{
|
|
3982
|
+
type: "text",
|
|
3983
|
+
text: results.join("\n")
|
|
3984
|
+
}
|
|
3985
|
+
]
|
|
3986
|
+
};
|
|
3987
|
+
}
|
|
3988
|
+
|
|
3989
|
+
// src/tools/apps/init.ts
|
|
3990
|
+
import fs13 from "fs";
|
|
3991
|
+
import path12 from "path";
|
|
3992
|
+
import { z as z8 } from "zod";
|
|
3993
|
+
import { zodToJsonSchema as zodToJsonSchema8 } from "zod-to-json-schema";
|
|
3392
3994
|
var listSlugDirs = (dir) => {
|
|
3393
|
-
if (!
|
|
3394
|
-
return
|
|
3995
|
+
if (!fs13.existsSync(dir)) return [];
|
|
3996
|
+
return fs13.readdirSync(dir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
|
|
3395
3997
|
};
|
|
3396
|
-
var initProjectInputSchema =
|
|
3397
|
-
slug:
|
|
3998
|
+
var initProjectInputSchema = z8.object({
|
|
3999
|
+
slug: z8.string().trim().optional().describe(
|
|
3398
4000
|
"Optional product slug to focus on. Defaults to all slugs in .aso/pullData/products/"
|
|
3399
4001
|
)
|
|
3400
4002
|
});
|
|
3401
|
-
var
|
|
4003
|
+
var jsonSchema8 = zodToJsonSchema8(initProjectInputSchema, {
|
|
3402
4004
|
name: "InitProjectInput",
|
|
3403
4005
|
$refStrategy: "none"
|
|
3404
4006
|
});
|
|
3405
|
-
var
|
|
4007
|
+
var inputSchema8 = jsonSchema8.definitions?.InitProjectInput || jsonSchema8;
|
|
3406
4008
|
var initProjectTool = {
|
|
3407
4009
|
name: "init-project",
|
|
3408
4010
|
description: `Guides the initialization flow: run pabal-store-api-mcp Init, then convert ASO pullData into public/products/[slug]/.
|
|
@@ -3413,10 +4015,10 @@ Steps:
|
|
|
3413
4015
|
1) Ensure pabal-store-api-mcp 'init' ran and .aso/pullData/products/[slug]/ exists (path from ~/.config/pabal-mcp/config.json dataDir)
|
|
3414
4016
|
2) Convert pulled ASO data -> public/products/[slug]/ using pabal-resource-mcp tools (aso-to-public, public-to-aso dry run)
|
|
3415
4017
|
3) Validate outputs and next actions`,
|
|
3416
|
-
inputSchema:
|
|
4018
|
+
inputSchema: inputSchema8
|
|
3417
4019
|
};
|
|
3418
4020
|
async function handleInitProject(input) {
|
|
3419
|
-
const pullDataDir =
|
|
4021
|
+
const pullDataDir = path12.join(getPullDataDir(), "products");
|
|
3420
4022
|
const publicDir = getProductsDir();
|
|
3421
4023
|
const pullDataSlugs = listSlugDirs(pullDataDir);
|
|
3422
4024
|
const publicSlugs = listSlugDirs(publicDir);
|
|
@@ -3494,14 +4096,14 @@ async function handleInitProject(input) {
|
|
|
3494
4096
|
}
|
|
3495
4097
|
|
|
3496
4098
|
// src/tools/content/create-blog-html.ts
|
|
3497
|
-
import
|
|
3498
|
-
import
|
|
3499
|
-
import { z as
|
|
3500
|
-
import { zodToJsonSchema as
|
|
4099
|
+
import fs15 from "fs";
|
|
4100
|
+
import path14 from "path";
|
|
4101
|
+
import { z as z9 } from "zod";
|
|
4102
|
+
import { zodToJsonSchema as zodToJsonSchema9 } from "zod-to-json-schema";
|
|
3501
4103
|
|
|
3502
4104
|
// src/utils/blog.util.ts
|
|
3503
|
-
import
|
|
3504
|
-
import
|
|
4105
|
+
import fs14 from "fs";
|
|
4106
|
+
import path13 from "path";
|
|
3505
4107
|
var DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
|
|
3506
4108
|
var BLOG_ROOT = "blogs";
|
|
3507
4109
|
var removeDiacritics = (value) => value.normalize("NFKD").replace(/[\u0300-\u036f]/g, "");
|
|
@@ -3589,13 +4191,13 @@ function resolveTargetLocales(input) {
|
|
|
3589
4191
|
return fallback ? [fallback] : [];
|
|
3590
4192
|
}
|
|
3591
4193
|
function getBlogOutputPaths(options) {
|
|
3592
|
-
const baseDir =
|
|
4194
|
+
const baseDir = path13.join(
|
|
3593
4195
|
options.publicDir,
|
|
3594
4196
|
BLOG_ROOT,
|
|
3595
4197
|
options.appSlug,
|
|
3596
4198
|
options.slug
|
|
3597
4199
|
);
|
|
3598
|
-
const filePath =
|
|
4200
|
+
const filePath = path13.join(baseDir, `${options.locale}.html`);
|
|
3599
4201
|
const publicBasePath = toPublicBlogBase(options.appSlug, options.slug);
|
|
3600
4202
|
return { baseDir, filePath, publicBasePath };
|
|
3601
4203
|
}
|
|
@@ -3620,18 +4222,18 @@ function findExistingBlogPosts({
|
|
|
3620
4222
|
publicDir,
|
|
3621
4223
|
limit = 2
|
|
3622
4224
|
}) {
|
|
3623
|
-
const blogAppDir =
|
|
3624
|
-
if (!
|
|
4225
|
+
const blogAppDir = path13.join(publicDir, BLOG_ROOT, appSlug);
|
|
4226
|
+
if (!fs14.existsSync(blogAppDir)) {
|
|
3625
4227
|
return [];
|
|
3626
4228
|
}
|
|
3627
4229
|
const posts = [];
|
|
3628
|
-
const subdirs =
|
|
4230
|
+
const subdirs = fs14.readdirSync(blogAppDir, { withFileTypes: true });
|
|
3629
4231
|
for (const subdir of subdirs) {
|
|
3630
4232
|
if (!subdir.isDirectory()) continue;
|
|
3631
|
-
const localeFile =
|
|
3632
|
-
if (!
|
|
4233
|
+
const localeFile = path13.join(blogAppDir, subdir.name, `${locale}.html`);
|
|
4234
|
+
if (!fs14.existsSync(localeFile)) continue;
|
|
3633
4235
|
try {
|
|
3634
|
-
const htmlContent =
|
|
4236
|
+
const htmlContent = fs14.readFileSync(localeFile, "utf-8");
|
|
3635
4237
|
const { meta, body } = parseBlogHtml(htmlContent);
|
|
3636
4238
|
if (meta && meta.locale === locale) {
|
|
3637
4239
|
posts.push({
|
|
@@ -3658,43 +4260,43 @@ function findExistingBlogPosts({
|
|
|
3658
4260
|
}
|
|
3659
4261
|
|
|
3660
4262
|
// src/tools/content/create-blog-html.ts
|
|
3661
|
-
var toJsonSchema5 =
|
|
4263
|
+
var toJsonSchema5 = zodToJsonSchema9;
|
|
3662
4264
|
var DATE_REGEX2 = /^\d{4}-\d{2}-\d{2}$/;
|
|
3663
|
-
var createBlogHtmlInputSchema =
|
|
3664
|
-
appSlug:
|
|
4265
|
+
var createBlogHtmlInputSchema = z9.object({
|
|
4266
|
+
appSlug: z9.string().trim().min(1).default(DEFAULT_APP_SLUG).describe(
|
|
3665
4267
|
`Product/app slug used for paths and CTAs. Defaults to "${DEFAULT_APP_SLUG}" when not provided.`
|
|
3666
4268
|
),
|
|
3667
|
-
title:
|
|
4269
|
+
title: z9.string().trim().optional().describe(
|
|
3668
4270
|
"English title used for slug (kebab-case). Falls back to topic when omitted."
|
|
3669
4271
|
),
|
|
3670
|
-
topic:
|
|
3671
|
-
locale:
|
|
4272
|
+
topic: z9.string().trim().min(1, "topic is required").describe("Topic/angle to write about in the blog body"),
|
|
4273
|
+
locale: z9.string().trim().min(1, "locale is required").describe(
|
|
3672
4274
|
"Primary locale (e.g., 'en-US', 'ko-KR'). Required to determine the language for blog content generation."
|
|
3673
4275
|
),
|
|
3674
|
-
locales:
|
|
4276
|
+
locales: z9.array(z9.string().trim().min(1)).optional().describe(
|
|
3675
4277
|
"Optional list of locales to generate. Each locale gets its own HTML file. If provided, locale parameter is ignored."
|
|
3676
4278
|
),
|
|
3677
|
-
content:
|
|
4279
|
+
content: z9.string().trim().min(1, "content is required").describe(
|
|
3678
4280
|
"HTML content for the blog body. You (the LLM) must generate this HTML content based on the topic and locale. Structure should follow the pattern in public/en-US.html: paragraphs (<p>), headings (<h2>, <h3>), images (<img>), lists (<ul>, <li>), horizontal rules (<hr>), etc. The content should be written in the language corresponding to the locale."
|
|
3679
4281
|
),
|
|
3680
|
-
description:
|
|
4282
|
+
description: z9.string().trim().min(1, "description is required").describe(
|
|
3681
4283
|
"Meta description for the blog post. You (the LLM) must generate this based on the topic and locale. Should be a concise summary of the blog content in the language corresponding to the locale."
|
|
3682
4284
|
),
|
|
3683
|
-
tags:
|
|
4285
|
+
tags: z9.array(z9.string().trim().min(1)).optional().describe(
|
|
3684
4286
|
"Optional tags for BLOG_META. Defaults to tags derived from topic."
|
|
3685
4287
|
),
|
|
3686
|
-
coverImage:
|
|
4288
|
+
coverImage: z9.string().trim().optional().describe(
|
|
3687
4289
|
"Cover image path. Relative paths rewrite to /blogs/<app>/<slug>/..., default is /products/<appSlug>/og-image.png."
|
|
3688
4290
|
),
|
|
3689
|
-
publishedAt:
|
|
3690
|
-
modifiedAt:
|
|
3691
|
-
overwrite:
|
|
4291
|
+
publishedAt: z9.string().trim().regex(DATE_REGEX2, "publishedAt must use YYYY-MM-DD").optional().describe("Publish date (YYYY-MM-DD). Defaults to today."),
|
|
4292
|
+
modifiedAt: z9.string().trim().regex(DATE_REGEX2, "modifiedAt must use YYYY-MM-DD").optional().describe("Last modified date (YYYY-MM-DD). Defaults to publishedAt."),
|
|
4293
|
+
overwrite: z9.boolean().optional().default(false).describe("Overwrite existing files when true (default: false).")
|
|
3692
4294
|
}).describe("Generate static HTML blog posts with BLOG_META headers.");
|
|
3693
|
-
var
|
|
4295
|
+
var jsonSchema9 = toJsonSchema5(createBlogHtmlInputSchema, {
|
|
3694
4296
|
name: "CreateBlogHtmlInput",
|
|
3695
4297
|
$refStrategy: "none"
|
|
3696
4298
|
});
|
|
3697
|
-
var
|
|
4299
|
+
var inputSchema9 = jsonSchema9.definitions?.CreateBlogHtmlInput || jsonSchema9;
|
|
3698
4300
|
var createBlogHtmlTool = {
|
|
3699
4301
|
name: "create-blog-html",
|
|
3700
4302
|
description: `Generate HTML blog posts under public/blogs/<appSlug>/<slug>/<locale>.html with a BLOG_META block.
|
|
@@ -3731,7 +4333,7 @@ Supports multiple locales when locales[] is provided. Each locale gets its own H
|
|
|
3731
4333
|
1. Read existing posts in that locale to understand the writing style
|
|
3732
4334
|
2. Generate appropriate content in that locale's language
|
|
3733
4335
|
3. Match the writing style and format of existing posts`,
|
|
3734
|
-
inputSchema:
|
|
4336
|
+
inputSchema: inputSchema9
|
|
3735
4337
|
};
|
|
3736
4338
|
async function handleCreateBlogHtml(input) {
|
|
3737
4339
|
const publicDir = getPublicDir();
|
|
@@ -3774,7 +4376,7 @@ async function handleCreateBlogHtml(input) {
|
|
|
3774
4376
|
}
|
|
3775
4377
|
const output = {
|
|
3776
4378
|
slug,
|
|
3777
|
-
baseDir:
|
|
4379
|
+
baseDir: path14.join(publicDir, "blogs", appSlug, slug),
|
|
3778
4380
|
files: [],
|
|
3779
4381
|
coverImage: coverImage && coverImage.trim().length > 0 ? coverImage.trim() : `/products/${appSlug}/og-image.png`,
|
|
3780
4382
|
metaByLocale: {}
|
|
@@ -3788,7 +4390,7 @@ async function handleCreateBlogHtml(input) {
|
|
|
3788
4390
|
})
|
|
3789
4391
|
);
|
|
3790
4392
|
const existing = plannedFiles.filter(
|
|
3791
|
-
({ filePath }) =>
|
|
4393
|
+
({ filePath }) => fs15.existsSync(filePath)
|
|
3792
4394
|
);
|
|
3793
4395
|
if (existing.length > 0 && !overwrite) {
|
|
3794
4396
|
const existingList = existing.map((f) => f.filePath).join("\n- ");
|
|
@@ -3797,7 +4399,7 @@ async function handleCreateBlogHtml(input) {
|
|
|
3797
4399
|
- ${existingList}`
|
|
3798
4400
|
);
|
|
3799
4401
|
}
|
|
3800
|
-
|
|
4402
|
+
fs15.mkdirSync(output.baseDir, { recursive: true });
|
|
3801
4403
|
for (const locale of targetLocales) {
|
|
3802
4404
|
const { filePath } = getBlogOutputPaths({
|
|
3803
4405
|
appSlug,
|
|
@@ -3823,7 +4425,7 @@ async function handleCreateBlogHtml(input) {
|
|
|
3823
4425
|
meta,
|
|
3824
4426
|
content
|
|
3825
4427
|
});
|
|
3826
|
-
|
|
4428
|
+
fs15.writeFileSync(filePath, html, "utf-8");
|
|
3827
4429
|
output.files.push({ locale, path: filePath });
|
|
3828
4430
|
}
|
|
3829
4431
|
const summaryLines = [
|
|
@@ -3902,6 +4504,14 @@ var tools = [
|
|
|
3902
4504
|
handler: handleKeywordResearch,
|
|
3903
4505
|
category: "aso"
|
|
3904
4506
|
},
|
|
4507
|
+
{
|
|
4508
|
+
name: localizeScreenshotsTool.name,
|
|
4509
|
+
description: localizeScreenshotsTool.description,
|
|
4510
|
+
inputSchema: localizeScreenshotsTool.inputSchema,
|
|
4511
|
+
zodSchema: localizeScreenshotsInputSchema,
|
|
4512
|
+
handler: handleLocalizeScreenshots,
|
|
4513
|
+
category: "aso"
|
|
4514
|
+
},
|
|
3905
4515
|
// Apps Tools
|
|
3906
4516
|
{
|
|
3907
4517
|
name: initProjectTool.name,
|
|
@@ -3937,6 +4547,7 @@ function getToolDefinitions() {
|
|
|
3937
4547
|
initProjectTool,
|
|
3938
4548
|
createBlogHtmlTool,
|
|
3939
4549
|
keywordResearchTool,
|
|
4550
|
+
localizeScreenshotsTool,
|
|
3940
4551
|
searchAppTool,
|
|
3941
4552
|
validateAsoTool
|
|
3942
4553
|
];
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_LOCALE,
|
|
3
|
+
isAppStoreLocale,
|
|
4
|
+
isGooglePlayLocale,
|
|
5
|
+
isSupportedLocale
|
|
6
|
+
} from "./chunk-BOWRBVVV.js";
|
|
7
|
+
|
|
8
|
+
// src/utils/config.util.ts
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import os from "os";
|
|
12
|
+
function getAsoDataDir() {
|
|
13
|
+
const configPath = path.join(
|
|
14
|
+
os.homedir(),
|
|
15
|
+
".config",
|
|
16
|
+
"pabal-mcp",
|
|
17
|
+
"config.json"
|
|
18
|
+
);
|
|
19
|
+
if (!fs.existsSync(configPath)) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`Config file not found at ${configPath}. Please create the config file and set the 'dataDir' property to specify the ASO data directory.`
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const configContent = fs.readFileSync(configPath, "utf-8");
|
|
26
|
+
const config = JSON.parse(configContent);
|
|
27
|
+
if (!config.dataDir) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`'dataDir' property is not set in ${configPath}. Please set 'dataDir' to specify the ASO data directory.`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
if (path.isAbsolute(config.dataDir)) {
|
|
33
|
+
return config.dataDir;
|
|
34
|
+
}
|
|
35
|
+
return path.resolve(os.homedir(), config.dataDir);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (error instanceof Error && error.message.includes("dataDir")) {
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Failed to read config from ${configPath}: ${error instanceof Error ? error.message : String(error)}`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function getPullDataDir() {
|
|
46
|
+
return path.join(getAsoDataDir(), ".aso", "pullData");
|
|
47
|
+
}
|
|
48
|
+
function getPushDataDir() {
|
|
49
|
+
return path.join(getAsoDataDir(), ".aso", "pushData");
|
|
50
|
+
}
|
|
51
|
+
function getPublicDir() {
|
|
52
|
+
return path.join(getAsoDataDir(), "public");
|
|
53
|
+
}
|
|
54
|
+
function getKeywordResearchDir() {
|
|
55
|
+
return path.join(getAsoDataDir(), ".aso", "keywordResearch");
|
|
56
|
+
}
|
|
57
|
+
function getProductsDir() {
|
|
58
|
+
return path.join(getPublicDir(), "products");
|
|
59
|
+
}
|
|
60
|
+
function loadConfig() {
|
|
61
|
+
const configPath = path.join(
|
|
62
|
+
os.homedir(),
|
|
63
|
+
".config",
|
|
64
|
+
"pabal-mcp",
|
|
65
|
+
"config.json"
|
|
66
|
+
);
|
|
67
|
+
if (!fs.existsSync(configPath)) {
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const configContent = fs.readFileSync(configPath, "utf-8");
|
|
72
|
+
return JSON.parse(configContent);
|
|
73
|
+
} catch {
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function getGeminiApiKey() {
|
|
78
|
+
const config = loadConfig();
|
|
79
|
+
if (config.gemini?.apiKey) {
|
|
80
|
+
return config.gemini.apiKey;
|
|
81
|
+
}
|
|
82
|
+
const envKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
|
|
83
|
+
if (envKey) {
|
|
84
|
+
return envKey;
|
|
85
|
+
}
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Gemini API key not found. Set it in ~/.config/pabal-mcp/config.json under "gemini.apiKey" or use GEMINI_API_KEY environment variable.`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/utils/aso-converter.ts
|
|
92
|
+
import fs2 from "fs";
|
|
93
|
+
import path2 from "path";
|
|
94
|
+
function generateFullDescription(localeData, metadata = {}) {
|
|
95
|
+
const { aso, landing } = localeData;
|
|
96
|
+
const template = aso?.template;
|
|
97
|
+
if (!template) {
|
|
98
|
+
return "";
|
|
99
|
+
}
|
|
100
|
+
const landingFeatures = landing?.features?.items || [];
|
|
101
|
+
const landingScreenshots = landing?.screenshots?.images || [];
|
|
102
|
+
const keyHeading = template.keyFeaturesHeading || "Key Features";
|
|
103
|
+
const featuresHeading = template.featuresHeading || "Additional Features";
|
|
104
|
+
const parts = [template.intro];
|
|
105
|
+
if (landingFeatures.length > 0) {
|
|
106
|
+
parts.push(
|
|
107
|
+
"",
|
|
108
|
+
keyHeading,
|
|
109
|
+
"",
|
|
110
|
+
...landingFeatures.map(
|
|
111
|
+
(feature) => [`\u25B6\uFE0E ${feature.title}`, feature.body || ""].filter(Boolean).join("\n")
|
|
112
|
+
)
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
if (landingScreenshots.length > 0) {
|
|
116
|
+
parts.push("", featuresHeading, "");
|
|
117
|
+
parts.push(
|
|
118
|
+
...landingScreenshots.map(
|
|
119
|
+
(screenshot) => [`\u25B6\uFE0E ${screenshot.title}`, screenshot.description || ""].filter(Boolean).join("\n")
|
|
120
|
+
)
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
parts.push("", template.outro);
|
|
124
|
+
const includeSupport = template.includeSupportLinks ?? true;
|
|
125
|
+
if (includeSupport) {
|
|
126
|
+
const contactLines = [
|
|
127
|
+
metadata.instagram ? `Instagram: ${metadata.instagram}` : null,
|
|
128
|
+
metadata.contactEmail ? `Email: ${metadata.contactEmail}` : null,
|
|
129
|
+
metadata.termsUrl ? `- Terms of Use: ${metadata.termsUrl}` : null,
|
|
130
|
+
metadata.privacyUrl ? `- Privacy Policy: ${metadata.privacyUrl}` : null
|
|
131
|
+
].filter((line) => line !== null);
|
|
132
|
+
if (contactLines.length > 0) {
|
|
133
|
+
parts.push("", "[Contact & Support]", "", ...contactLines);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return parts.join("\n");
|
|
137
|
+
}
|
|
138
|
+
function loadAsoFromConfig(slug) {
|
|
139
|
+
const productsDir = getProductsDir();
|
|
140
|
+
const configPath = path2.join(productsDir, slug, "config.json");
|
|
141
|
+
console.debug(`[loadAsoFromConfig] Looking for ${slug}:`);
|
|
142
|
+
console.debug(` - productsDir: ${productsDir}`);
|
|
143
|
+
console.debug(` - configPath: ${configPath}`);
|
|
144
|
+
console.debug(` - configPath exists: ${fs2.existsSync(configPath)}`);
|
|
145
|
+
if (!fs2.existsSync(configPath)) {
|
|
146
|
+
console.warn(`[loadAsoFromConfig] Config file not found at ${configPath}`);
|
|
147
|
+
return {};
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const configContent = fs2.readFileSync(configPath, "utf-8");
|
|
151
|
+
const config = JSON.parse(configContent);
|
|
152
|
+
const localesDir = path2.join(productsDir, slug, "locales");
|
|
153
|
+
console.debug(` - localesDir: ${localesDir}`);
|
|
154
|
+
console.debug(` - localesDir exists: ${fs2.existsSync(localesDir)}`);
|
|
155
|
+
if (!fs2.existsSync(localesDir)) {
|
|
156
|
+
console.warn(
|
|
157
|
+
`[loadAsoFromConfig] Locales directory not found at ${localesDir}`
|
|
158
|
+
);
|
|
159
|
+
return {};
|
|
160
|
+
}
|
|
161
|
+
const localeFiles = fs2.readdirSync(localesDir).filter((f) => f.endsWith(".json"));
|
|
162
|
+
const locales = {};
|
|
163
|
+
for (const file of localeFiles) {
|
|
164
|
+
const localeCode = file.replace(".json", "");
|
|
165
|
+
const localePath = path2.join(localesDir, file);
|
|
166
|
+
const localeContent = fs2.readFileSync(localePath, "utf-8");
|
|
167
|
+
locales[localeCode] = JSON.parse(localeContent);
|
|
168
|
+
}
|
|
169
|
+
console.debug(
|
|
170
|
+
` - Found ${Object.keys(locales).length} locale file(s): ${Object.keys(
|
|
171
|
+
locales
|
|
172
|
+
).join(", ")}`
|
|
173
|
+
);
|
|
174
|
+
if (Object.keys(locales).length === 0) {
|
|
175
|
+
console.warn(
|
|
176
|
+
`[loadAsoFromConfig] No locale files found in ${localesDir}`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
const defaultLocale = config.content?.defaultLocale || DEFAULT_LOCALE;
|
|
180
|
+
const asoData = {};
|
|
181
|
+
if (config.packageName) {
|
|
182
|
+
const googlePlayLocales = {};
|
|
183
|
+
const metadata = config.metadata || {};
|
|
184
|
+
const screenshots = metadata.screenshots || {};
|
|
185
|
+
for (const [locale, localeData] of Object.entries(locales)) {
|
|
186
|
+
if (!isSupportedLocale(locale)) {
|
|
187
|
+
console.debug(
|
|
188
|
+
`Skipping locale ${locale} - not a valid unified locale`
|
|
189
|
+
);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (!isGooglePlayLocale(locale)) {
|
|
193
|
+
console.debug(
|
|
194
|
+
`Skipping locale ${locale} - not supported by Google Play`
|
|
195
|
+
);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const aso = localeData.aso || {};
|
|
199
|
+
if (!aso || !aso.title && !aso.shortDescription) {
|
|
200
|
+
console.warn(
|
|
201
|
+
`Locale ${locale} has no ASO data (title or shortDescription)`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
const localeScreenshots = {
|
|
205
|
+
phone: screenshots.phone?.map(
|
|
206
|
+
(p) => p.replace("/screenshots/", `/screenshots/${locale}/`)
|
|
207
|
+
),
|
|
208
|
+
tablet: screenshots.tablet?.map(
|
|
209
|
+
(p) => p.replace("/screenshots/", `/screenshots/${locale}/`)
|
|
210
|
+
)
|
|
211
|
+
};
|
|
212
|
+
googlePlayLocales[locale] = {
|
|
213
|
+
title: aso.title || "",
|
|
214
|
+
shortDescription: aso.shortDescription || "",
|
|
215
|
+
fullDescription: generateFullDescription(localeData, metadata),
|
|
216
|
+
packageName: config.packageName,
|
|
217
|
+
defaultLanguage: locale,
|
|
218
|
+
screenshots: {
|
|
219
|
+
phone: localeScreenshots.phone || [],
|
|
220
|
+
tablet: localeScreenshots.tablet
|
|
221
|
+
},
|
|
222
|
+
contactEmail: metadata.contactEmail
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
const googleLocaleKeys = Object.keys(googlePlayLocales);
|
|
226
|
+
if (googleLocaleKeys.length > 0) {
|
|
227
|
+
const hasConfigDefault = isGooglePlayLocale(defaultLocale) && Boolean(googlePlayLocales[defaultLocale]);
|
|
228
|
+
const resolvedDefault = hasConfigDefault ? defaultLocale : googlePlayLocales[DEFAULT_LOCALE] ? DEFAULT_LOCALE : googleLocaleKeys[0];
|
|
229
|
+
asoData.googlePlay = {
|
|
230
|
+
locales: googlePlayLocales,
|
|
231
|
+
defaultLocale: resolvedDefault
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (config.bundleId) {
|
|
236
|
+
const appStoreLocales = {};
|
|
237
|
+
const metadata = config.metadata || {};
|
|
238
|
+
const screenshots = metadata.screenshots || {};
|
|
239
|
+
for (const [locale, localeData] of Object.entries(locales)) {
|
|
240
|
+
if (!isSupportedLocale(locale)) {
|
|
241
|
+
console.debug(
|
|
242
|
+
`Skipping locale ${locale} - not a valid unified locale`
|
|
243
|
+
);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (!isAppStoreLocale(locale)) {
|
|
247
|
+
console.debug(
|
|
248
|
+
`Skipping locale ${locale} - not supported by App Store`
|
|
249
|
+
);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const aso = localeData.aso || {};
|
|
253
|
+
if (!aso || !aso.title && !aso.shortDescription) {
|
|
254
|
+
console.warn(
|
|
255
|
+
`Locale ${locale} has no ASO data (title or shortDescription)`
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
const localeScreenshots = {
|
|
259
|
+
phone: screenshots.phone?.map(
|
|
260
|
+
(p) => p.replace("/screenshots/", `/screenshots/${locale}/`)
|
|
261
|
+
),
|
|
262
|
+
tablet: screenshots.tablet?.map(
|
|
263
|
+
(p) => p.replace("/screenshots/", `/screenshots/${locale}/`)
|
|
264
|
+
)
|
|
265
|
+
};
|
|
266
|
+
appStoreLocales[locale] = {
|
|
267
|
+
name: aso.title || "",
|
|
268
|
+
subtitle: aso.subtitle,
|
|
269
|
+
description: generateFullDescription(localeData, metadata),
|
|
270
|
+
keywords: Array.isArray(aso.keywords) ? aso.keywords.join(", ") : aso.keywords,
|
|
271
|
+
promotionalText: void 0,
|
|
272
|
+
bundleId: config.bundleId,
|
|
273
|
+
locale,
|
|
274
|
+
supportUrl: metadata.supportUrl,
|
|
275
|
+
marketingUrl: metadata.marketingUrl,
|
|
276
|
+
privacyPolicyUrl: metadata.privacyUrl,
|
|
277
|
+
screenshots: {
|
|
278
|
+
// 폰 스크린샷을 iphone65로 매핑
|
|
279
|
+
iphone65: localeScreenshots.phone || [],
|
|
280
|
+
// 태블릿 스크린샷을 ipadPro129로 매핑
|
|
281
|
+
ipadPro129: localeScreenshots.tablet
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const appStoreLocaleKeys = Object.keys(appStoreLocales);
|
|
286
|
+
if (appStoreLocaleKeys.length > 0) {
|
|
287
|
+
const hasConfigDefault = isAppStoreLocale(defaultLocale) && Boolean(appStoreLocales[defaultLocale]);
|
|
288
|
+
const resolvedDefault = hasConfigDefault ? defaultLocale : appStoreLocales[DEFAULT_LOCALE] ? DEFAULT_LOCALE : appStoreLocaleKeys[0];
|
|
289
|
+
asoData.appStore = {
|
|
290
|
+
locales: appStoreLocales,
|
|
291
|
+
defaultLocale: resolvedDefault
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const hasGooglePlay = !!asoData.googlePlay;
|
|
296
|
+
const hasAppStore = !!asoData.appStore;
|
|
297
|
+
console.debug(`[loadAsoFromConfig] Result for ${slug}:`);
|
|
298
|
+
console.debug(
|
|
299
|
+
` - Google Play data: ${hasGooglePlay ? "found" : "not found"}`
|
|
300
|
+
);
|
|
301
|
+
console.debug(` - App Store data: ${hasAppStore ? "found" : "not found"}`);
|
|
302
|
+
if (!hasGooglePlay && !hasAppStore) {
|
|
303
|
+
console.warn(`[loadAsoFromConfig] No ASO data generated for ${slug}`);
|
|
304
|
+
}
|
|
305
|
+
return asoData;
|
|
306
|
+
} catch (error) {
|
|
307
|
+
console.error(
|
|
308
|
+
`[loadAsoFromConfig] Failed to load ASO data from config for ${slug}:`,
|
|
309
|
+
error
|
|
310
|
+
);
|
|
311
|
+
return {};
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function saveAsoToConfig(slug, config) {
|
|
315
|
+
const productsDir = getProductsDir();
|
|
316
|
+
const configPath = path2.join(productsDir, slug, "config.json");
|
|
317
|
+
fs2.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
318
|
+
}
|
|
319
|
+
function saveAsoToAsoDir(slug, asoData) {
|
|
320
|
+
const rootDir = getPushDataDir();
|
|
321
|
+
if (asoData.googlePlay) {
|
|
322
|
+
const asoPath = path2.join(
|
|
323
|
+
rootDir,
|
|
324
|
+
"products",
|
|
325
|
+
slug,
|
|
326
|
+
"store",
|
|
327
|
+
"google-play",
|
|
328
|
+
"aso-data.json"
|
|
329
|
+
);
|
|
330
|
+
const dir = path2.dirname(asoPath);
|
|
331
|
+
if (!fs2.existsSync(dir)) {
|
|
332
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
333
|
+
}
|
|
334
|
+
const googlePlayData = asoData.googlePlay;
|
|
335
|
+
const multilingualData = "locales" in googlePlayData ? googlePlayData : {
|
|
336
|
+
locales: {
|
|
337
|
+
[googlePlayData.defaultLanguage || DEFAULT_LOCALE]: googlePlayData
|
|
338
|
+
},
|
|
339
|
+
defaultLocale: googlePlayData.defaultLanguage || DEFAULT_LOCALE
|
|
340
|
+
};
|
|
341
|
+
fs2.writeFileSync(
|
|
342
|
+
asoPath,
|
|
343
|
+
JSON.stringify({ googlePlay: multilingualData }, null, 2) + "\n",
|
|
344
|
+
"utf-8"
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
if (asoData.appStore) {
|
|
348
|
+
const asoPath = path2.join(
|
|
349
|
+
rootDir,
|
|
350
|
+
"products",
|
|
351
|
+
slug,
|
|
352
|
+
"store",
|
|
353
|
+
"app-store",
|
|
354
|
+
"aso-data.json"
|
|
355
|
+
);
|
|
356
|
+
const dir = path2.dirname(asoPath);
|
|
357
|
+
if (!fs2.existsSync(dir)) {
|
|
358
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
359
|
+
}
|
|
360
|
+
const appStoreData = asoData.appStore;
|
|
361
|
+
const multilingualData = "locales" in appStoreData ? appStoreData : {
|
|
362
|
+
locales: {
|
|
363
|
+
[appStoreData.locale || DEFAULT_LOCALE]: appStoreData
|
|
364
|
+
},
|
|
365
|
+
defaultLocale: appStoreData.locale || DEFAULT_LOCALE
|
|
366
|
+
};
|
|
367
|
+
fs2.writeFileSync(
|
|
368
|
+
asoPath,
|
|
369
|
+
JSON.stringify({ appStore: multilingualData }, null, 2) + "\n",
|
|
370
|
+
"utf-8"
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export {
|
|
376
|
+
getAsoDataDir,
|
|
377
|
+
getPullDataDir,
|
|
378
|
+
getPushDataDir,
|
|
379
|
+
getPublicDir,
|
|
380
|
+
getKeywordResearchDir,
|
|
381
|
+
getProductsDir,
|
|
382
|
+
getGeminiApiKey,
|
|
383
|
+
loadAsoFromConfig,
|
|
384
|
+
saveAsoToConfig,
|
|
385
|
+
saveAsoToAsoDir
|
|
386
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -48,5 +48,10 @@ declare function getKeywordResearchDir(): string;
|
|
|
48
48
|
* Get the products directory path (dataDir/public/products)
|
|
49
49
|
*/
|
|
50
50
|
declare function getProductsDir(): string;
|
|
51
|
+
/**
|
|
52
|
+
* Get the Gemini API key from config.json or environment variable
|
|
53
|
+
* Priority: config.json > GEMINI_API_KEY > GOOGLE_API_KEY
|
|
54
|
+
*/
|
|
55
|
+
declare function getGeminiApiKey(): string;
|
|
51
56
|
|
|
52
|
-
export { AsoData, ProductConfig, getAsoDataDir, getKeywordResearchDir, getProductsDir, getPublicDir, getPullDataDir, getPushDataDir, loadAsoFromConfig, saveAsoToAsoDir, saveAsoToConfig };
|
|
57
|
+
export { AsoData, ProductConfig, getAsoDataDir, getGeminiApiKey, getKeywordResearchDir, getProductsDir, getPublicDir, getPullDataDir, getPushDataDir, loadAsoFromConfig, saveAsoToAsoDir, saveAsoToConfig };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getAsoDataDir,
|
|
3
|
+
getGeminiApiKey,
|
|
3
4
|
getKeywordResearchDir,
|
|
4
5
|
getProductsDir,
|
|
5
6
|
getPublicDir,
|
|
@@ -8,7 +9,7 @@ import {
|
|
|
8
9
|
loadAsoFromConfig,
|
|
9
10
|
saveAsoToAsoDir,
|
|
10
11
|
saveAsoToConfig
|
|
11
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-YKUBCCJA.js";
|
|
12
13
|
import {
|
|
13
14
|
APP_STORE_TO_UNIFIED,
|
|
14
15
|
DEFAULT_LOCALE,
|
|
@@ -52,6 +53,7 @@ export {
|
|
|
52
53
|
convertObjectToAppStore,
|
|
53
54
|
convertObjectToGooglePlay,
|
|
54
55
|
getAsoDataDir,
|
|
56
|
+
getGeminiApiKey,
|
|
55
57
|
getKeywordResearchDir,
|
|
56
58
|
getProductsDir,
|
|
57
59
|
getPublicDir,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pabal-resource-mcp",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MCP server for ASO data management with shared types and utilities",
|
|
6
6
|
"author": "skyu",
|
|
@@ -61,14 +61,17 @@
|
|
|
61
61
|
"prepublishOnly": "npm run build"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
+
"@google/genai": "^1.38.0",
|
|
64
65
|
"@googleapis/androidpublisher": "^33.2.0",
|
|
65
66
|
"@modelcontextprotocol/sdk": "^1.22.0",
|
|
66
67
|
"appstore-connect-sdk": "^1.3.2",
|
|
68
|
+
"sharp": "^0.34.5",
|
|
67
69
|
"zod": "^3.25.76",
|
|
68
70
|
"zod-to-json-schema": "^3.25.0"
|
|
69
71
|
},
|
|
70
72
|
"devDependencies": {
|
|
71
73
|
"@types/node": "^20.11.30",
|
|
74
|
+
"@types/sharp": "^0.31.1",
|
|
72
75
|
"tsup": "^8.0.0",
|
|
73
76
|
"tsx": "^4.20.6",
|
|
74
77
|
"typescript": "^5.4.5"
|