pabal-resource-mcp 1.4.8 → 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/README.md +6 -6
- package/dist/bin/mcp-server.js +673 -62
- package/dist/chunk-YKUBCCJA.js +386 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +3 -1
- package/package.json +5 -2
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,
|
|
@@ -1023,7 +1024,7 @@ ${validationMessage}`
|
|
|
1023
1024
|
`;
|
|
1024
1025
|
}
|
|
1025
1026
|
responseText += `
|
|
1026
|
-
Next step: Push to stores using pabal-mcp's aso-push tool`;
|
|
1027
|
+
Next step: Push to stores using pabal-store-api-mcp's aso-push tool`;
|
|
1027
1028
|
responseText += `
|
|
1028
1029
|
Reference: ${FIELD_LIMITS_DOC_PATH}`;
|
|
1029
1030
|
if (sanitizeWarnings.length > 0) {
|
|
@@ -3384,45 +3385,646 @@ 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
|
-
description: `Guides the initialization flow: run pabal-mcp Init, then convert ASO pullData into public/products/[slug]/.
|
|
4010
|
+
description: `Guides the initialization flow: run pabal-store-api-mcp Init, then convert ASO pullData into public/products/[slug]/.
|
|
3409
4011
|
|
|
3410
|
-
This tool is read-only and returns a checklist. It does not call pabal-mcp directly or write files.
|
|
4012
|
+
This tool is read-only and returns a checklist. It does not call pabal-store-api-mcp directly or write files.
|
|
3411
4013
|
|
|
3412
4014
|
Steps:
|
|
3413
|
-
1) Ensure pabal-mcp 'init' ran and .aso/pullData/products/[slug]/ exists (path from ~/.config/pabal-mcp/config.json dataDir)
|
|
3414
|
-
2) Convert pulled ASO data -> public/products/[slug]/ using pabal-
|
|
4015
|
+
1) Ensure pabal-store-api-mcp 'init' ran and .aso/pullData/products/[slug]/ exists (path from ~/.config/pabal-mcp/config.json dataDir)
|
|
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);
|
|
3423
4025
|
const targetSlugs = input.slug?.length && input.slug.trim().length > 0 ? [input.slug.trim()] : pullDataSlugs.length > 0 ? pullDataSlugs : publicSlugs;
|
|
3424
4026
|
const lines = [];
|
|
3425
|
-
lines.push("Init workflow (pabal-mcp -> pabal-
|
|
4027
|
+
lines.push("Init workflow (pabal-store-api-mcp -> pabal-resource-mcp)");
|
|
3426
4028
|
lines.push(
|
|
3427
4029
|
`Target slugs: ${targetSlugs.length > 0 ? targetSlugs.join(", ") : "(none detected)"}`
|
|
3428
4030
|
);
|
|
@@ -3435,7 +4037,7 @@ async function handleInitProject(input) {
|
|
|
3435
4037
|
lines.push("");
|
|
3436
4038
|
if (targetSlugs.length === 0) {
|
|
3437
4039
|
lines.push(
|
|
3438
|
-
"No products detected. Run pabal-mcp 'init' for your slug(s) to populate .aso/pullData/products/, then rerun this tool."
|
|
4040
|
+
"No products detected. Run pabal-store-api-mcp 'init' for your slug(s) to populate .aso/pullData/products/, then rerun this tool."
|
|
3439
4041
|
);
|
|
3440
4042
|
return {
|
|
3441
4043
|
content: [
|
|
@@ -3446,17 +4048,17 @@ async function handleInitProject(input) {
|
|
|
3446
4048
|
]
|
|
3447
4049
|
};
|
|
3448
4050
|
}
|
|
3449
|
-
lines.push("Step 1: Fetch raw ASO data (pabal-mcp 'init')");
|
|
4051
|
+
lines.push("Step 1: Fetch raw ASO data (pabal-store-api-mcp 'init')");
|
|
3450
4052
|
for (const slug of targetSlugs) {
|
|
3451
4053
|
const hasPull = pullDataSlugs.includes(slug);
|
|
3452
4054
|
lines.push(`- ${slug}: ${hasPull ? "pullData ready" : "pullData missing"}`);
|
|
3453
4055
|
}
|
|
3454
4056
|
lines.push(
|
|
3455
|
-
"Action: In pabal-mcp, run the 'init' tool for each slug above that is missing pullData."
|
|
4057
|
+
"Action: In pabal-store-api-mcp, run the 'init' tool for each slug above that is missing pullData."
|
|
3456
4058
|
);
|
|
3457
4059
|
lines.push("");
|
|
3458
4060
|
lines.push(
|
|
3459
|
-
"Step 2: Convert pullData to web assets (pabal-
|
|
4061
|
+
"Step 2: Convert pullData to web assets (pabal-resource-mcp 'aso-to-public')"
|
|
3460
4062
|
);
|
|
3461
4063
|
for (const slug of targetSlugs) {
|
|
3462
4064
|
const hasPull = pullDataSlugs.includes(slug);
|
|
@@ -3468,17 +4070,17 @@ async function handleInitProject(input) {
|
|
|
3468
4070
|
);
|
|
3469
4071
|
}
|
|
3470
4072
|
lines.push(
|
|
3471
|
-
"Action: After pullData exists, run pabal-
|
|
4073
|
+
"Action: After pullData exists, run pabal-resource-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."
|
|
3472
4074
|
);
|
|
3473
4075
|
lines.push("");
|
|
3474
4076
|
lines.push("Step 3: Verify and prepare for push (optional)");
|
|
3475
4077
|
lines.push(
|
|
3476
|
-
"Use pabal-
|
|
4078
|
+
"Use pabal-resource-mcp 'public-to-aso' with dryRun=true to validate structure and build pushData before uploading via store tooling."
|
|
3477
4079
|
);
|
|
3478
4080
|
lines.push("");
|
|
3479
4081
|
lines.push("Notes:");
|
|
3480
4082
|
lines.push(
|
|
3481
|
-
"- This tool is read-only; it does not write files or call pabal-mcp."
|
|
4083
|
+
"- This tool is read-only; it does not write files or call pabal-store-api-mcp."
|
|
3482
4084
|
);
|
|
3483
4085
|
lines.push(
|
|
3484
4086
|
"- Extend this init checklist as new processes are added (e.g., asset generation or validations)."
|
|
@@ -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
|
];
|
|
@@ -3959,7 +4570,7 @@ function getToolZodSchema(name) {
|
|
|
3959
4570
|
// src/bin/mcp-server.ts
|
|
3960
4571
|
var server = new Server(
|
|
3961
4572
|
{
|
|
3962
|
-
name: "pabal-
|
|
4573
|
+
name: "pabal-resource-mcp",
|
|
3963
4574
|
version: "0.1.0"
|
|
3964
4575
|
},
|
|
3965
4576
|
{
|
|
@@ -3986,7 +4597,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3986
4597
|
async function main() {
|
|
3987
4598
|
const transport = new StdioServerTransport();
|
|
3988
4599
|
await server.connect(transport);
|
|
3989
|
-
console.error("pabal-
|
|
4600
|
+
console.error("pabal-resource-mcp server running on stdio");
|
|
3990
4601
|
}
|
|
3991
4602
|
main().catch((error) => {
|
|
3992
4603
|
console.error("Fatal error in main():", error);
|