pabal-resource-mcp 1.8.9 → 1.8.10

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.
@@ -5126,8 +5126,8 @@ Expected phone screenshots in: ${screenshotsDir2}/{locale}/phone/`
5126
5126
  // src/tools/app-icon/generate-app-icons.ts
5127
5127
  import { z as z10 } from "zod";
5128
5128
  import { zodToJsonSchema as zodToJsonSchema10 } from "zod-to-json-schema";
5129
- import fs17 from "fs";
5130
- import path17 from "path";
5129
+ import fs16 from "fs";
5130
+ import path16 from "path";
5131
5131
  import sharp6 from "sharp";
5132
5132
 
5133
5133
  // src/tools/app-icon/utils/icon-specs.util.ts
@@ -5401,97 +5401,21 @@ function calculateFinalPosition(areaLeft, areaTop, areaWidth, areaHeight, logoWi
5401
5401
  }
5402
5402
 
5403
5403
  // src/tools/app-icon/utils/icon-masking.util.ts
5404
- import fs16 from "fs";
5405
- import path16 from "path";
5406
- import sharp5 from "sharp";
5407
-
5408
- // src/tools/app-icon/utils/gemini.util.ts
5409
5404
  import fs15 from "fs";
5410
5405
  import path15 from "path";
5411
- import { GoogleGenAI as GoogleGenAI3 } from "@google/genai";
5412
- function getGeminiClient3() {
5413
- const apiKey = getGeminiApiKey();
5414
- return new GoogleGenAI3({ apiKey });
5415
- }
5416
- function readImageAsBase643(imagePath) {
5417
- const buffer = fs15.readFileSync(imagePath);
5418
- const base64 = buffer.toString("base64");
5419
- const ext = path15.extname(imagePath).toLowerCase();
5420
- let mimeType = "image/png";
5421
- if (ext === ".jpg" || ext === ".jpeg") {
5422
- mimeType = "image/jpeg";
5423
- } else if (ext === ".webp") {
5424
- mimeType = "image/webp";
5425
- }
5426
- return { data: base64, mimeType };
5427
- }
5428
-
5429
- // src/tools/app-icon/utils/icon-masking.util.ts
5430
- async function applyWhiteMasking(inputPath, outputPath, targetSize, logoPosition) {
5406
+ import sharp5 from "sharp";
5407
+ async function applyWhiteMasking(inputPath, outputPath, targetSize) {
5431
5408
  try {
5432
- const client = getGeminiClient3();
5433
- const { data: imageData, mimeType } = readImageAsBase643(inputPath);
5434
- const positionInstruction = logoPosition ? `
5435
- - Logo positioning hint: ${logoPosition}` : "";
5436
- const prompt = `Convert this icon into a simple white Android notification icon.
5437
-
5438
- Requirements:
5439
- - White logo (#FFFFFF) on transparent background
5440
- - Preserve the logo shape exactly
5441
- - No gradients, solid white only${positionInstruction}`;
5442
- const chat = client.chats.create({
5443
- model: "gemini-3-pro-image-preview",
5444
- config: {
5445
- responseModalities: ["TEXT", "IMAGE"]
5446
- }
5447
- });
5448
- const response = await chat.sendMessage({
5449
- message: [
5450
- { text: prompt },
5451
- {
5452
- inlineData: {
5453
- mimeType,
5454
- data: imageData
5455
- }
5456
- }
5457
- ],
5458
- config: {
5459
- responseModalities: ["TEXT", "IMAGE"]
5460
- }
5461
- });
5462
- const candidates = response.candidates;
5463
- if (!candidates || candidates.length === 0) {
5464
- return {
5465
- success: false,
5466
- error: "No response from Gemini API"
5467
- };
5468
- }
5469
- const parts = candidates[0].content?.parts;
5470
- if (!parts) {
5471
- return {
5472
- success: false,
5473
- error: "No content parts in response"
5474
- };
5475
- }
5476
- for (const part of parts) {
5477
- if (part.inlineData?.data) {
5478
- const imageBuffer = Buffer.from(part.inlineData.data, "base64");
5479
- const outputDir = path16.dirname(outputPath);
5480
- if (!fs16.existsSync(outputDir)) {
5481
- fs16.mkdirSync(outputDir, { recursive: true });
5482
- }
5483
- const processedBuffer = await ensureTransparentBackground(imageBuffer);
5484
- await sharp5(processedBuffer).resize(targetSize, targetSize, {
5485
- fit: "contain",
5486
- background: { r: 0, g: 0, b: 0, alpha: 0 }
5487
- }).png().toFile(outputPath);
5488
- return { success: true };
5489
- }
5490
- }
5491
- return {
5492
- success: false,
5493
- error: "No image data in Gemini response"
5494
- };
5409
+ const outputDir = path15.dirname(outputPath);
5410
+ if (!fs15.existsSync(outputDir)) {
5411
+ fs15.mkdirSync(outputDir, { recursive: true });
5412
+ }
5413
+ const processedBuffer = await createWhiteMaskFromOriginal(inputPath);
5414
+ await sharp5(processedBuffer).resize(targetSize, targetSize, {
5415
+ fit: "contain",
5416
+ background: { r: 0, g: 0, b: 0, alpha: 0 }
5417
+ }).png().toFile(outputPath);
5418
+ return { success: true };
5495
5419
  } catch (error) {
5496
5420
  const message = error instanceof Error ? error.message : String(error);
5497
5421
  return {
@@ -5500,16 +5424,12 @@ Requirements:
5500
5424
  };
5501
5425
  }
5502
5426
  }
5503
- async function ensureTransparentBackground(imageBuffer) {
5504
- const { data, info } = await sharp5(imageBuffer).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
5427
+ async function createWhiteMaskFromOriginal(inputPath) {
5428
+ const { data, info } = await sharp5(inputPath).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
5505
5429
  const pixels = new Uint8Array(data.length);
5506
5430
  for (let i = 0; i < data.length; i += 4) {
5507
- const r = data[i];
5508
- const g = data[i + 1];
5509
- const b = data[i + 2];
5510
5431
  const a = data[i + 3];
5511
- const brightness = (r + g + b) / 3;
5512
- if (brightness > 200 && a > 10) {
5432
+ if (a > 10) {
5513
5433
  pixels[i] = 255;
5514
5434
  pixels[i + 1] = 255;
5515
5435
  pixels[i + 2] = 255;
@@ -5565,11 +5485,8 @@ var generateAppIconsInputSchema = z10.object({
5565
5485
  ]).optional().describe(
5566
5486
  "Logo alignment within the canvas (default: center or config default). Affects how the logo is positioned relative to the safe zone."
5567
5487
  ),
5568
- useAiMasking: z10.boolean().optional().default(false).describe(
5569
- "Use Gemini API for intelligent white masking on notification icon (default: false). When false, uses Sharp-based threshold conversion (faster, free, no API key needed). When true, uses Gemini AI for more sophisticated logo extraction (requires GEMINI_API_KEY)."
5570
- ),
5571
- logoPosition: z10.string().optional().describe(
5572
- "Additional prompt for logo positioning when using AI masking (e.g., 'centered', 'slightly above center'). Only used when useAiMasking is true."
5488
+ useAlphaMasking: z10.boolean().optional().default(false).describe(
5489
+ "Use alpha channel for white masking (default: false). When false, uses Sharp-based threshold conversion (converts bright pixels to white). When true, uses original image's alpha channel (preserves exact logo shape)."
5573
5490
  ),
5574
5491
  skipExisting: z10.boolean().optional().default(false).describe("Skip generation if output file already exists (default: false)"),
5575
5492
  dryRun: z10.boolean().optional().default(false).describe(
@@ -5598,20 +5515,20 @@ var generateAppIconsTool = {
5598
5515
  - **Automatic Padding Removal**: Extracts actual logo from icon.png (removes surrounding padding)
5599
5516
  - **Smart Safe Zone Positioning**: Logo automatically fits within platform-specific circles
5600
5517
  - **Flexible Alignment**: Position logo center/left/right/top/bottom relative to safe zone
5601
- - **White Masking**: Sharp-based (default, fast, free) or AI-powered (Gemini, more sophisticated)
5518
+ - **White Masking**: Threshold-based (default) or alpha-based (preserves exact shape)
5602
5519
  - **Custom Background**: Only applies to ios-light.png. Others (adaptive-icon, splash, notification) are transparent
5603
5520
  - **Style Variants**: Generate themed icons (christmas, halloween, etc.) with style-specific defaults
5604
5521
  - **Config Integration**: Uses config.json appIcon settings for default colors and alignment
5605
5522
 
5606
5523
  **White Masking Options:**
5607
- - **Default (useAiMasking=false)**: Sharp threshold conversion - fast, free, no API key needed
5608
- - **AI-Powered (useAiMasking=true)**: Gemini API - more sophisticated, requires GEMINI_API_KEY
5524
+ - **Default (useAlphaMasking=false)**: Threshold conversion - converts bright pixels to white
5525
+ - **Alpha-based (useAlphaMasking=true)**: Uses original alpha channel - preserves exact logo shape
5609
5526
 
5610
5527
  **\u26A0\uFE0F Important: After Generation**
5611
5528
  After generating icons, always check **android-notification-icon.png**:
5612
- - If white masking is not clean (unwanted artifacts, incomplete conversion, or poor contrast)
5613
- - Regenerate with \`useAiMasking: true\` for better AI-powered masking
5614
- - Example: \`{ "appName": "my-app", "useAiMasking": true, "iconTypes": ["android-notification-icon"] }\`
5529
+ - If white masking is not clean (unwanted artifacts or shape issues)
5530
+ - Regenerate with \`useAlphaMasking: true\` for alpha-based masking
5531
+ - Example: \`{ "appName": "my-app", "useAlphaMasking": true, "iconTypes": ["android-notification-icon"] }\`
5615
5532
 
5616
5533
  **Example:**
5617
5534
  \`\`\`
@@ -5620,7 +5537,7 @@ INPUT: my-app/icons/icon.png (source logo with padding)
5620
5537
  OUTPUT: my-app/icons/ios-light.png (logo centered in safe zone)
5621
5538
  my-app/icons/adaptive-icon.png (logo aligned as specified)
5622
5539
  my-app/icons/splash-icon-light.png
5623
- my-app/icons/android-notification-icon.png (white mask, Sharp or AI)
5540
+ my-app/icons/android-notification-icon.png (white mask)
5624
5541
 
5625
5542
  With styleFolder='christmas':
5626
5543
  INPUT: my-app/icons/christmas/icon.png
@@ -5638,11 +5555,11 @@ function validateApp4(appName) {
5638
5555
  );
5639
5556
  }
5640
5557
  const productsDir = getProductsDir();
5641
- const configPath = path17.join(productsDir, app.slug, "config.json");
5558
+ const configPath = path16.join(productsDir, app.slug, "config.json");
5642
5559
  let config;
5643
- if (fs17.existsSync(configPath)) {
5560
+ if (fs16.existsSync(configPath)) {
5644
5561
  try {
5645
- const configData = fs17.readFileSync(configPath, "utf-8");
5562
+ const configData = fs16.readFileSync(configPath, "utf-8");
5646
5563
  config = JSON.parse(configData);
5647
5564
  } catch (error) {
5648
5565
  console.warn(
@@ -5661,7 +5578,7 @@ function buildGenerationTasks(slug, iconTypes, skipExisting, styleFolder) {
5661
5578
  const tasks = [];
5662
5579
  const baseIconPath = getBaseIconPath(slug, styleFolder);
5663
5580
  const iconsDir = getIconsDir(slug, styleFolder);
5664
- if (!fs17.existsSync(baseIconPath)) {
5581
+ if (!fs16.existsSync(baseIconPath)) {
5665
5582
  throw new Error(
5666
5583
  `Base icon not found: ${baseIconPath}
5667
5584
 
@@ -5670,7 +5587,7 @@ Please place your base icon at this location first.`
5670
5587
  }
5671
5588
  for (const iconType of iconTypes) {
5672
5589
  const outputPath = getIconOutputPath(slug, iconType, styleFolder);
5673
- if (skipExisting && fs17.existsSync(outputPath)) {
5590
+ if (skipExisting && fs16.existsSync(outputPath)) {
5674
5591
  continue;
5675
5592
  }
5676
5593
  tasks.push({
@@ -5682,7 +5599,7 @@ Please place your base icon at this location first.`
5682
5599
  }
