pabal-resource-mcp 1.8.11 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@ Build synced websites from App Store Connect and Google Play Console data.
6
6
 
7
7
  > 💡 **Example**: [labs.quartz.best](https://labs.quartz.best/)
8
8
 
9
- [![Documentation](https://img.shields.io/badge/docs-English-blue)](https://pabal.quartz.best/docs/en-US/pabal-store-api-mcp/README) [![한국어](https://img.shields.io/badge/docs-한국어-green)](https://pabal.quartz.best/docs/ko-KR/pabal-store-api-mcp/README)
9
+ [![Documentation](https://img.shields.io/badge/docs-English-blue)](https://pabal.quartz.best/en-US/docs/pabal-resource-mcp/README) [![한국어](https://img.shields.io/badge/docs-한국어-green)](https://pabal.quartz.best/ko-KR/docs/pabal-resource-mcp/README)
10
10
 
11
11
  ## Installation
12
12
 
@@ -61,7 +61,7 @@ Set `dataDir` in `~/.config/pabal-mcp/config.json`:
61
61
  | App Icon | `generate-app-icons` |
62
62
  | Content | `create-blog-html` |
63
63
 
64
- See [documentation](./docs/en-US/README.md) for details.
64
+ See [documentation](https://pabal.quartz.best/en-US/docs/pabal-resource-mcp/README) for details.
65
65
 
66
66
  ## License
67
67
 
@@ -3398,8 +3398,8 @@ Context around ${pos}: ${context}`
3398
3398
  // src/tools/screenshots/translate-screenshots.ts
3399
3399
  import { z as z7 } from "zod";
3400
3400
  import { zodToJsonSchema as zodToJsonSchema7 } from "zod-to-json-schema";
3401
- import fs11 from "fs";
3402
- import path11 from "path";
3401
+ import fs12 from "fs";
3402
+ import path12 from "path";
3403
3403
 
3404
3404
  // src/tools/screenshots/utils/scan-screenshots.util.ts
3405
3405
  import fs9 from "fs";
@@ -3481,10 +3481,161 @@ function scanRawScreenshots(slug, locale) {
3481
3481
  }
3482
3482
 
3483
3483
  // src/tools/screenshots/utils/gemini-image-translator.util.ts
3484
+ import fs11 from "fs";
3485
+ import path11 from "path";
3486
+ import sharp from "sharp";
3487
+
3488
+ // src/utils/gemini-image-model.util.ts
3489
+ var GEMINI_IMAGE_MODEL_PRESETS = {
3490
+ flash: "gemini-3.1-flash-image-preview",
3491
+ pro: "gemini-3-pro-image-preview"
3492
+ };
3493
+ var GEMINI_IMAGE_MODEL_VALUES = Object.keys(
3494
+ GEMINI_IMAGE_MODEL_PRESETS
3495
+ );
3496
+ var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([429, 500, 503, 504]);
3497
+ function parseCsv(value) {
3498
+ if (!value) {
3499
+ return [];
3500
+ }
3501
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
3502
+ }
3503
+ function getStatusCode(error) {
3504
+ if (typeof error !== "object" || error === null) {
3505
+ return void 0;
3506
+ }
3507
+ const status = error.status;
3508
+ return typeof status === "number" ? status : void 0;
3509
+ }
3510
+ function getDefaultModelOrder(preference) {
3511
+ if (preference === "pro") {
3512
+ return [
3513
+ GEMINI_IMAGE_MODEL_PRESETS.pro,
3514
+ GEMINI_IMAGE_MODEL_PRESETS.flash
3515
+ ];
3516
+ }
3517
+ return [
3518
+ GEMINI_IMAGE_MODEL_PRESETS.flash,
3519
+ GEMINI_IMAGE_MODEL_PRESETS.pro
3520
+ ];
3521
+ }
3522
+ function getGeminiImageModelCandidates(preference = "flash") {
3523
+ const defaultOrder = getDefaultModelOrder(preference);
3524
+ const preferredModel = defaultOrder[0];
3525
+ const envModelOverride = process.env.GEMINI_IMAGE_MODEL?.trim();
3526
+ const fallbackModels = parseCsv(process.env.GEMINI_IMAGE_FALLBACK_MODELS);
3527
+ const orderedModels = [
3528
+ preferredModel,
3529
+ ...envModelOverride ? [envModelOverride] : [],
3530
+ ...defaultOrder,
3531
+ ...fallbackModels
3532
+ ];
3533
+ return Array.from(
3534
+ new Set(orderedModels.filter((model) => Boolean(model)))
3535
+ );
3536
+ }
3537
+ function getGeminiErrorMessage(error) {
3538
+ const defaultMessage = error instanceof Error ? error.message : String(error);
3539
+ const normalizedMessage = defaultMessage.replace(/^exception\s*/i, "").trim();
3540
+ try {
3541
+ const parsed = JSON.parse(normalizedMessage);
3542
+ const apiMessage = parsed?.error?.message;
3543
+ if (typeof apiMessage === "string" && apiMessage.trim()) {
3544
+ return apiMessage.trim();
3545
+ }
3546
+ } catch {
3547
+ }
3548
+ return normalizedMessage || "Unknown Gemini API error";
3549
+ }
3550
+ function shouldTryNextGeminiImageModel(error) {
3551
+ const status = getStatusCode(error);
3552
+ if (status && RETRYABLE_STATUS_CODES.has(status)) {
3553
+ return true;
3554
+ }
3555
+ const message = getGeminiErrorMessage(error).toLowerCase();
3556
+ return message.includes("high demand") || message.includes("temporarily unavailable") || message.includes("try again later") || message.includes("overloaded") || message.includes("resource_exhausted") || message.includes("unavailable");
3557
+ }
3558
+
3559
+ // src/utils/gemini-client.util.ts
3484
3560
  import { GoogleGenAI } from "@google/genai";
3561
+ function createGeminiClient() {
3562
+ const apiKey = getGeminiApiKey();
3563
+ return new GoogleGenAI({ apiKey });
3564
+ }
3565
+
3566
+ // src/utils/image-file.util.ts
3485
3567
  import fs10 from "fs";
3486
3568
  import path10 from "path";
3487
- import sharp from "sharp";
3569
+ function readImageAsBase64(imagePath) {
3570
+ const buffer = fs10.readFileSync(imagePath);
3571
+ const base64 = buffer.toString("base64");
3572
+ const ext = path10.extname(imagePath).toLowerCase();
3573
+ let mimeType = "image/png";
3574
+ if (ext === ".jpg" || ext === ".jpeg") {
3575
+ mimeType = "image/jpeg";
3576
+ } else if (ext === ".webp") {
3577
+ mimeType = "image/webp";
3578
+ }
3579
+ return { data: base64, mimeType };
3580
+ }
3581
+
3582
+ // src/utils/gemini-image-generation.util.ts
3583
+ async function generateImageWithFallback(input) {
3584
+ const models = getGeminiImageModelCandidates(input.imageModel);
3585
+ let lastError;
3586
+ for (const model of models) {
3587
+ try {
3588
+ const chat = input.client.chats.create({
3589
+ model,
3590
+ config: {
3591
+ responseModalities: ["TEXT", "IMAGE"]
3592
+ }
3593
+ });
3594
+ const response = await chat.sendMessage({
3595
+ message: [
3596
+ { text: input.prompt },
3597
+ {
3598
+ inlineData: {
3599
+ mimeType: input.image.mimeType,
3600
+ data: input.image.data
3601
+ }
3602
+ }
3603
+ ],
3604
+ config: {
3605
+ responseModalities: ["TEXT", "IMAGE"],
3606
+ ...input.aspectRatio ? {
3607
+ imageConfig: {
3608
+ aspectRatio: input.aspectRatio
3609
+ }
3610
+ } : {}
3611
+ }
3612
+ });
3613
+ const parts = response.candidates?.[0]?.content?.parts;
3614
+ if (!parts) {
3615
+ throw new Error("No content parts in Gemini response");
3616
+ }
3617
+ for (const part of parts) {
3618
+ if (part.inlineData?.data) {
3619
+ return {
3620
+ model,
3621
+ imageBase64: part.inlineData.data
3622
+ };
3623
+ }
3624
+ }
3625
+ throw new Error("No image data in Gemini response");
3626
+ } catch (error) {
3627
+ lastError = error;
3628
+ if (!shouldTryNextGeminiImageModel(error)) {
3629
+ break;
3630
+ }
3631
+ }
3632
+ }
3633
+ throw new Error(
3634
+ `All Gemini image models failed (${models.join(", ")}): ${getGeminiErrorMessage(lastError)}`
3635
+ );
3636
+ }
3637
+
3638
+ // src/tools/screenshots/utils/gemini-image-translator.util.ts
3488
3639
  var GEMINI_ASPECT_RATIOS = {
3489
3640
  "1:1": { ratio: 1 / 1, width: 2048, height: 2048 },
3490
3641
  "2:3": { ratio: 2 / 3, width: 1696, height: 2528 },
@@ -3524,35 +3675,18 @@ function getLanguageName(locale) {
3524
3675
  }
3525
3676
  return locale;
3526
3677
  }
3527
- function getGeminiClient() {
3528
- const apiKey = getGeminiApiKey();
3529
- return new GoogleGenAI({ apiKey });
3530
- }
3531
- function readImageAsBase64(imagePath) {
3532
- const buffer = fs10.readFileSync(imagePath);
3533
- const base64 = buffer.toString("base64");
3534
- const ext = path10.extname(imagePath).toLowerCase();
3535
- let mimeType = "image/png";
3536
- if (ext === ".jpg" || ext === ".jpeg") {
3537
- mimeType = "image/jpeg";
3538
- } else if (ext === ".webp") {
3539
- mimeType = "image/webp";
3540
- }
3541
- return { data: base64, mimeType };
3542
- }
3543
3678
  function getAspectRatioForDevice(deviceType) {
3544
3679
  return DEVICE_ASPECT_RATIOS[deviceType];
3545
3680
  }
3546
- async function translateImage(sourcePath, sourceLocale, targetLocale, outputPaths, deviceType, preserveWords) {
3547
- try {
3548
- const client = getGeminiClient();
3549
- const sourceLanguage = getLanguageName(sourceLocale);
3550
- const targetLanguage = getLanguageName(targetLocale);
3551
- const aspectRatio = getAspectRatioForDevice(deviceType);
3552
- const { data: imageData, mimeType } = readImageAsBase64(sourcePath);
3553
- const preserveInstruction = preserveWords && preserveWords.length > 0 ? `
3681
+ async function translateImage(sourcePath, sourceLocale, targetLocale, outputPaths, deviceType, preserveWords, imageModel = "flash") {
3682
+ const client = createGeminiClient();
3683
+ const sourceLanguage = getLanguageName(sourceLocale);
3684
+ const targetLanguage = getLanguageName(targetLocale);
3685
+ const aspectRatio = getAspectRatioForDevice(deviceType);
3686
+ const { data: imageData, mimeType } = readImageAsBase64(sourcePath);
3687
+ const preserveInstruction = preserveWords && preserveWords.length > 0 ? `
3554
3688
  - Do NOT translate these words, keep them exactly as-is: ${preserveWords.join(", ")}` : "";
3555
- const prompt = `This is an app screenshot with text in ${sourceLanguage}.
3689
+ const prompt = `This is an app screenshot with text in ${sourceLanguage}.
3556
3690
  Please translate ONLY the text/words in this image to ${targetLanguage}.
3557
3691
 
3558
3692
  IMPORTANT INSTRUCTIONS:
@@ -3562,73 +3696,38 @@ IMPORTANT INSTRUCTIONS:
3562
3696
  - Do NOT add any new elements or remove existing design elements
3563
3697
  - The output should look identical except the text language is ${targetLanguage}
3564
3698
  - Preserve all icons, images, and graphical elements exactly as they are${preserveInstruction}`;
3565
- const chat = client.chats.create({
3566
- model: "gemini-3-pro-image-preview",
3567
- config: {
3568
- responseModalities: ["TEXT", "IMAGE"]
3569
- }
3570
- });
3571
- const response = await chat.sendMessage({
3572
- message: [
3573
- { text: prompt },
3574
- {
3575
- inlineData: {
3576
- mimeType,
3577
- data: imageData
3578
- }
3579
- }
3580
- ],
3581
- config: {
3582
- responseModalities: ["TEXT", "IMAGE"],
3583
- imageConfig: {
3584
- aspectRatio
3585
- }
3586
- }
3699
+ try {
3700
+ const generated = await generateImageWithFallback({
3701
+ client,
3702
+ prompt,
3703
+ image: {
3704
+ mimeType,
3705
+ data: imageData
3706
+ },
3707
+ aspectRatio,
3708
+ imageModel
3587
3709
  });
3588
- const candidates = response.candidates;
3589
- if (!candidates || candidates.length === 0) {
3590
- return {
3591
- success: false,
3592
- error: "No response from Gemini API"
3593
- };
3594
- }
3595
- const parts = candidates[0].content?.parts;
3596
- if (!parts) {
3597
- return {
3598
- success: false,
3599
- error: "No content parts in response"
3600
- };
3601
- }
3602
- for (const part of parts) {
3603
- if (part.inlineData?.data) {
3604
- const imageBuffer = Buffer.from(part.inlineData.data, "base64");
3605
- for (const outputPath of outputPaths) {
3606
- const outputDir = path10.dirname(outputPath);
3607
- if (!fs10.existsSync(outputDir)) {
3608
- fs10.mkdirSync(outputDir, { recursive: true });
3609
- }
3610
- await sharp(imageBuffer).png().toFile(outputPath);
3611
- }
3612
- return {
3613
- success: true,
3614
- outputPath: outputPaths[0]
3615
- // Return primary path
3616
- };
3710
+ const imageBuffer = Buffer.from(generated.imageBase64, "base64");
3711
+ for (const outputPath of outputPaths) {
3712
+ const outputDir = path11.dirname(outputPath);
3713
+ if (!fs11.existsSync(outputDir)) {
3714
+ fs11.mkdirSync(outputDir, { recursive: true });
3617
3715
  }
3716
+ await sharp(imageBuffer).png().toFile(outputPath);
3618
3717
  }
3619
3718
  return {
3620
- success: false,
3621
- error: "No image data in Gemini response"
3719
+ success: true,
3720
+ outputPath: outputPaths[0]
3721
+ // Return primary path
3622
3722
  };
3623
3723
  } catch (error) {
3624
- const message = error instanceof Error ? error.message : String(error);
3625
3724
  return {
3626
3725
  success: false,
3627
- error: message
3726
+ error: error instanceof Error ? error.message : String(error)
3628
3727
  };
3629
3728
  }
3630
3729
  }
3631
- async function translateImagesWithProgress(translations, onProgress, preserveWords) {
3730
+ async function translateImagesWithProgress(translations, onProgress, preserveWords, imageModel = "flash") {
3632
3731
  let successful = 0;
3633
3732
  let failed = 0;
3634
3733
  const errors = [];
@@ -3652,7 +3751,8 @@ async function translateImagesWithProgress(translations, onProgress, preserveWor
3652
3751
  translation.targetLocale,
3653
3752
  translation.outputPaths,
3654
3753
  translation.deviceType,
3655
- preserveWords
3754
+ preserveWords,
3755
+ imageModel
3656
3756
  );
3657
3757
  if (result.success) {
3658
3758
  successful++;
@@ -3671,6 +3771,9 @@ async function translateImagesWithProgress(translations, onProgress, preserveWor
3671
3771
  }
3672
3772
  return { successful, failed, errors };
3673
3773
  }
3774
+ function getImageModelLabel(imageModel) {
3775
+ return `${imageModel} (${GEMINI_IMAGE_MODEL_PRESETS[imageModel]})`;
3776
+ }
3674
3777
 
3675
3778
  // src/tools/screenshots/utils/locale-mapping.constants.ts
3676
3779
  var GEMINI_LOCALE_GROUPS = {
@@ -3773,6 +3876,9 @@ var translateScreenshotsInputSchema = z7.object({
3773
3876
  ),
3774
3877
  preserveWords: z7.array(z7.string()).optional().describe(
3775
3878
  'Words to keep untranslated (e.g., brand names, product names). Example: ["Pabal", "Pro", "AI"]'
3879
+ ),
3880
+ imageModel: z7.enum(GEMINI_IMAGE_MODEL_VALUES).optional().default("flash").describe(
3881
+ "Gemini image model preference. 'flash' (default) is faster/cheaper, 'pro' prioritizes quality."
3776
3882
  )
3777
3883
  });
3778
3884
  var jsonSchema7 = zodToJsonSchema7(translateScreenshotsInputSchema, {
@@ -3798,6 +3904,10 @@ Use \`resize-screenshots\` after this tool to resize images to final dimensions.
3798
3904
  - Screenshots must be in: public/products/{slug}/screenshots/{locale}/phone/ and /tablet/
3799
3905
  - Locale files must exist in: public/products/{slug}/locales/
3800
3906
 
3907
+ **Model Selection:**
3908
+ - \`imageModel: "flash"\` (default) for speed/cost
3909
+ - \`imageModel: "pro"\` for higher instruction fidelity
3910
+
3801
3911
  **Example output structure:**
3802
3912
  \`\`\`
3803
3913
  public/products/my-app/screenshots/
@@ -3873,14 +3983,14 @@ function buildTranslationTasks(slug, screenshots, primaryLocale, targetLocales,
3873
3983
  for (const screenshot of screenshots) {
3874
3984
  const outputPaths = [];
3875
3985
  for (const locale of outputLocales) {
3876
- const outputPath = path11.join(
3986
+ const outputPath = path12.join(
3877
3987
  screenshotsDir,
3878
3988
  locale,
3879
3989
  screenshot.type,
3880
3990
  "raw",
3881
3991
  screenshot.filename
3882
3992
  );
3883
- if (!skipExisting || !fs11.existsSync(outputPath)) {
3993
+ if (!skipExisting || !fs12.existsSync(outputPath)) {
3884
3994
  outputPaths.push(outputPath);
3885
3995
  }
3886
3996
  }
@@ -3907,7 +4017,8 @@ async function handleTranslateScreenshots(input) {
3907
4017
  dryRun = false,
3908
4018
  skipExisting = true,
3909
4019
  screenshotNumbers,
3910
- preserveWords
4020
+ preserveWords,
4021
+ imageModel = "flash"
3911
4022
  } = input;
3912
4023
  const results = [];
3913
4024
  let appInfo;
@@ -3960,6 +4071,7 @@ async function handleTranslateScreenshots(input) {
3960
4071
  };
3961
4072
  }
3962
4073
  results.push(`\u{1F3AF} Target locales to translate: ${targetLocales.join(", ")}`);
4074
+ results.push(`\u{1F9E0} Image model: ${getImageModelLabel(imageModel)}`);
3963
4075
  if (groupedLocales.length > 0) {
3964
4076
  results.push(
3965
4077
  `\u{1F4CB} Grouped locales (saved together): ${groupedLocales.join(", ")}`
@@ -4087,9 +4199,9 @@ ${screenshotsDir2}/${primaryLocale}/tablet/1.png, 2.png, ...`
4087
4199
  }
4088
4200
  for (const task of tasks) {
4089
4201
  for (const outputPath of task.outputPaths) {
4090
- const outputDir = path11.dirname(outputPath);
4091
- if (!fs11.existsSync(outputDir)) {
4092
- fs11.mkdirSync(outputDir, { recursive: true });
4202
+ const outputDir = path12.dirname(outputPath);
4203
+ if (!fs12.existsSync(outputDir)) {
4204
+ fs12.mkdirSync(outputDir, { recursive: true });
4093
4205
  }
4094
4206
  }
4095
4207
  }
@@ -4111,7 +4223,8 @@ ${screenshotsDir2}/${primaryLocale}/tablet/1.png, 2.png, ...`
4111
4223
  );
4112
4224
  }
4113
4225
  },
4114
- preserveWords
4226
+ preserveWords,
4227
+ imageModel
4115
4228
  );
4116
4229
  results.push(`
4117
4230
  \u{1F4CA} Translation Results:`);
@@ -4121,7 +4234,7 @@ ${screenshotsDir2}/${primaryLocale}/tablet/1.png, 2.png, ...`
4121
4234
  results.push(`
4122
4235
  \u26A0\uFE0F Errors:`);
4123
4236
  for (const err of translationResult.errors.slice(0, 5)) {
4124
- results.push(` - ${path11.basename(err.path)}: ${err.error}`);
4237
+ results.push(` - ${path12.basename(err.path)}: ${err.error}`);
4125
4238
  }
4126
4239
  if (translationResult.errors.length > 5) {
4127
4240
  results.push(
@@ -4149,12 +4262,12 @@ ${screenshotsDir2}/${primaryLocale}/tablet/1.png, 2.png, ...`
4149
4262
  // src/tools/screenshots/resize-screenshots.ts
4150
4263
  import { z as z8 } from "zod";
4151
4264
  import { zodToJsonSchema as zodToJsonSchema8 } from "zod-to-json-schema";
4152
- import fs13 from "fs";
4153
- import path12 from "path";
4265
+ import fs14 from "fs";
4266
+ import path13 from "path";
4154
4267
 
4155
4268
  // src/tools/screenshots/utils/image-resizer.util.ts
4156
4269
  import sharp2 from "sharp";
4157
- import fs12 from "fs";
4270
+ import fs13 from "fs";
4158
4271
  var SCREENSHOT_DIMENSIONS = {
4159
4272
  phone: { width: 1242, height: 2688 },
4160
4273
  tablet: { width: 2048, height: 2732 }
@@ -4253,7 +4366,7 @@ async function resizeImage(inputPath, outputPath, targetDimensions, bgColor) {
4253
4366
  kernel: "lanczos3"
4254
4367
  // High-quality downscaling algorithm
4255
4368
  }).flatten({ background: backgroundColor }).png().toFile(outputPath + ".tmp");
4256
- fs12.renameSync(outputPath + ".tmp", outputPath);
4369
+ fs13.renameSync(outputPath + ".tmp", outputPath);
4257
4370
  }
4258
4371
 
4259
4372
  // src/tools/screenshots/resize-screenshots.ts
@@ -4375,13 +4488,13 @@ function buildResizeTasks(slug, sourceScreenshots, rawLocales, deviceTypes, scre
4375
4488
  if (!sourceReferencePath) {
4376
4489
  continue;
4377
4490
  }
4378
- const outputPath = path12.join(
4491
+ const outputPath = path13.join(
4379
4492
  screenshotsDir,
4380
4493
  locale,
4381
4494
  screenshot.type,
4382
4495
  screenshot.filename
4383
4496
  );
4384
- if (skipExisting && fs13.existsSync(outputPath)) {
4497
+ if (skipExisting && fs14.existsSync(outputPath)) {
4385
4498
  continue;
4386
4499
  }
4387
4500
  tasks.push({
@@ -4413,7 +4526,7 @@ async function batchResizeFromRaw(tasks, bgColor, onProgress) {
4413
4526
  };
4414
4527
  onProgress?.(progress);
4415
4528
  try {
4416
- if (!fs13.existsSync(task.rawPath)) {
4529
+ if (!fs14.existsSync(task.rawPath)) {
4417
4530
  progress.status = "skipped";
4418
4531
  onProgress?.(progress);
4419
4532
  skippedCount++;
@@ -4421,9 +4534,9 @@ async function batchResizeFromRaw(tasks, bgColor, onProgress) {
4421
4534
  }
4422
4535
  const targetDimensions = SCREENSHOT_DIMENSIONS[task.deviceType];
4423
4536
  const rawDimensions = await getImageDimensions(task.rawPath);
4424
- const outputDir = path12.dirname(task.outputPath);
4425
- if (!fs13.existsSync(outputDir)) {
4426
- fs13.mkdirSync(outputDir, { recursive: true });
4537
+ const outputDir = path13.dirname(task.outputPath);
4538
+ if (!fs14.existsSync(outputDir)) {
4539
+ fs14.mkdirSync(outputDir, { recursive: true });
4427
4540
  }
4428
4541
  await resizeImage(task.rawPath, task.outputPath, targetDimensions, bgColor);
4429
4542
  progress.status = "completed";
@@ -4650,7 +4763,7 @@ Available locales with raw/: ${allRawLocales.join(", ")}`
4650
4763
  results.push(`
4651
4764
  \u26A0\uFE0F Errors:`);
4652
4765
  for (const err of resizeResult.errors.slice(0, 5)) {
4653
- results.push(` - ${path12.basename(err.path)}: ${err.error}`);
4766
+ results.push(` - ${path13.basename(err.path)}: ${err.error}`);
4654
4767
  }
4655
4768
  if (resizeResult.errors.length > 5) {
4656
4769
  results.push(` ... and ${resizeResult.errors.length - 5} more errors`);
@@ -4674,10 +4787,9 @@ Available locales with raw/: ${allRawLocales.join(", ")}`
4674
4787
  // src/tools/screenshots/phone-to-tablet.ts
4675
4788
  import { z as z9 } from "zod";
4676
4789
  import { zodToJsonSchema as zodToJsonSchema9 } from "zod-to-json-schema";
4677
- import fs14 from "fs";
4678
- import path13 from "path";
4790
+ import fs15 from "fs";
4791
+ import path14 from "path";
4679
4792
  import sharp3 from "sharp";
4680
- import { GoogleGenAI as GoogleGenAI2 } from "@google/genai";
4681
4793
  var TOOL_NAME5 = "phone-to-tablet";
4682
4794
  var TABLET_ASPECT_RATIO = "3:4";
4683
4795
  var phoneToTabletInputSchema = z9.object({
@@ -4698,6 +4810,9 @@ var phoneToTabletInputSchema = z9.object({
4698
4810
  ),
4699
4811
  preserveWords: z9.array(z9.string()).optional().describe(
4700
4812
  'Words to keep exactly as-is in the generated image (e.g., brand names). Example: ["Pabal", "Pro", "AI"]'
4813
+ ),
4814
+ imageModel: z9.enum(GEMINI_IMAGE_MODEL_VALUES).optional().default("flash").describe(
4815
+ "Gemini image model preference. 'flash' (default) is faster/cheaper, 'pro' prioritizes quality."
4701
4816
  )
4702
4817
  });
4703
4818
  var jsonSchema9 = zodToJsonSchema9(phoneToTabletInputSchema, {
@@ -4728,6 +4843,10 @@ Run \`resize-screenshots --deviceTypes tablet\` after this tool to resize images
4728
4843
  - Phone screenshots must exist in: public/products/{slug}/screenshots/{locale}/phone/
4729
4844
  - Locale files must exist in: public/products/{slug}/locales/
4730
4845
 
4846
+ **Model Selection:**
4847
+ - \`imageModel: "flash"\` (default) for speed/cost
4848
+ - \`imageModel: "pro"\` for higher instruction fidelity
4849
+
4731
4850
  **Example output structure:**
4732
4851
  \`\`\`
4733
4852
  public/products/my-app/screenshots/
@@ -4779,14 +4898,14 @@ function buildConversionTasks(slug, phoneScreenshots, locale, screenshotNumbers,
4779
4898
  });
4780
4899
  }
4781
4900
  for (const screenshot of filteredScreenshots) {
4782
- const tabletRawPath = path13.join(
4901
+ const tabletRawPath = path14.join(
4783
4902
  screenshotsDir,
4784
4903
  locale,
4785
4904
  "tablet",
4786
4905
  "raw",
4787
4906
  screenshot.filename
4788
4907
  );
4789
- if (skipExisting && fs14.existsSync(tabletRawPath)) {
4908
+ if (skipExisting && fs15.existsSync(tabletRawPath)) {
4790
4909
  continue;
4791
4910
  }
4792
4911
  tasks.push({
@@ -4798,29 +4917,12 @@ function buildConversionTasks(slug, phoneScreenshots, locale, screenshotNumbers,
4798
4917
  }
4799
4918
  return tasks;
4800
4919
  }
4801
- function getGeminiClient2() {
4802
- const apiKey = getGeminiApiKey();
4803
- return new GoogleGenAI2({ apiKey });
4804
- }
4805
- function readImageAsBase642(imagePath) {
4806
- const buffer = fs14.readFileSync(imagePath);
4807
- const base64 = buffer.toString("base64");
4808
- const ext = path13.extname(imagePath).toLowerCase();
4809
- let mimeType = "image/png";
4810
- if (ext === ".jpg" || ext === ".jpeg") {
4811
- mimeType = "image/jpeg";
4812
- } else if (ext === ".webp") {
4813
- mimeType = "image/webp";
4814
- }
4815
- return { data: base64, mimeType };
4816
- }
4817
- async function convertPhoneToTablet(phonePath, tabletRawPath, preserveWords) {
4818
- try {
4819
- const client = getGeminiClient2();
4820
- const { data: imageData, mimeType } = readImageAsBase642(phonePath);
4821
- const preserveInstruction = preserveWords && preserveWords.length > 0 ? `
4920
+ async function convertPhoneToTablet(phonePath, tabletRawPath, preserveWords, imageModel = "flash") {
4921
+ const client = createGeminiClient();
4922
+ const { data: imageData, mimeType } = readImageAsBase64(phonePath);
4923
+ const preserveInstruction = preserveWords && preserveWords.length > 0 ? `
4822
4924
  - Do NOT change these words, keep them exactly as-is: ${preserveWords.join(", ")}` : "";
4823
- const prompt = `This is a phone app screenshot. Please recreate this screenshot as a TABLET version.
4925
+ const prompt = `This is a phone app screenshot. Please recreate this screenshot as a TABLET version.
4824
4926
 
4825
4927
  IMPORTANT INSTRUCTIONS:
4826
4928
  - Convert this phone UI layout to a tablet-friendly WIDER layout
@@ -4834,67 +4936,32 @@ IMPORTANT INSTRUCTIONS:
4834
4936
  - Keep the same app functionality visible, just optimized for tablet${preserveInstruction}
4835
4937
 
4836
4938
  Generate a new tablet screenshot that represents the same app screen but optimized for tablet display.`;
4837
- const chat = client.chats.create({
4838
- model: "gemini-3-pro-image-preview",
4839
- config: {
4840
- responseModalities: ["TEXT", "IMAGE"]
4841
- }
4842
- });
4843
- const response = await chat.sendMessage({
4844
- message: [
4845
- { text: prompt },
4846
- {
4847
- inlineData: {
4848
- mimeType,
4849
- data: imageData
4850
- }
4851
- }
4852
- ],
4853
- config: {
4854
- responseModalities: ["TEXT", "IMAGE"],
4855
- imageConfig: {
4856
- aspectRatio: TABLET_ASPECT_RATIO
4857
- }
4858
- }
4939
+ try {
4940
+ const generated = await generateImageWithFallback({
4941
+ client,
4942
+ prompt,
4943
+ image: {
4944
+ mimeType,
4945
+ data: imageData
4946
+ },
4947
+ aspectRatio: TABLET_ASPECT_RATIO,
4948
+ imageModel
4859
4949
  });
4860
- const candidates = response.candidates;
4861
- if (!candidates || candidates.length === 0) {
4862
- return {
4863
- success: false,
4864
- error: "No response from Gemini API"
4865
- };
4866
- }
4867
- const parts = candidates[0].content?.parts;
4868
- if (!parts) {
4869
- return {
4870
- success: false,
4871
- error: "No content parts in response"
4872
- };
4873
- }
4874
- for (const part of parts) {
4875
- if (part.inlineData?.data) {
4876
- const imageBuffer = Buffer.from(part.inlineData.data, "base64");
4877
- const outputDir = path13.dirname(tabletRawPath);
4878
- if (!fs14.existsSync(outputDir)) {
4879
- fs14.mkdirSync(outputDir, { recursive: true });
4880
- }
4881
- await sharp3(imageBuffer).png().toFile(tabletRawPath);
4882
- return { success: true };
4883
- }
4950
+ const imageBuffer = Buffer.from(generated.imageBase64, "base64");
4951
+ const outputDir = path14.dirname(tabletRawPath);
4952
+ if (!fs15.existsSync(outputDir)) {
4953
+ fs15.mkdirSync(outputDir, { recursive: true });
4884
4954
  }
4885
- return {
4886
- success: false,
4887
- error: "No image data in Gemini response"
4888
- };
4955
+ await sharp3(imageBuffer).png().toFile(tabletRawPath);
4956
+ return { success: true };
4889
4957
  } catch (error) {
4890
- const message = error instanceof Error ? error.message : String(error);
4891
4958
  return {
4892
4959
  success: false,
4893
- error: message
4960
+ error: error instanceof Error ? error.message : String(error)
4894
4961
  };
4895
4962
  }
4896
4963
  }
4897
- async function convertWithProgress(tasks, onProgress, preserveWords) {
4964
+ async function convertWithProgress(tasks, onProgress, preserveWords, imageModel = "flash") {
4898
4965
  let successful = 0;
4899
4966
  let failed = 0;
4900
4967
  const errors = [];
@@ -4913,7 +4980,8 @@ async function convertWithProgress(tasks, onProgress, preserveWords) {
4913
4980
  const result = await convertPhoneToTablet(
4914
4981
  task.phonePath,
4915
4982
  task.tabletRawPath,
4916
- preserveWords
4983
+ preserveWords,
4984
+ imageModel
4917
4985
  );
4918
4986
  if (result.success) {
4919
4987
  successful++;
@@ -4939,7 +5007,8 @@ async function handlePhoneToTablet(input) {
4939
5007
  screenshotNumbers,
4940
5008
  dryRun = false,
4941
5009
  skipExisting = true,
4942
- preserveWords
5010
+ preserveWords,
5011
+ imageModel = "flash"
4943
5012
  } = input;
4944
5013
  const results = [];
4945
5014
  let appInfo;
@@ -4989,6 +5058,9 @@ async function handlePhoneToTablet(input) {
4989
5058
  }
4990
5059
  }
4991
5060
  results.push(`\u{1F3AF} Locales to process: ${localesToProcess.join(", ")}`);
5061
+ results.push(
5062
+ `\u{1F9E0} Image model: ${imageModel} (${GEMINI_IMAGE_MODEL_PRESETS[imageModel]})`
5063
+ );
4992
5064
  const allTasks = [];
4993
5065
  for (const locale of localesToProcess) {
4994
5066
  const phoneScreenshots = scanLocaleScreenshots(appInfo.slug, locale).filter(
@@ -5086,7 +5158,8 @@ Expected phone screenshots in: ${screenshotsDir2}/{locale}/phone/`
5086
5158
  );
5087
5159
  }
5088
5160
  },
5089
- preserveWords
5161
+ preserveWords,
5162
+ imageModel
5090
5163
  );
5091
5164
  results.push(`
5092
5165
  \u{1F4CA} Conversion Results:`);
@@ -5096,7 +5169,7 @@ Expected phone screenshots in: ${screenshotsDir2}/{locale}/phone/`
5096
5169
  results.push(`
5097
5170
  \u26A0\uFE0F Errors:`);
5098
5171
  for (const err of conversionResult.errors.slice(0, 5)) {
5099
- results.push(` - ${path13.basename(err.path)}: ${err.error}`);
5172
+ results.push(` - ${path14.basename(err.path)}: ${err.error}`);
5100
5173
  }
5101
5174
  if (conversionResult.errors.length > 5) {
5102
5175
  results.push(
@@ -5126,12 +5199,12 @@ Expected phone screenshots in: ${screenshotsDir2}/{locale}/phone/`
5126
5199
  // src/tools/app-icon/generate-app-icons.ts
5127
5200
  import { z as z10 } from "zod";
5128
5201
  import { zodToJsonSchema as zodToJsonSchema10 } from "zod-to-json-schema";
5129
- import fs15 from "fs";
5130
- import path15 from "path";
5202
+ import fs16 from "fs";
5203
+ import path16 from "path";
5131
5204
  import sharp5 from "sharp";
5132
5205
 
5133
5206
  // src/tools/app-icon/utils/icon-specs.util.ts
5134
- import path14 from "path";
5207
+ import path15 from "path";
5135
5208
  var ICON_FILENAMES = {
5136
5209
  BASE: "icon.png",
5137
5210
  IOS_LIGHT: "ios-light.png",
@@ -5172,14 +5245,14 @@ var ALL_ICON_TYPES = Object.keys(
5172
5245
  );
5173
5246
  function getIconsDir(slug, styleFolder) {
5174
5247
  const productsDir = getProductsDir();
5175
- const baseDir = path14.join(productsDir, slug, "icons");
5176
- return styleFolder ? path14.join(baseDir, styleFolder) : baseDir;
5248
+ const baseDir = path15.join(productsDir, slug, "icons");
5249
+ return styleFolder ? path15.join(baseDir, styleFolder) : baseDir;
5177
5250
  }
5178
5251
  function getBaseIconPath(slug, styleFolder) {
5179
- return path14.join(getIconsDir(slug, styleFolder), ICON_FILENAMES.BASE);
5252
+ return path15.join(getIconsDir(slug, styleFolder), ICON_FILENAMES.BASE);
5180
5253
  }
5181
5254
  function getIconOutputPath(slug, iconType, styleFolder) {
5182
- return path14.join(getIconsDir(slug, styleFolder), ICON_SPECS[iconType].filename);
5255
+ return path15.join(getIconsDir(slug, styleFolder), ICON_SPECS[iconType].filename);
5183
5256
  }
5184
5257
 
5185
5258
  // src/tools/app-icon/utils/icon-resizer.util.ts
@@ -5489,11 +5562,11 @@ function validateApp4(appName) {
5489
5562
  );
5490
5563
  }
5491
5564
  const productsDir = getProductsDir();
5492
- const configPath = path15.join(productsDir, app.slug, "config.json");
5565
+ const configPath = path16.join(productsDir, app.slug, "config.json");
5493
5566
  let config;
5494
- if (fs15.existsSync(configPath)) {
5567
+ if (fs16.existsSync(configPath)) {
5495
5568
  try {
5496
- const configData = fs15.readFileSync(configPath, "utf-8");
5569
+ const configData = fs16.readFileSync(configPath, "utf-8");
5497
5570
  config = JSON.parse(configData);
5498
5571
  } catch (error) {
5499
5572
  console.warn(
@@ -5512,7 +5585,7 @@ function buildGenerationTasks(slug, iconTypes, skipExisting, styleFolder) {
5512
5585
  const tasks = [];
5513
5586
  const baseIconPath = getBaseIconPath(slug, styleFolder);
5514
5587
  const iconsDir = getIconsDir(slug, styleFolder);
5515
- if (!fs15.existsSync(baseIconPath)) {
5588
+ if (!fs16.existsSync(baseIconPath)) {
5516
5589
  throw new Error(
5517
5590
  `Base icon not found: ${baseIconPath}
5518
5591
 
@@ -5521,7 +5594,7 @@ Please place your base icon at this location first.`
5521
5594
  }
5522
5595
  for (const iconType of iconTypes) {
5523
5596
  const outputPath = getIconOutputPath(slug, iconType, styleFolder);
5524
- if (skipExisting && fs15.existsSync(outputPath)) {
5597
+ if (skipExisting && fs16.existsSync(outputPath)) {
5525
5598
  continue;
5526
5599
  }
5527
5600
  tasks.push({
@@ -5547,9 +5620,9 @@ async function generateIcons(tasks, backgroundColor, logoAlignment, onProgress)
5547
5620
  };
5548
5621
  onProgress?.(progress);
5549
5622
  try {
5550
- const outputDir = path15.dirname(task.outputPath);
5551
- if (!fs15.existsSync(outputDir)) {
5552
- fs15.mkdirSync(outputDir, { recursive: true });
5623
+ const outputDir = path16.dirname(task.outputPath);
5624
+ if (!fs16.existsSync(outputDir)) {
5625
+ fs16.mkdirSync(outputDir, { recursive: true });
5553
5626
  }
5554
5627
  if (task.iconType === "android-notification-icon") {
5555
5628
  const whiteMask = await convertToWhiteMask(task.inputPath);
@@ -5753,24 +5826,8 @@ import path17 from "path";
5753
5826
  import sharp6 from "sharp";
5754
5827
 
5755
5828
  // src/tools/app-icon/utils/gemini.util.ts
5756
- import fs16 from "fs";
5757
- import path16 from "path";
5758
- import { GoogleGenAI as GoogleGenAI3 } from "@google/genai";
5759
- function getGeminiClient3() {
5760
- const apiKey = getGeminiApiKey();
5761
- return new GoogleGenAI3({ apiKey });
5762
- }
5763
- function readImageAsBase643(imagePath) {
5764
- const buffer = fs16.readFileSync(imagePath);
5765
- const base64 = buffer.toString("base64");
5766
- const ext = path16.extname(imagePath).toLowerCase();
5767
- let mimeType = "image/png";
5768
- if (ext === ".jpg" || ext === ".jpeg") {
5769
- mimeType = "image/jpeg";
5770
- } else if (ext === ".webp") {
5771
- mimeType = "image/webp";
5772
- }
5773
- return { data: base64, mimeType };
5829
+ function getGeminiClient() {
5830
+ return createGeminiClient();
5774
5831
  }
5775
5832
 
5776
5833
  // src/tools/app-icon/stylize-app-icon.ts
@@ -5788,6 +5845,9 @@ var stylizeAppIconInputSchema = z11.object({
5788
5845
  preserveShape: z11.boolean().optional().default(true).describe(
5789
5846
  "Preserve the original icon shape and structure (default: true). When true, only applies style elements without changing the core design."
5790
5847
  ),
5848
+ imageModel: z11.enum(GEMINI_IMAGE_MODEL_VALUES).optional().default("flash").describe(
5849
+ "Gemini image model preference. 'flash' (default) is faster/cheaper, 'pro' prioritizes quality."
5850
+ ),
5791
5851
  dryRun: z11.boolean().optional().default(false).describe(
5792
5852
  "Preview mode - shows what would be generated without actually calling API or saving files"
5793
5853
  )
@@ -5816,6 +5876,10 @@ var stylizeAppIconTool = {
5816
5876
  - GEMINI_API_KEY or GOOGLE_API_KEY environment variable
5817
5877
  - Base icon exists at {slug}/icons/icon.png
5818
5878
 
5879
+ **Model Selection:**
5880
+ - \`imageModel: "flash"\` (default) for speed/cost
5881
+ - \`imageModel: "pro"\` for higher instruction fidelity
5882
+
5819
5883
  **Example Flow:**
5820
5884
  \`\`\`
5821
5885
  Step 1: Stylize icon
@@ -5850,10 +5914,10 @@ function validateApp5(appName) {
5850
5914
  name: app.name || app.slug
5851
5915
  };
5852
5916
  }
5853
- async function stylizeIconWithAI(inputPath, outputPath, stylePrompt, preserveShape) {
5917
+ async function stylizeIconWithAI(inputPath, outputPath, stylePrompt, preserveShape, imageModel = "flash") {
5854
5918
  try {
5855
- const client = getGeminiClient3();
5856
- const { data: imageData, mimeType } = readImageAsBase643(inputPath);
5919
+ const client = getGeminiClient();
5920
+ const { data: imageData, mimeType } = readImageAsBase64(inputPath);
5857
5921
  const shapeInstruction = preserveShape ? "IMPORTANT: Preserve the original icon's shape, structure, and core design elements. Only add style-specific decorations and color adjustments." : "You can modify the icon structure as needed to achieve the style.";
5858
5922
  const fullPrompt = `You are an expert app icon designer. Transform this app icon with the following style:
5859
5923
 
@@ -5870,56 +5934,23 @@ Requirements:
5870
5934
  6. Ensure the result is visually appealing and brand-appropriate
5871
5935
 
5872
5936
  Generate the stylized icon with transparent background now.`;
5873
- const chat = client.chats.create({
5874
- model: "gemini-3-pro-image-preview",
5875
- config: {
5876
- responseModalities: ["TEXT", "IMAGE"]
5877
- }
5878
- });
5879
- const response = await chat.sendMessage({
5880
- message: [
5881
- { text: fullPrompt },
5882
- {
5883
- inlineData: {
5884
- mimeType,
5885
- data: imageData
5886
- }
5887
- }
5888
- ],
5889
- config: {
5890
- responseModalities: ["TEXT", "IMAGE"]
5891
- }
5937
+ const generated = await generateImageWithFallback({
5938
+ client,
5939
+ prompt: fullPrompt,
5940
+ image: {
5941
+ mimeType,
5942
+ data: imageData
5943
+ },
5944
+ imageModel
5892
5945
  });
5893
- const candidates = response.candidates;
5894
- if (!candidates || candidates.length === 0) {
5895
- return {
5896
- success: false,
5897
- error: "No response from Gemini API"
5898
- };
5899
- }
5900
- const parts = candidates[0].content?.parts;
5901
- if (!parts) {
5902
- return {
5903
- success: false,
5904
- error: "No content parts in response"
5905
- };
5906
- }
5907
- for (const part of parts) {
5908
- if (part.inlineData?.data) {
5909
- const imageBuffer = Buffer.from(part.inlineData.data, "base64");
5910
- const outputDir = path17.dirname(outputPath);
5911
- if (!fs17.existsSync(outputDir)) {
5912
- fs17.mkdirSync(outputDir, { recursive: true });
5913
- }
5914
- const processedImage = await sharp6(imageBuffer).ensureAlpha().png().toBuffer();
5915
- await sharp6(processedImage).toFile(outputPath);
5916
- return { success: true };
5917
- }
5918
- }
5919
- return {
5920
- success: false,
5921
- error: "No image data in Gemini response"
5922
- };
5946
+ const imageBuffer = Buffer.from(generated.imageBase64, "base64");
5947
+ const outputDir = path17.dirname(outputPath);
5948
+ if (!fs17.existsSync(outputDir)) {
5949
+ fs17.mkdirSync(outputDir, { recursive: true });
5950
+ }
5951
+ const processedImage = await sharp6(imageBuffer).ensureAlpha().png().toBuffer();
5952
+ await sharp6(processedImage).toFile(outputPath);
5953
+ return { success: true };
5923
5954
  } catch (error) {
5924
5955
  const message = error instanceof Error ? error.message : String(error);
5925
5956
  return {
@@ -5934,6 +5965,7 @@ async function handleStylizeAppIcon(input) {
5934
5965
  styleFolder,
5935
5966
  stylePrompt,
5936
5967
  preserveShape = true,
5968
+ imageModel = "flash",
5937
5969
  dryRun = false
5938
5970
  } = input;
5939
5971
  const results = [];
@@ -5975,6 +6007,9 @@ Please place your base icon at this location first.`
5975
6007
  results.push(
5976
6008
  `\u{1F527} Shape preservation: ${preserveShape ? "enabled" : "disabled"}`
5977
6009
  );
6010
+ results.push(
6011
+ `\u{1F9E0} Image model: ${imageModel} (${GEMINI_IMAGE_MODEL_PRESETS[imageModel]})`
6012
+ );
5978
6013
  if (dryRun) {
5979
6014
  results.push(`
5980
6015
  \u{1F50D} DRY RUN - No actual generation will be performed`);
@@ -5997,7 +6032,7 @@ Next step: Run generate-app-icons with styleFolder='${styleFolder}' to create pl
5997
6032
  };
5998
6033
  }
5999
6034
  try {
6000
- getGeminiClient3();
6035
+ getGeminiClient();
6001
6036
  } catch (error) {
6002
6037
  return {
6003
6038
  content: [
@@ -6014,7 +6049,8 @@ Next step: Run generate-app-icons with styleFolder='${styleFolder}' to create pl
6014
6049
  baseIconPath,
6015
6050
  outputPath,
6016
6051
  stylePrompt,
6017
- preserveShape
6052
+ preserveShape,
6053
+ imageModel
6018
6054
  );
6019
6055
  if (!stylizeResult.success) {
6020
6056
  return {
@@ -6670,6 +6706,12 @@ function getToolZodSchema(name) {
6670
6706
  }
6671
6707
 
6672
6708
  // src/bin/mcp-server.ts
6709
+ console.log = function(...args) {
6710
+ console.error(...args);
6711
+ };
6712
+ console.info = function(...args) {
6713
+ console.error(...args);
6714
+ };
6673
6715
  var server = new Server(
6674
6716
  {
6675
6717
  name: "pabal-resource-mcp",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pabal-resource-mcp",
3
- "version": "1.8.11",
3
+ "version": "1.9.0",
4
4
  "type": "module",
5
5
  "description": "MCP server for ASO data management with shared types and utilities",
6
6
  "author": "skyu",