5683
5600
  return { tasks, baseIconPath };
5684
5601
  }
5685
- async function generateIcons(tasks, backgroundColor, logoAlignment, useAiMasking, logoPosition, onProgress) {
5602
+ async function generateIcons(tasks, backgroundColor, logoAlignment, useAlphaMasking, onProgress) {
5686
5603
  let generatedCount = 0;
5687
5604
  const errors = [];
5688
5605
  const total = tasks.length;
@@ -5696,17 +5613,16 @@ async function generateIcons(tasks, backgroundColor, logoAlignment, useAiMasking
5696
5613
  };
5697
5614
  onProgress?.(progress);
5698
5615
  try {
5699
- const outputDir = path17.dirname(task.outputPath);
5700
- if (!fs17.existsSync(outputDir)) {
5701
- fs17.mkdirSync(outputDir, { recursive: true });
5616
+ const outputDir = path16.dirname(task.outputPath);
5617
+ if (!fs16.existsSync(outputDir)) {
5618
+ fs16.mkdirSync(outputDir, { recursive: true });
5702
5619
  }
5703
5620
  if (task.iconType === "android-notification-icon") {
5704
- if (useAiMasking) {
5621
+ if (useAlphaMasking) {
5705
5622
  const result = await applyWhiteMasking(
5706
5623
  task.inputPath,
5707
5624
  task.outputPath,
5708
- task.spec.size,
5709
- logoPosition
5625
+ task.spec.size
5710
5626
  );
5711
5627
  if (!result.success) {
5712
5628
  throw new Error(result.error || "AI white masking failed");
@@ -5751,8 +5667,7 @@ async function handleGenerateAppIcons(input) {
5751
5667
  styleFolder,
5752
5668
  backgroundColor: bgColorInput,
5753
5669
  logoAlignment: logoAlignmentInput,
5754
- useAiMasking = false,
5755
- logoPosition,
5670
+ useAlphaMasking = false,
5756
5671
  skipExisting = false,
5757
5672
  dryRun = false
5758
5673
  } = input;
@@ -5800,13 +5715,10 @@ async function handleGenerateAppIcons(input) {
5800
5715
  const iconTypes = requestedIconTypes || ALL_ICON_TYPES;
5801
5716
  results.push(`\u{1F3AF} Icon types: ${iconTypes.join(", ")}`);
5802
5717
  results.push(`\u{1F4D0} Logo alignment: ${logoAlignment}`);
5803
- if (useAiMasking) {
5804
- results.push(`\u{1F916} AI masking: enabled (uses Gemini API)`);
5805
- if (logoPosition) {
5806
- results.push(`\u{1F4CD} Logo positioning: ${logoPosition}`);
5807
- }
5718
+ if (useAlphaMasking) {
5719
+ results.push(`\u{1F3AD} White masking: alpha-based (preserves exact shape)`);
5808
5720
  } else {
5809
- results.push(`\u26A1 AI masking: disabled (uses Sharp threshold)`);
5721
+ results.push(`\u{1F3AD} White masking: threshold-based (converts bright pixels)`);
5810
5722
  }
5811
5723
  let tasks;
5812
5724
  let baseIconPath;
@@ -5875,8 +5787,7 @@ async function handleGenerateAppIcons(input) {
5875
5787
  tasks,
5876
5788
  bgColor,
5877
5789
  logoAlignment,
5878
- useAiMasking,
5879
- logoPosition,
5790
+ useAlphaMasking,
5880
5791
  (progress) => {
5881
5792
  const progressPrefix = `[${progress.current}/${progress.total}]`;
5882
5793
  if (progress.status === "generating") {
@@ -5911,16 +5822,16 @@ async function handleGenerateAppIcons(input) {
5911
5822
  const hasNotificationIcon = tasks.some((t) => t.iconType === "android-notification-icon") && !generationResult.errors.some(
5912
5823
  (e) => e.iconType === "android-notification-icon"
5913
5824
  );
5914
- if (hasNotificationIcon && !useAiMasking) {
5825
+ if (hasNotificationIcon && !useAlphaMasking) {
5915
5826
  results.push(
5916
5827
  `
5917
5828
  \u26A0\uFE0F IMPORTANT: Check android-notification-icon.png`
5918
5829
  );
5919
5830
  results.push(
5920
- ` If white masking looks incorrect (artifacts, incomplete conversion, poor contrast):`
5831
+ ` If the shape is not correct, regenerate with alpha masking:`
5921
5832
  );
5922
5833
  results.push(
5923
- ` Regenerate with AI masking: { "appName": "${appInfo.slug}", "useAiMasking": true, "iconTypes": ["android-notification-icon"]${styleFolder ? `, "styleFolder": "${styleFolder}"` : ""} }`
5834
+ ` { "appName": "${appInfo.slug}", "useAlphaMasking": true, "iconTypes": ["android-notification-icon"]${styleFolder ? `, "styleFolder": "${styleFolder}"` : ""} }`
5924
5835
  );
5925
5836
  }
5926
5837
  return {
@@ -5939,6 +5850,29 @@ import { zodToJsonSchema as zodToJsonSchema11 } from "zod-to-json-schema";
5939
5850
  import fs18 from "fs";
5940
5851
  import path18 from "path";
5941
5852
  import sharp7 from "sharp";
5853
+
5854
+ // src/tools/app-icon/utils/gemini.util.ts
5855
+ import fs17 from "fs";
5856
+ import path17 from "path";
5857
+ import { GoogleGenAI as GoogleGenAI3 } from "@google/genai";
5858
+ function getGeminiClient3() {
5859
+ const apiKey = getGeminiApiKey();
5860
+ return new GoogleGenAI3({ apiKey });
5861
+ }
5862
+ function readImageAsBase643(imagePath) {
5863
+ const buffer = fs17.readFileSync(imagePath);
5864
+ const base64 = buffer.toString("base64");
5865
+ const ext = path17.extname(imagePath).toLowerCase();
5866
+ let mimeType = "image/png";
5867
+ if (ext === ".jpg" || ext === ".jpeg") {
5868
+ mimeType = "image/jpeg";
5869
+ } else if (ext === ".webp") {
5870
+ mimeType = "image/webp";
5871
+ }
5872
+ return { data: base64, mimeType };
5873
+ }
5874
+
5875
+ // src/tools/app-icon/stylize-app-icon.ts
5942
5876
  var TOOL_NAME7 = "stylize-app-icon";
5943
5877
  var stylizeAppIconInputSchema = z11.object({
5944
5878
  appName: z11.string().describe(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pabal-resource-mcp",
3
- "version": "1.8.9",
3
+ "version": "1.8.10",
4
4
  "type": "module",
5
5
  "description": "MCP server for ASO data management with shared types and utilities",
6
6
  "author": "skyu",