pabal-web-mcp 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/mcp-server.js +413 -10
- package/dist/index.d.ts +249 -52
- package/package.json +3 -1
package/dist/bin/mcp-server.js
CHANGED
|
@@ -199,7 +199,6 @@ var asoToPublicInputSchema = z.object({
|
|
|
199
199
|
});
|
|
200
200
|
var jsonSchema = toJsonSchema(asoToPublicInputSchema, {
|
|
201
201
|
name: "AsoToPublicInput",
|
|
202
|
-
target: "openApi3",
|
|
203
202
|
$refStrategy: "none"
|
|
204
203
|
});
|
|
205
204
|
var inputSchema = jsonSchema.definitions?.AsoToPublicInput || jsonSchema;
|
|
@@ -304,9 +303,11 @@ async function handleAsoToPublic(input) {
|
|
|
304
303
|
prompt
|
|
305
304
|
});
|
|
306
305
|
const sourcesText = sources.join(" + ");
|
|
307
|
-
conversionPrompts.push(
|
|
306
|
+
conversionPrompts.push(
|
|
307
|
+
`
|
|
308
308
|
--- ${unifiedLocale} (${sourcesText}) ---
|
|
309
|
-
${prompt}`
|
|
309
|
+
${prompt}`
|
|
310
|
+
);
|
|
310
311
|
}
|
|
311
312
|
const pullDataDir = getPullDataDir();
|
|
312
313
|
let responseText = `Converting ASO data from pullData to public/products/${slug}/ structure.
|
|
@@ -517,7 +518,6 @@ var publicToAsoInputSchema = z2.object({
|
|
|
517
518
|
});
|
|
518
519
|
var jsonSchema2 = toJsonSchema2(publicToAsoInputSchema, {
|
|
519
520
|
name: "PublicToAsoInput",
|
|
520
|
-
target: "openApi3",
|
|
521
521
|
$refStrategy: "none"
|
|
522
522
|
});
|
|
523
523
|
var inputSchema2 = jsonSchema2.definitions?.PublicToAsoInput || jsonSchema2;
|
|
@@ -546,9 +546,10 @@ async function downloadScreenshotsToAsoDir(slug, asoData) {
|
|
|
546
546
|
"screenshots",
|
|
547
547
|
googlePlayLocale
|
|
548
548
|
);
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
549
|
+
const phoneScreenshots = localeData.screenshots?.phone;
|
|
550
|
+
if (phoneScreenshots && phoneScreenshots.length > 0) {
|
|
551
|
+
for (let i = 0; i < phoneScreenshots.length; i++) {
|
|
552
|
+
const url = phoneScreenshots[i];
|
|
552
553
|
const filename = `phone-${i + 1}.png`;
|
|
553
554
|
const outputPath = path4.join(asoDir, filename);
|
|
554
555
|
if (isLocalAssetPath(url)) {
|
|
@@ -1609,7 +1610,6 @@ var improvePublicInputSchema = z3.object({
|
|
|
1609
1610
|
});
|
|
1610
1611
|
var jsonSchema3 = toJsonSchema3(improvePublicInputSchema, {
|
|
1611
1612
|
name: "ImprovePublicInput",
|
|
1612
|
-
target: "openApi3",
|
|
1613
1613
|
$refStrategy: "none"
|
|
1614
1614
|
});
|
|
1615
1615
|
var inputSchema3 = jsonSchema3.definitions?.ImprovePublicInput || jsonSchema3;
|
|
@@ -1795,7 +1795,6 @@ var initProjectInputSchema = z4.object({
|
|
|
1795
1795
|
});
|
|
1796
1796
|
var jsonSchema4 = zodToJsonSchema4(initProjectInputSchema, {
|
|
1797
1797
|
name: "InitProjectInput",
|
|
1798
|
-
target: "openApi3",
|
|
1799
1798
|
$refStrategy: "none"
|
|
1800
1799
|
});
|
|
1801
1800
|
var inputSchema4 = jsonSchema4.definitions?.InitProjectInput || jsonSchema4;
|
|
@@ -1889,6 +1888,401 @@ async function handleInitProject(input) {
|
|
|
1889
1888
|
};
|
|
1890
1889
|
}
|
|
1891
1890
|
|
|
1891
|
+
// src/tools/create-blog-html.ts
|
|
1892
|
+
import fs7 from "fs";
|
|
1893
|
+
import path8 from "path";
|
|
1894
|
+
import { z as z5 } from "zod";
|
|
1895
|
+
import { zodToJsonSchema as zodToJsonSchema5 } from "zod-to-json-schema";
|
|
1896
|
+
|
|
1897
|
+
// src/utils/blog.util.ts
|
|
1898
|
+
import path7 from "path";
|
|
1899
|
+
var DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
|
|
1900
|
+
var BLOG_ROOT = "blogs";
|
|
1901
|
+
var removeDiacritics = (value) => value.normalize("NFKD").replace(/[\u0300-\u036f]/g, "");
|
|
1902
|
+
var compact = (items) => (items || []).filter((item) => Boolean(item && item.trim()));
|
|
1903
|
+
function slugifyTitle(title) {
|
|
1904
|
+
const normalized = removeDiacritics(title).toLowerCase().replace(/[^a-z0-9\s-]/g, " ").replace(/[_\s]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
1905
|
+
return normalized || "post";
|
|
1906
|
+
}
|
|
1907
|
+
function normalizeDate(date) {
|
|
1908
|
+
if (date) {
|
|
1909
|
+
if (!DATE_REGEX.test(date)) {
|
|
1910
|
+
throw new Error(
|
|
1911
|
+
`Invalid date format "${date}". Use YYYY-MM-DD (e.g. 2024-09-30).`
|
|
1912
|
+
);
|
|
1913
|
+
}
|
|
1914
|
+
return date;
|
|
1915
|
+
}
|
|
1916
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1917
|
+
}
|
|
1918
|
+
var isKoreanLocale = (locale) => locale.trim().toLowerCase().startsWith("ko");
|
|
1919
|
+
var toPublicBlogBase = (appSlug, slug) => `/${BLOG_ROOT}/${appSlug}/${slug}`;
|
|
1920
|
+
function resolveCoverImagePath(appSlug, slug, coverImage) {
|
|
1921
|
+
if (!coverImage || !coverImage.trim()) {
|
|
1922
|
+
return `/products/${appSlug}/og-image.png`;
|
|
1923
|
+
}
|
|
1924
|
+
const cleaned = coverImage.trim();
|
|
1925
|
+
const relativePath = cleaned.replace(/^\.\//, "");
|
|
1926
|
+
if (!cleaned.startsWith("/") && !/^https?:\/\//.test(cleaned)) {
|
|
1927
|
+
return `${toPublicBlogBase(appSlug, slug)}/${relativePath}`;
|
|
1928
|
+
}
|
|
1929
|
+
if (cleaned.startsWith("./")) {
|
|
1930
|
+
return `${toPublicBlogBase(appSlug, slug)}/${relativePath}`;
|
|
1931
|
+
}
|
|
1932
|
+
return cleaned;
|
|
1933
|
+
}
|
|
1934
|
+
function resolveRelativeImagePath(appSlug, slug, relativePath) {
|
|
1935
|
+
const raw = relativePath?.trim() || "./images/hero.png";
|
|
1936
|
+
const normalized = raw.replace(/^\.\//, "");
|
|
1937
|
+
return {
|
|
1938
|
+
raw,
|
|
1939
|
+
absolute: `${toPublicBlogBase(appSlug, slug)}/${normalized}`
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
function buildDescription(locale, topic, appSlug) {
|
|
1943
|
+
if (isKoreanLocale(locale)) {
|
|
1944
|
+
return `${topic}\uB97C \uC8FC\uC81C\uB85C ${appSlug}\uAC00 ASO\uC640 SEO\uB97C \uC5B4\uB5BB\uAC8C \uC5F0\uACB0\uD558\uACE0 \uBE14\uB85C\uADF8 \uD2B8\uB798\uD53D\uC744 \uC81C\uD488 \uD398\uC774\uC9C0\uB85C \uC774\uC5B4\uC8FC\uB294\uC9C0 \uC815\uB9AC\uD588\uC2B5\uB2C8\uB2E4.`;
|
|
1945
|
+
}
|
|
1946
|
+
return `How ${appSlug} teams turn "${topic}" into a bridge between ASO pages and SEO blogs without losing consistency.`;
|
|
1947
|
+
}
|
|
1948
|
+
function deriveTags(topic, appSlug) {
|
|
1949
|
+
const topicParts = topic.toLowerCase().split(/[^a-z0-9+]+/).filter(Boolean).slice(0, 6);
|
|
1950
|
+
const set = /* @__PURE__ */ new Set([...topicParts, appSlug.toLowerCase(), "blog"]);
|
|
1951
|
+
return Array.from(set);
|
|
1952
|
+
}
|
|
1953
|
+
function buildBlogMeta(options) {
|
|
1954
|
+
const publishedAt = normalizeDate(options.publishedAt);
|
|
1955
|
+
const modifiedAt = normalizeDate(options.modifiedAt || publishedAt);
|
|
1956
|
+
const coverImage = resolveCoverImagePath(
|
|
1957
|
+
options.appSlug,
|
|
1958
|
+
options.slug,
|
|
1959
|
+
options.coverImage
|
|
1960
|
+
);
|
|
1961
|
+
return {
|
|
1962
|
+
title: options.title,
|
|
1963
|
+
description: options.description || buildDescription(options.locale, options.topic, options.appSlug),
|
|
1964
|
+
appSlug: options.appSlug,
|
|
1965
|
+
slug: options.slug,
|
|
1966
|
+
locale: options.locale,
|
|
1967
|
+
publishedAt,
|
|
1968
|
+
modifiedAt,
|
|
1969
|
+
coverImage,
|
|
1970
|
+
tags: compact(options.tags)?.length ? Array.from(
|
|
1971
|
+
new Set(compact(options.tags).map((tag) => tag.toLowerCase()))
|
|
1972
|
+
) : deriveTags(options.topic, options.appSlug)
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
function renderBlogMetaBlock(meta) {
|
|
1976
|
+
const serialized = JSON.stringify(meta, null, 2);
|
|
1977
|
+
return `<!--BLOG_META
|
|
1978
|
+
${serialized}
|
|
1979
|
+
-->`;
|
|
1980
|
+
}
|
|
1981
|
+
function renderEnglishBody(args) {
|
|
1982
|
+
const {
|
|
1983
|
+
meta,
|
|
1984
|
+
topic,
|
|
1985
|
+
appSlug,
|
|
1986
|
+
includeRelativeImageExample,
|
|
1987
|
+
relativeImagePath
|
|
1988
|
+
} = args;
|
|
1989
|
+
const lines = [];
|
|
1990
|
+
lines.push(`<h1>${meta.title}</h1>`);
|
|
1991
|
+
lines.push(
|
|
1992
|
+
`<p>${appSlug} keeps product pages and blogs aligned. This article shows how to use "${topic}" as a shared story so ASO and SEO stay in sync.</p>`
|
|
1993
|
+
);
|
|
1994
|
+
if (includeRelativeImageExample && relativeImagePath) {
|
|
1995
|
+
lines.push(
|
|
1996
|
+
`<img src="${relativeImagePath.raw}" alt="${appSlug} ${topic} cover" />`
|
|
1997
|
+
);
|
|
1998
|
+
lines.push(
|
|
1999
|
+
`<p>The image above is stored next to this file and resolves to <code>${relativeImagePath.absolute}</code> when published.</p>`
|
|
2000
|
+
);
|
|
2001
|
+
}
|
|
2002
|
+
lines.push(`<h2>Why the gap appears</h2>`);
|
|
2003
|
+
lines.push(
|
|
2004
|
+
`<p>ASO pages are tuned for storefronts while SEO posts speak to search crawlers. Teams often duplicate work, drift on messaging, and miss internal links back to /products/${appSlug}.</p>`
|
|
2005
|
+
);
|
|
2006
|
+
lines.push(`<h3>Signals that drift</h3>`);
|
|
2007
|
+
lines.push(
|
|
2008
|
+
`<p>Different headlines, mismatched screenshots, and stale dates make ranking harder. "${topic}" is a strong bridge topic because it touches both acquisition paths.</p>`
|
|
2009
|
+
);
|
|
2010
|
+
lines.push(`<h2>How to bridge with ${appSlug}</h2>`);
|
|
2011
|
+
lines.push(
|
|
2012
|
+
`<p>Start with the product story, then reuse it in blog form. Keep the same core claim, swap storefront keywords for search intent, and reference the canonical product slug.</p>`
|
|
2013
|
+
);
|
|
2014
|
+
lines.push(`<h3>Mini playbook</h3>`);
|
|
2015
|
+
lines.push(
|
|
2016
|
+
`<ul>
|
|
2017
|
+
<li>Reuse the app store hero claim inside the intro.</li>
|
|
2018
|
+
<li>Map ASO keywords to SEO phrases for the "${topic}" angle.</li>
|
|
2019
|
+
<li>Link feature blurbs to product screenshots and changelog notes.</li>
|
|
2020
|
+
<li>Close with a CTA back to <code>/products/${appSlug}</code>.</li>
|
|
2021
|
+
</ul>`
|
|
2022
|
+
);
|
|
2023
|
+
lines.push(`<h2>Example flow to copy</h2>`);
|
|
2024
|
+
lines.push(
|
|
2025
|
+
`<p>Pick one release, outline how it helps with "${topic}", then add a short proof (metric, quote, or screenshot). Keep h2/h3 hierarchy stable so translations stay predictable.</p>`
|
|
2026
|
+
);
|
|
2027
|
+
lines.push(`<h2>Wrap up</h2>`);
|
|
2028
|
+
lines.push(
|
|
2029
|
+
`<p>${appSlug} keeps ASO and SEO talking to each other. Publish this HTML under <code>/blogs/${appSlug}/${meta.slug}/${meta.locale}.html</code> and link it from the product page so traffic flows both ways.</p>`
|
|
2030
|
+
);
|
|
2031
|
+
lines.push(
|
|
2032
|
+
`<p><strong>CTA:</strong> Explore the product page at <a href="/products/${appSlug}">/products/${appSlug}</a>.</p>`
|
|
2033
|
+
);
|
|
2034
|
+
return lines.join("\n\n");
|
|
2035
|
+
}
|
|
2036
|
+
function renderKoreanBody(args) {
|
|
2037
|
+
const {
|
|
2038
|
+
meta,
|
|
2039
|
+
topic,
|
|
2040
|
+
appSlug,
|
|
2041
|
+
includeRelativeImageExample,
|
|
2042
|
+
relativeImagePath
|
|
2043
|
+
} = args;
|
|
2044
|
+
const lines = [];
|
|
2045
|
+
lines.push(`<h1>${meta.title}</h1>`);
|
|
2046
|
+
lines.push(
|
|
2047
|
+
`<p>${appSlug}\uB294 \uC81C\uD488 \uD398\uC774\uC9C0\uC640 \uBE14\uB85C\uADF8\uC758 \uD750\uB984\uC774 \uB04A\uAE30\uC9C0 \uC54A\uB3C4\uB85D "${topic}"\uC744 \uAC19\uC740 \uC774\uC57C\uAE30\uB85C \uBB36\uC5B4\uB0C5\uB2C8\uB2E4. \uC774 \uAE00\uC740 ASO \uC2E0\uD638\uC640 SEO \uCF58\uD150\uCE20\uB97C \uD558\uB098\uC758 \uBA54\uC2DC\uC9C0\uB85C \uC5F0\uACB0\uD558\uB294 \uBC29\uBC95\uC744 \uC124\uBA85\uD569\uB2C8\uB2E4.</p>`
|
|
2048
|
+
);
|
|
2049
|
+
if (includeRelativeImageExample && relativeImagePath) {
|
|
2050
|
+
lines.push(
|
|
2051
|
+
`<img src="${relativeImagePath.raw}" alt="${appSlug} ${topic} \uD45C\uC9C0 \uC774\uBBF8\uC9C0" />`
|
|
2052
|
+
);
|
|
2053
|
+
lines.push(
|
|
2054
|
+
`<p>\uC704 \uC774\uBBF8\uC9C0\uB294 \uAE00\uACFC \uAC19\uC740 \uD3F4\uB354\uC5D0 \uC800\uC7A5\uB418\uC5B4 \uD37C\uBE14\uB9AC\uC2DC \uC2DC <code>${relativeImagePath.absolute}</code> \uACBD\uB85C\uB85C \uB178\uCD9C\uB429\uB2C8\uB2E4.</p>`
|
|
2055
|
+
);
|
|
2056
|
+
}
|
|
2057
|
+
lines.push(`<h2>ASO\uC640 SEO\uAC00 \uAC08\uB77C\uC9C0\uB294 \uC9C0\uC810</h2>`);
|
|
2058
|
+
lines.push(
|
|
2059
|
+
`<p>\uC2A4\uD1A0\uC5B4 \uCD5C\uC801\uD654\uB294 \uC804\uD658\uC5D0 \uC9D1\uC911\uD558\uACE0, \uBE14\uB85C\uADF8\uB294 \uAC80\uC0C9 \uB178\uCD9C\uC744 \uB178\uB9BD\uB2C8\uB2E4. \uAC19\uC740 \uC81C\uD488\uC774\uB77C\uB3C4 \uC81C\uBAA9, \uC2A4\uD06C\uB9B0\uC0F7, \uB0A0\uC9DC\uAC00 \uB2EC\uB77C\uC9C0\uBA74 \uC2E0\uB8B0\uB3C4\uAC00 \uB5A8\uC5B4\uC9C0\uACE0 /products/${appSlug}\uB85C \uC774\uC5B4\uC9C0\uB294 \uB9C1\uD06C\uB3C4 \uC57D\uD574\uC9D1\uB2C8\uB2E4.</p>`
|
|
2060
|
+
);
|
|
2061
|
+
lines.push(`<h3>\uD754\uD788 \uB193\uCE58\uB294 \uC2E0\uD638</h3>`);
|
|
2062
|
+
lines.push(
|
|
2063
|
+
`<p>\uC2A4\uD1A0\uC5B4 \uBA54\uC2DC\uC9C0\uC640 \uBE14\uB85C\uADF8 \uCE74\uD53C\uAC00 \uB530\uB85C \uB180\uAC70\uB098, \uCD9C\uC2DC \uB9E5\uB77D\uC774 \uBE60\uC9C0\uB294 \uACBD\uC6B0\uC785\uB2C8\uB2E4. "${topic}" \uAC19\uC740 \uC8FC\uC81C\uB294 \uB450 \uCC44\uB110\uC744 \uBAA8\uB450 \uAC74\uB4DC\uB9AC\uAE30 \uB54C\uBB38\uC5D0 \uC77C\uAD00\uC131\uC774 \uB354 \uC911\uC694\uD569\uB2C8\uB2E4.</p>`
|
|
2064
|
+
);
|
|
2065
|
+
lines.push(`<h2>${appSlug}\uB85C \uB2E4\uB9AC \uB193\uAE30</h2>`);
|
|
2066
|
+
lines.push(
|
|
2067
|
+
`<p>\uC81C\uD488 \uC774\uC57C\uAE30\uB97C \uBA3C\uC800 \uC815\uB9AC\uD558\uACE0 \uBE14\uB85C\uADF8 \uBC84\uC804\uC73C\uB85C \uC7AC\uC0AC\uC6A9\uD569\uB2C8\uB2E4. \uD575\uC2EC \uC8FC\uC7A5\uACFC \uC99D\uAC70\uB294 \uC720\uC9C0\uD558\uB418, \uAC80\uC0C9 \uC758\uB3C4\uC5D0 \uB9DE\uCDB0 \uD0A4\uC6CC\uB4DC\uC640 \uC608\uC2DC\uB97C \uC870\uC815\uD558\uACE0 \uC81C\uD488 \uC2AC\uB7EC\uADF8\uB97C \uD568\uAED8 \uB178\uCD9C\uD569\uB2C8\uB2E4.</p>`
|
|
2068
|
+
);
|
|
2069
|
+
lines.push(`<h3>\uC801\uC6A9 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8</h3>`);
|
|
2070
|
+
lines.push(
|
|
2071
|
+
`<ul>
|
|
2072
|
+
<li>\uC2A4\uD1A0\uC5B4 \uD5E4\uB4DC\uB77C\uC778\uC744 \uC778\uD2B8\uB85C\uC5D0 \uC7AC\uC0AC\uC6A9\uD558\uACE0 \uB3D9\uC77C\uD55C \uC8FC\uC7A5\uC73C\uB85C \uD480\uC5B4\uAC00\uAE30</li>
|
|
2073
|
+
<li>"${topic}"\uB97C \uC704\uD55C SEO \uD0A4\uC6CC\uB4DC\uB97C ASO \uD0A4\uC6CC\uB4DC\uC640 \uB9E4\uD551\uD558\uAE30</li>
|
|
2074
|
+
<li>\uC2E0\uAE30\uB2A5/\uC2A4\uD06C\uB9B0\uC0F7/\uBCC0\uACBD \uC0AC\uD56D\uC744 \uBE14\uB85C\uADF8 \uBCF8\uBB38\uC5D0 \uC9E7\uAC8C \uC5F0\uACB0\uD558\uAE30</li>
|
|
2075
|
+
<li>\uB9C8\uC9C0\uB9C9 \uBB38\uB2E8\uC5D0\uC11C <code>/products/${appSlug}</code>\uB85C \uC790\uC5F0\uC2A4\uB7EC\uC6B4 CTA \uBC30\uCE58</li>
|
|
2076
|
+
</ul>`
|
|
2077
|
+
);
|
|
2078
|
+
lines.push(`<h2>\uC0AC\uB840 \uD750\uB984 \uC608\uC2DC</h2>`);
|
|
2079
|
+
lines.push(
|
|
2080
|
+
`<p>\uCD5C\uADFC \uB9B4\uB9AC\uC2A4\uB97C \uD558\uB098 \uACE8\uB77C "${topic}"\uACFC \uC5B4\uB5BB\uAC8C \uB9DE\uBB3C\uB9AC\uB294\uC9C0 \uC124\uBA85\uD558\uACE0, \uC22B\uC790\xB7\uC778\uC6A9\xB7\uC2A4\uD06C\uB9B0\uC0F7 \uC911 \uD558\uB098\uB85C \uC99D\uAC70\uB97C \uB0A8\uAE30\uC138\uC694. h2/h3 \uAD6C\uC870\uB97C \uACE0\uC815\uD558\uBA74 \uB2E4\uAD6D\uC5B4 \uD655\uC7A5\uB3C4 \uC218\uC6D4\uD574\uC9D1\uB2C8\uB2E4.</p>`
|
|
2081
|
+
);
|
|
2082
|
+
lines.push(`<h2>\uB9C8\uBB34\uB9AC</h2>`);
|
|
2083
|
+
lines.push(
|
|
2084
|
+
`<p>${appSlug}\uB294 \uBE14\uB85C\uADF8\uC640 \uC81C\uD488 \uD398\uC774\uC9C0\uAC00 \uC11C\uB85C \uD2B8\uB798\uD53D\uC744 \uC8FC\uACE0\uBC1B\uB3C4\uB85D \uC124\uACC4\uD588\uC2B5\uB2C8\uB2E4. \uC774 HTML\uC744 <code>/blogs/${appSlug}/${meta.slug}/${meta.locale}.html</code> \uC704\uCE58\uC5D0 \uC800\uC7A5\uD558\uACE0 \uC81C\uD488 \uC0C1\uC138\uC5D0\uC11C \uB9C1\uD06C\uB97C \uAC78\uC5B4\uB450\uC138\uC694.</p>`
|
|
2085
|
+
);
|
|
2086
|
+
lines.push(
|
|
2087
|
+
`<p><strong>CTA:</strong> \uC81C\uD488 \uD398\uC774\uC9C0 <a href="/products/${appSlug}">/products/${appSlug}</a>\uC5D0\uC11C \uB354 \uC790\uC138\uD788 \uC0B4\uD3B4\uBCF4\uC138\uC694.</p>`
|
|
2088
|
+
);
|
|
2089
|
+
return lines.join("\n\n");
|
|
2090
|
+
}
|
|
2091
|
+
function renderBlogBody(options) {
|
|
2092
|
+
if (isKoreanLocale(options.meta.locale)) {
|
|
2093
|
+
return renderKoreanBody(options);
|
|
2094
|
+
}
|
|
2095
|
+
return renderEnglishBody(options);
|
|
2096
|
+
}
|
|
2097
|
+
function buildBlogHtmlDocument(options) {
|
|
2098
|
+
const metaBlock = renderBlogMetaBlock(options.meta);
|
|
2099
|
+
const body = renderBlogBody({
|
|
2100
|
+
meta: options.meta,
|
|
2101
|
+
topic: options.topic,
|
|
2102
|
+
appSlug: options.appSlug,
|
|
2103
|
+
includeRelativeImageExample: options.includeRelativeImageExample,
|
|
2104
|
+
relativeImagePath: options.relativeImagePath
|
|
2105
|
+
});
|
|
2106
|
+
return `${metaBlock}
|
|
2107
|
+
${body}`;
|
|
2108
|
+
}
|
|
2109
|
+
function resolveTargetLocales(input, defaultLocale = "en-US") {
|
|
2110
|
+
if (input.locales?.length) {
|
|
2111
|
+
const locales = input.locales.map((loc) => loc.trim()).filter(Boolean);
|
|
2112
|
+
return Array.from(new Set(locales));
|
|
2113
|
+
}
|
|
2114
|
+
const fallback = input.locale?.trim() || defaultLocale;
|
|
2115
|
+
return fallback ? [fallback] : [];
|
|
2116
|
+
}
|
|
2117
|
+
function getBlogOutputPaths(options) {
|
|
2118
|
+
const baseDir = path7.join(
|
|
2119
|
+
options.publicDir,
|
|
2120
|
+
BLOG_ROOT,
|
|
2121
|
+
options.appSlug,
|
|
2122
|
+
options.slug
|
|
2123
|
+
);
|
|
2124
|
+
const filePath = path7.join(baseDir, `${options.locale}.html`);
|
|
2125
|
+
const publicBasePath = toPublicBlogBase(options.appSlug, options.slug);
|
|
2126
|
+
return { baseDir, filePath, publicBasePath };
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
// src/tools/create-blog-html.ts
|
|
2130
|
+
var toJsonSchema4 = zodToJsonSchema5;
|
|
2131
|
+
var DATE_REGEX2 = /^\d{4}-\d{2}-\d{2}$/;
|
|
2132
|
+
var createBlogHtmlInputSchema = z5.object({
|
|
2133
|
+
appSlug: z5.string().trim().min(1, "appSlug is required").describe("Product/app slug used for paths and CTAs"),
|
|
2134
|
+
title: z5.string().trim().optional().describe(
|
|
2135
|
+
"English title used for slug (kebab-case). Falls back to topic when omitted."
|
|
2136
|
+
),
|
|
2137
|
+
topic: z5.string().trim().min(1, "topic is required").describe("Topic/angle to write about in the blog body"),
|
|
2138
|
+
locale: z5.string().trim().optional().default("en-US").describe(
|
|
2139
|
+
"Primary locale (default en-US). Ignored when locales[] is set."
|
|
2140
|
+
),
|
|
2141
|
+
locales: z5.array(z5.string().trim().min(1)).optional().describe(
|
|
2142
|
+
"Optional list of locales to generate. Each locale gets its own HTML file."
|
|
2143
|
+
),
|
|
2144
|
+
description: z5.string().trim().optional().describe(
|
|
2145
|
+
"Meta description override. If omitted, the tool generates one from appSlug/topic per locale."
|
|
2146
|
+
),
|
|
2147
|
+
tags: z5.array(z5.string().trim().min(1)).optional().describe(
|
|
2148
|
+
"Optional tags for BLOG_META. Defaults to tags derived from topic."
|
|
2149
|
+
),
|
|
2150
|
+
coverImage: z5.string().trim().optional().describe(
|
|
2151
|
+
"Cover image path. Relative paths rewrite to /blogs/<app>/<slug>/..., default is /products/<appSlug>/og-image.png."
|
|
2152
|
+
),
|
|
2153
|
+
includeRelativeImageExample: z5.boolean().optional().default(false).describe(
|
|
2154
|
+
"Inject a relative image example (./images/hero.png) into the body to demonstrate path rewriting."
|
|
2155
|
+
),
|
|
2156
|
+
relativeImagePath: z5.string().trim().optional().describe(
|
|
2157
|
+
"Override the relative image path (default ./images/hero.png)."
|
|
2158
|
+
),
|
|
2159
|
+
publishedAt: z5.string().trim().regex(DATE_REGEX2, "publishedAt must use YYYY-MM-DD").optional().describe("Publish date (YYYY-MM-DD). Defaults to today."),
|
|
2160
|
+
modifiedAt: z5.string().trim().regex(DATE_REGEX2, "modifiedAt must use YYYY-MM-DD").optional().describe("Last modified date (YYYY-MM-DD). Defaults to publishedAt."),
|
|
2161
|
+
overwrite: z5.boolean().optional().default(false).describe("Overwrite existing files when true (default: false).")
|
|
2162
|
+
}).describe("Generate static HTML blog posts with BLOG_META headers.");
|
|
2163
|
+
var jsonSchema5 = toJsonSchema4(createBlogHtmlInputSchema, {
|
|
2164
|
+
name: "CreateBlogHtmlInput",
|
|
2165
|
+
$refStrategy: "none"
|
|
2166
|
+
});
|
|
2167
|
+
var inputSchema5 = jsonSchema5.definitions?.CreateBlogHtmlInput || jsonSchema5;
|
|
2168
|
+
var createBlogHtmlTool = {
|
|
2169
|
+
name: "create-blog-html",
|
|
2170
|
+
description: `Generate HTML blog posts under public/blogs/<appSlug>/<slug>/<locale>.html with a BLOG_META block.
|
|
2171
|
+
|
|
2172
|
+
Slug rules:
|
|
2173
|
+
- slug = slugify(English title, kebab-case ASCII)
|
|
2174
|
+
- path: public/blogs/<appSlug>/<slug>/<locale>.html
|
|
2175
|
+
- coverImage default: /products/<appSlug>/og-image.png (relative paths are rewritten under /blogs/<app>/<slug>/)
|
|
2176
|
+
- overwrite defaults to false (throws when file exists)
|
|
2177
|
+
|
|
2178
|
+
Template:
|
|
2179
|
+
- Intro connecting topic/app
|
|
2180
|
+
- 3-4 sections (problem \u2192 solution \u2192 tips/examples) using h2/h3
|
|
2181
|
+
- Optional relative image example (./images/hero.png)
|
|
2182
|
+
- Conclusion + CTA linking to /products/<appSlug>
|
|
2183
|
+
|
|
2184
|
+
Supports multiple locales when locales[] is provided (default single locale). Content language follows locale (ko -> Korean, otherwise English).`,
|
|
2185
|
+
inputSchema: inputSchema5
|
|
2186
|
+
};
|
|
2187
|
+
async function handleCreateBlogHtml(input) {
|
|
2188
|
+
const publicDir = getPublicDir();
|
|
2189
|
+
const {
|
|
2190
|
+
appSlug,
|
|
2191
|
+
topic,
|
|
2192
|
+
title,
|
|
2193
|
+
description,
|
|
2194
|
+
tags,
|
|
2195
|
+
coverImage,
|
|
2196
|
+
includeRelativeImageExample = false,
|
|
2197
|
+
relativeImagePath,
|
|
2198
|
+
publishedAt,
|
|
2199
|
+
modifiedAt,
|
|
2200
|
+
overwrite = false
|
|
2201
|
+
} = input;
|
|
2202
|
+
const resolvedTitle = title && title.trim() || topic.trim();
|
|
2203
|
+
const slug = slugifyTitle(resolvedTitle);
|
|
2204
|
+
const targetLocales = resolveTargetLocales(input);
|
|
2205
|
+
if (!targetLocales.length) {
|
|
2206
|
+
throw new Error("At least one locale is required to generate blog HTML.");
|
|
2207
|
+
}
|
|
2208
|
+
const shouldIncludeRelativeImage = includeRelativeImageExample || Boolean(relativeImagePath);
|
|
2209
|
+
const relativeImage = shouldIncludeRelativeImage ? resolveRelativeImagePath(appSlug, slug, relativeImagePath) : void 0;
|
|
2210
|
+
const output = {
|
|
2211
|
+
slug,
|
|
2212
|
+
baseDir: path8.join(publicDir, "blogs", appSlug, slug),
|
|
2213
|
+
files: [],
|
|
2214
|
+
coverImage: coverImage && coverImage.trim().length > 0 ? coverImage.trim() : `/products/${appSlug}/og-image.png`,
|
|
2215
|
+
metaByLocale: {}
|
|
2216
|
+
};
|
|
2217
|
+
const plannedFiles = targetLocales.map(
|
|
2218
|
+
(locale) => getBlogOutputPaths({
|
|
2219
|
+
appSlug,
|
|
2220
|
+
slug,
|
|
2221
|
+
locale,
|
|
2222
|
+
publicDir
|
|
2223
|
+
})
|
|
2224
|
+
);
|
|
2225
|
+
const existing = plannedFiles.filter(
|
|
2226
|
+
({ filePath }) => fs7.existsSync(filePath)
|
|
2227
|
+
);
|
|
2228
|
+
if (existing.length > 0 && !overwrite) {
|
|
2229
|
+
const existingList = existing.map((f) => f.filePath).join("\n- ");
|
|
2230
|
+
throw new Error(
|
|
2231
|
+
`Blog HTML already exists. Set overwrite=true to replace:
|
|
2232
|
+
- ${existingList}`
|
|
2233
|
+
);
|
|
2234
|
+
}
|
|
2235
|
+
fs7.mkdirSync(output.baseDir, { recursive: true });
|
|
2236
|
+
for (const locale of targetLocales) {
|
|
2237
|
+
const { filePath } = getBlogOutputPaths({
|
|
2238
|
+
appSlug,
|
|
2239
|
+
slug,
|
|
2240
|
+
locale,
|
|
2241
|
+
publicDir
|
|
2242
|
+
});
|
|
2243
|
+
const meta = buildBlogMeta({
|
|
2244
|
+
title: resolvedTitle,
|
|
2245
|
+
description,
|
|
2246
|
+
appSlug,
|
|
2247
|
+
slug,
|
|
2248
|
+
locale,
|
|
2249
|
+
topic,
|
|
2250
|
+
coverImage,
|
|
2251
|
+
tags,
|
|
2252
|
+
publishedAt,
|
|
2253
|
+
modifiedAt
|
|
2254
|
+
});
|
|
2255
|
+
output.coverImage = meta.coverImage;
|
|
2256
|
+
output.metaByLocale[locale] = meta;
|
|
2257
|
+
const html = buildBlogHtmlDocument({
|
|
2258
|
+
meta,
|
|
2259
|
+
topic,
|
|
2260
|
+
appSlug,
|
|
2261
|
+
includeRelativeImageExample: shouldIncludeRelativeImage,
|
|
2262
|
+
relativeImagePath: relativeImage
|
|
2263
|
+
});
|
|
2264
|
+
fs7.writeFileSync(filePath, html, "utf-8");
|
|
2265
|
+
output.files.push({ locale, path: filePath });
|
|
2266
|
+
}
|
|
2267
|
+
const summaryLines = [
|
|
2268
|
+
`Created blog HTML for ${appSlug}`,
|
|
2269
|
+
`Slug: ${slug}`,
|
|
2270
|
+
`Locales: ${targetLocales.join(", ")}`,
|
|
2271
|
+
`Cover image: ${output.coverImage}`,
|
|
2272
|
+
"",
|
|
2273
|
+
"Files:",
|
|
2274
|
+
...output.files.map((file) => `- ${file.locale}: ${file.path}`)
|
|
2275
|
+
];
|
|
2276
|
+
return {
|
|
2277
|
+
content: [
|
|
2278
|
+
{
|
|
2279
|
+
type: "text",
|
|
2280
|
+
text: summaryLines.join("\n")
|
|
2281
|
+
}
|
|
2282
|
+
]
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2285
|
+
|
|
1892
2286
|
// src/tools/index.ts
|
|
1893
2287
|
var tools = [
|
|
1894
2288
|
{
|
|
@@ -1922,6 +2316,14 @@ var tools = [
|
|
|
1922
2316
|
zodSchema: initProjectInputSchema,
|
|
1923
2317
|
handler: handleInitProject,
|
|
1924
2318
|
category: "Setup"
|
|
2319
|
+
},
|
|
2320
|
+
{
|
|
2321
|
+
name: createBlogHtmlTool.name,
|
|
2322
|
+
description: createBlogHtmlTool.description,
|
|
2323
|
+
inputSchema: createBlogHtmlTool.inputSchema,
|
|
2324
|
+
zodSchema: createBlogHtmlInputSchema,
|
|
2325
|
+
handler: handleCreateBlogHtml,
|
|
2326
|
+
category: "Content"
|
|
1925
2327
|
}
|
|
1926
2328
|
];
|
|
1927
2329
|
function getToolDefinitions() {
|
|
@@ -1929,7 +2331,8 @@ function getToolDefinitions() {
|
|
|
1929
2331
|
asoToPublicTool,
|
|
1930
2332
|
publicToAsoTool,
|
|
1931
2333
|
improvePublicTool,
|
|
1932
|
-
initProjectTool
|
|
2334
|
+
initProjectTool,
|
|
2335
|
+
createBlogHtmlTool
|
|
1933
2336
|
];
|
|
1934
2337
|
}
|
|
1935
2338
|
function getToolHandler(name) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { androidpublisher_v3 } from '@googleapis/androidpublisher';
|
|
2
|
+
import { AppStoreVersionAttributes, AppStoreVersionLocalizationAttributes, AppInfoLocalizationAttributes, ScreenshotDisplayType } from 'appstore-connect-sdk/openapi';
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Unified locale system for ASO (App Store Optimization)
|
|
3
6
|
* Consolidates App Store Connect and Google Play Console locale codes
|
|
@@ -70,21 +73,32 @@ declare function isAppStoreLocale(locale: string): locale is AppStoreLocale;
|
|
|
70
73
|
* Check if locale is supported by Google Play
|
|
71
74
|
*/
|
|
72
75
|
declare function isGooglePlayLocale(locale: string): locale is GooglePlayLocale;
|
|
76
|
+
/**
|
|
77
|
+
* Google Play Android Publisher base types
|
|
78
|
+
*/
|
|
79
|
+
type GooglePlayListing = androidpublisher_v3.Schema$Listing;
|
|
80
|
+
type GooglePlayImageType = NonNullable<androidpublisher_v3.Params$Resource$Edits$Images$List["imageType"]>;
|
|
81
|
+
type GooglePlayScreenshotType = Extract<GooglePlayImageType, "phoneScreenshots" | "sevenInchScreenshots" | "tenInchScreenshots" | "tvScreenshots" | "wearScreenshots">;
|
|
82
|
+
/**
|
|
83
|
+
* Google Play screenshots keyed by Android Publisher imageType
|
|
84
|
+
* Includes legacy aliases for existing data structures
|
|
85
|
+
*/
|
|
86
|
+
interface GooglePlayScreenshots extends Partial<Record<GooglePlayScreenshotType, string[]>> {
|
|
87
|
+
phone?: string[];
|
|
88
|
+
tablet?: string[];
|
|
89
|
+
tablet7?: string[];
|
|
90
|
+
tablet10?: string[];
|
|
91
|
+
tv?: string[];
|
|
92
|
+
wear?: string[];
|
|
93
|
+
}
|
|
73
94
|
/**
|
|
74
95
|
* Google Play Store ASO data
|
|
75
96
|
*/
|
|
76
|
-
interface GooglePlayAsoData {
|
|
77
|
-
title:
|
|
78
|
-
shortDescription:
|
|
79
|
-
fullDescription:
|
|
80
|
-
screenshots:
|
|
81
|
-
phone: string[];
|
|
82
|
-
tablet?: string[];
|
|
83
|
-
tablet7?: string[];
|
|
84
|
-
tablet10?: string[];
|
|
85
|
-
tv?: string[];
|
|
86
|
-
wear?: string[];
|
|
87
|
-
};
|
|
97
|
+
interface GooglePlayAsoData extends Pick<GooglePlayListing, "video" | "language"> {
|
|
98
|
+
title: NonNullable<GooglePlayListing["title"]>;
|
|
99
|
+
shortDescription: NonNullable<GooglePlayListing["shortDescription"]>;
|
|
100
|
+
fullDescription: NonNullable<GooglePlayListing["fullDescription"]>;
|
|
101
|
+
screenshots: GooglePlayScreenshots;
|
|
88
102
|
featureGraphic?: string;
|
|
89
103
|
promoGraphic?: string;
|
|
90
104
|
category?: string;
|
|
@@ -99,59 +113,57 @@ interface GooglePlayAsoData {
|
|
|
99
113
|
/**
|
|
100
114
|
* Google Play release notes (per version)
|
|
101
115
|
*/
|
|
102
|
-
|
|
103
|
-
versionCode: number;
|
|
104
|
-
versionName: string;
|
|
105
|
-
releaseNotes: {
|
|
106
|
-
[language: string]: string;
|
|
107
|
-
};
|
|
108
|
-
track: string;
|
|
109
|
-
status: string;
|
|
110
|
-
releaseDate?: string;
|
|
111
|
-
}
|
|
116
|
+
type GooglePlayReleaseNote = androidpublisher_v3.Schema$TrackRelease;
|
|
112
117
|
/**
|
|
113
118
|
* App Store release notes (per version)
|
|
114
119
|
*/
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
releaseNotes: {
|
|
118
|
-
[locale: string]: string;
|
|
119
|
-
};
|
|
120
|
-
platform: string;
|
|
120
|
+
type AppStoreReleaseNote = Pick<AppStoreVersionAttributes, "versionString" | "platform"> & {
|
|
121
|
+
releaseNotes: Record<string, NonNullable<AppStoreVersionLocalizationAttributes["whatsNew"]>>;
|
|
121
122
|
releaseDate?: string;
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* App Store Connect base types
|
|
126
|
+
*/
|
|
127
|
+
type AppStoreInfoLocalization = AppInfoLocalizationAttributes;
|
|
128
|
+
type AppStoreVersionLocalization = AppStoreVersionLocalizationAttributes;
|
|
129
|
+
type AppStoreScreenshotDisplayType = ScreenshotDisplayType;
|
|
130
|
+
/**
|
|
131
|
+
* App Store screenshots keyed by official display type
|
|
132
|
+
* Includes legacy aliases used in existing data structures
|
|
133
|
+
*/
|
|
134
|
+
interface AppStoreScreenshots extends Partial<Record<AppStoreScreenshotDisplayType, string[]>> {
|
|
135
|
+
iphone65?: string[];
|
|
136
|
+
iphone61?: string[];
|
|
137
|
+
iphone58?: string[];
|
|
138
|
+
iphone55?: string[];
|
|
139
|
+
iphone47?: string[];
|
|
140
|
+
iphone40?: string[];
|
|
141
|
+
ipadPro129?: string[];
|
|
142
|
+
ipadPro11?: string[];
|
|
143
|
+
ipad105?: string[];
|
|
144
|
+
ipad97?: string[];
|
|
145
|
+
appleWatch?: string[];
|
|
122
146
|
}
|
|
123
147
|
/**
|
|
124
148
|
* App Store ASO data
|
|
125
149
|
*/
|
|
126
150
|
interface AppStoreAsoData {
|
|
127
|
-
name:
|
|
128
|
-
subtitle?:
|
|
129
|
-
description:
|
|
130
|
-
keywords?:
|
|
131
|
-
promotionalText?:
|
|
132
|
-
screenshots:
|
|
133
|
-
iphone65?: string[];
|
|
134
|
-
iphone61?: string[];
|
|
135
|
-
iphone58?: string[];
|
|
136
|
-
iphone55?: string[];
|
|
137
|
-
iphone47?: string[];
|
|
138
|
-
iphone40?: string[];
|
|
139
|
-
ipadPro129?: string[];
|
|
140
|
-
ipadPro11?: string[];
|
|
141
|
-
ipad105?: string[];
|
|
142
|
-
ipad97?: string[];
|
|
143
|
-
appleWatch?: string[];
|
|
144
|
-
};
|
|
151
|
+
name: NonNullable<AppStoreInfoLocalization["name"]>;
|
|
152
|
+
subtitle?: AppStoreInfoLocalization["subtitle"];
|
|
153
|
+
description: NonNullable<AppStoreVersionLocalization["description"]>;
|
|
154
|
+
keywords?: AppStoreVersionLocalization["keywords"];
|
|
155
|
+
promotionalText?: AppStoreVersionLocalization["promotionalText"];
|
|
156
|
+
screenshots: AppStoreScreenshots;
|
|
145
157
|
appPreview?: string[];
|
|
146
158
|
primaryCategory?: string;
|
|
147
159
|
secondaryCategory?: string;
|
|
148
160
|
contentRightId?: string;
|
|
149
|
-
supportUrl?:
|
|
150
|
-
marketingUrl?:
|
|
151
|
-
privacyPolicyUrl?:
|
|
161
|
+
supportUrl?: AppStoreVersionLocalization["supportUrl"];
|
|
162
|
+
marketingUrl?: AppStoreVersionLocalization["marketingUrl"];
|
|
163
|
+
privacyPolicyUrl?: AppStoreInfoLocalization["privacyPolicyUrl"];
|
|
152
164
|
bundleId: string;
|
|
153
|
-
locale:
|
|
154
|
-
whatsNew?:
|
|
165
|
+
locale: NonNullable<AppStoreVersionLocalization["locale"]>;
|
|
166
|
+
whatsNew?: AppStoreVersionLocalization["whatsNew"];
|
|
155
167
|
}
|
|
156
168
|
/**
|
|
157
169
|
* Multilingual Google Play ASO data
|
|
@@ -192,6 +204,61 @@ declare function isGooglePlayMultilingual(data: GooglePlayAsoData | GooglePlayMu
|
|
|
192
204
|
*/
|
|
193
205
|
declare function isAppStoreMultilingual(data: AppStoreAsoData | AppStoreMultilingualAsoData | undefined): data is AppStoreMultilingualAsoData;
|
|
194
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Blog type definitions
|
|
209
|
+
* Used by both MCP tools and web application
|
|
210
|
+
*/
|
|
211
|
+
/**
|
|
212
|
+
* Blog Meta Block (raw from HTML comment)
|
|
213
|
+
* All fields are optional as they may be parsed from HTML or defaults
|
|
214
|
+
*/
|
|
215
|
+
interface BlogMetaBlock {
|
|
216
|
+
title: string;
|
|
217
|
+
description: string;
|
|
218
|
+
appSlug: string;
|
|
219
|
+
slug: string;
|
|
220
|
+
locale?: string;
|
|
221
|
+
coverImage?: string;
|
|
222
|
+
publishedAt?: string;
|
|
223
|
+
modifiedAt?: string;
|
|
224
|
+
tags?: string[];
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Blog Meta (resolved with UnifiedLocale)
|
|
228
|
+
* Used by web application after normalization
|
|
229
|
+
*/
|
|
230
|
+
interface BlogMeta extends BlogMetaBlock {
|
|
231
|
+
locale: UnifiedLocale;
|
|
232
|
+
coverImage?: string;
|
|
233
|
+
publishedAt?: string;
|
|
234
|
+
modifiedAt?: string;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Blog Article (complete parsed article with HTML content)
|
|
238
|
+
*/
|
|
239
|
+
interface BlogArticle {
|
|
240
|
+
appSlug: string;
|
|
241
|
+
slug: string;
|
|
242
|
+
locale: UnifiedLocale;
|
|
243
|
+
html: string;
|
|
244
|
+
meta: BlogMeta;
|
|
245
|
+
filePath: string;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Blog Summary (aggregated metadata across locales)
|
|
249
|
+
*/
|
|
250
|
+
interface BlogSummary {
|
|
251
|
+
appSlug: string;
|
|
252
|
+
slug: string;
|
|
253
|
+
title: string;
|
|
254
|
+
description: string;
|
|
255
|
+
coverImage?: string;
|
|
256
|
+
publishedAt?: string;
|
|
257
|
+
modifiedAt?: string;
|
|
258
|
+
locales: UnifiedLocale[];
|
|
259
|
+
defaultLocale: UnifiedLocale;
|
|
260
|
+
}
|
|
261
|
+
|
|
195
262
|
interface ImageAsset {
|
|
196
263
|
src: string;
|
|
197
264
|
alt: string;
|
|
@@ -354,6 +421,136 @@ interface ProductLocale {
|
|
|
354
421
|
landing?: LandingPageLocale;
|
|
355
422
|
}
|
|
356
423
|
|
|
424
|
+
/**
|
|
425
|
+
* Site data type definitions
|
|
426
|
+
* Structure for public/site/ directory
|
|
427
|
+
* - contacts.json: Contact information
|
|
428
|
+
* - locales/en-US.json: Localized content (meta, hero, developer)
|
|
429
|
+
*/
|
|
430
|
+
interface SiteData {
|
|
431
|
+
meta: {
|
|
432
|
+
title: string;
|
|
433
|
+
description: string;
|
|
434
|
+
};
|
|
435
|
+
hero: {
|
|
436
|
+
brand: string;
|
|
437
|
+
tagline: string;
|
|
438
|
+
subtitle: string;
|
|
439
|
+
};
|
|
440
|
+
contacts: {
|
|
441
|
+
id: string;
|
|
442
|
+
label: string;
|
|
443
|
+
icon: string;
|
|
444
|
+
value: string;
|
|
445
|
+
}[];
|
|
446
|
+
developer: {
|
|
447
|
+
name: string;
|
|
448
|
+
heading: string;
|
|
449
|
+
role: string;
|
|
450
|
+
bio: string;
|
|
451
|
+
principles: string[];
|
|
452
|
+
stack: string[];
|
|
453
|
+
now: string;
|
|
454
|
+
availability: string;
|
|
455
|
+
likes: string[];
|
|
456
|
+
location?: string;
|
|
457
|
+
timezone?: string;
|
|
458
|
+
visited: {
|
|
459
|
+
countries: {
|
|
460
|
+
code: string;
|
|
461
|
+
name: string;
|
|
462
|
+
flagEmoji: string;
|
|
463
|
+
cities: string[];
|
|
464
|
+
}[];
|
|
465
|
+
};
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Types for the create-blog-html MCP tool
|
|
471
|
+
*/
|
|
472
|
+
/**
|
|
473
|
+
* BlogMetaOutput for MCP tool output
|
|
474
|
+
* All fields are required (resolved values)
|
|
475
|
+
* Note: This is separate from blog.types.ts BlogMeta which uses UnifiedLocale
|
|
476
|
+
*/
|
|
477
|
+
interface BlogMetaOutput {
|
|
478
|
+
title: string;
|
|
479
|
+
description: string;
|
|
480
|
+
appSlug: string;
|
|
481
|
+
slug: string;
|
|
482
|
+
locale: string;
|
|
483
|
+
publishedAt: string;
|
|
484
|
+
modifiedAt: string;
|
|
485
|
+
coverImage: string;
|
|
486
|
+
tags: string[];
|
|
487
|
+
}
|
|
488
|
+
interface CreateBlogHtmlInput {
|
|
489
|
+
/**
|
|
490
|
+
* Product/app slug used for paths and CTAs
|
|
491
|
+
*/
|
|
492
|
+
appSlug: string;
|
|
493
|
+
/**
|
|
494
|
+
* English title used for slug creation and H1
|
|
495
|
+
*/
|
|
496
|
+
title?: string;
|
|
497
|
+
/**
|
|
498
|
+
* Topic/angle to cover inside the article body
|
|
499
|
+
*/
|
|
500
|
+
topic: string;
|
|
501
|
+
/**
|
|
502
|
+
* Single locale to generate (default en-US). Ignored when locales[] is provided.
|
|
503
|
+
*/
|
|
504
|
+
locale?: string;
|
|
505
|
+
/**
|
|
506
|
+
* Optional list of locales to generate. Each gets its own HTML file.
|
|
507
|
+
*/
|
|
508
|
+
locales?: string[];
|
|
509
|
+
/**
|
|
510
|
+
* Meta description override. If absent, a locale-aware summary is generated from topic/appSlug.
|
|
511
|
+
*/
|
|
512
|
+
description?: string;
|
|
513
|
+
/**
|
|
514
|
+
* Optional tags for BLOG_META. If absent, tags are derived from topic and appSlug.
|
|
515
|
+
*/
|
|
516
|
+
tags?: string[];
|
|
517
|
+
/**
|
|
518
|
+
* Optional cover image. Relative paths are rewritten to /blogs/<app>/<slug>/...
|
|
519
|
+
*/
|
|
520
|
+
coverImage?: string;
|
|
521
|
+
/**
|
|
522
|
+
* Include a relative image example in the body (./images/hero.png by default).
|
|
523
|
+
*/
|
|
524
|
+
includeRelativeImageExample?: boolean;
|
|
525
|
+
/**
|
|
526
|
+
* Override the relative image path used when includeRelativeImageExample is true.
|
|
527
|
+
*/
|
|
528
|
+
relativeImagePath?: string;
|
|
529
|
+
/**
|
|
530
|
+
* Publish date (YYYY-MM-DD). Defaults to today.
|
|
531
|
+
*/
|
|
532
|
+
publishedAt?: string;
|
|
533
|
+
/**
|
|
534
|
+
* Last modified date (YYYY-MM-DD). Defaults to publishedAt.
|
|
535
|
+
*/
|
|
536
|
+
modifiedAt?: string;
|
|
537
|
+
/**
|
|
538
|
+
* Overwrite existing HTML files when true (default false).
|
|
539
|
+
*/
|
|
540
|
+
overwrite?: boolean;
|
|
541
|
+
}
|
|
542
|
+
interface GeneratedBlogFile {
|
|
543
|
+
locale: string;
|
|
544
|
+
path: string;
|
|
545
|
+
}
|
|
546
|
+
interface CreateBlogHtmlResult {
|
|
547
|
+
slug: string;
|
|
548
|
+
baseDir: string;
|
|
549
|
+
files: GeneratedBlogFile[];
|
|
550
|
+
coverImage: string;
|
|
551
|
+
metaByLocale: Record<string, BlogMetaOutput>;
|
|
552
|
+
}
|
|
553
|
+
|
|
357
554
|
/**
|
|
358
555
|
* Locale conversion utilities for ASO platforms
|
|
359
556
|
* Handles conversion between unified locales and platform-specific locale codes
|
|
@@ -609,4 +806,4 @@ declare function getPublicDir(): string;
|
|
|
609
806
|
*/
|
|
610
807
|
declare function getProductsDir(): string;
|
|
611
808
|
|
|
612
|
-
export { APP_STORE_TO_UNIFIED, type AppMetaLinks, type AppPageData, type AppStoreAsoData, type AppStoreLocale, type AppStoreMultilingualAsoData, type AppStoreReleaseNote, type AsoData, type AsoLocaleContent, type AsoTemplate, DEFAULT_LOCALE, type DeepPartial, type FeatureItem, GOOGLE_PLAY_TO_UNIFIED, type GooglePlayAsoData, type GooglePlayLocale, type GooglePlayMultilingualAsoData, type GooglePlayReleaseNote, type ImageAsset, type LandingCta, type LandingFeatures, type LandingHero, type LandingPage, type LandingPageLocale, type LandingReviews, type LandingScreenshots, type LayoutColors, type ProductConfig, type ProductContent, type ProductLocale, type ProductMetadata, type ProductScreenshots, type SupportedLocale, type Testimonial, UNIFIED_LOCALES, UNIFIED_TO_APP_STORE, UNIFIED_TO_GOOGLE_PLAY, type UnifiedLocale, appStoreToGooglePlay, appStoreToUnified, appStoreToUnifiedBatch, convertObjectFromAppStore, convertObjectFromGooglePlay, convertObjectToAppStore, convertObjectToGooglePlay, getAsoDataDir, getProductsDir, getPublicDir, getPullDataDir, getPushDataDir, googlePlayToAppStore, googlePlayToUnified, googlePlayToUnifiedBatch, isAppStoreLocale, isAppStoreMultilingual, isGooglePlayLocale, isGooglePlayMultilingual, isSupportedLocale, loadAsoFromConfig, saveAsoToAsoDir, saveAsoToConfig, unifiedToAppStore, unifiedToAppStoreBatch, unifiedToBothPlatforms, unifiedToGooglePlay, unifiedToGooglePlayBatch };
|
|
809
|
+
export { APP_STORE_TO_UNIFIED, type AppMetaLinks, type AppPageData, type AppStoreAsoData, type AppStoreInfoLocalization, type AppStoreLocale, type AppStoreMultilingualAsoData, type AppStoreReleaseNote, type AppStoreScreenshotDisplayType, type AppStoreScreenshots, type AppStoreVersionLocalization, type AsoData, type AsoLocaleContent, type AsoTemplate, type BlogArticle, type BlogMeta, type BlogMetaBlock, type BlogMetaOutput, type BlogSummary, type CreateBlogHtmlInput, type CreateBlogHtmlResult, DEFAULT_LOCALE, type DeepPartial, type FeatureItem, GOOGLE_PLAY_TO_UNIFIED, type GeneratedBlogFile, type GooglePlayAsoData, type GooglePlayImageType, type GooglePlayListing, type GooglePlayLocale, type GooglePlayMultilingualAsoData, type GooglePlayReleaseNote, type GooglePlayScreenshotType, type GooglePlayScreenshots, type ImageAsset, type LandingCta, type LandingFeatures, type LandingHero, type LandingPage, type LandingPageLocale, type LandingReviews, type LandingScreenshots, type LayoutColors, type ProductConfig, type ProductContent, type ProductLocale, type ProductMetadata, type ProductScreenshots, type SiteData, type SupportedLocale, type Testimonial, UNIFIED_LOCALES, UNIFIED_TO_APP_STORE, UNIFIED_TO_GOOGLE_PLAY, type UnifiedLocale, appStoreToGooglePlay, appStoreToUnified, appStoreToUnifiedBatch, convertObjectFromAppStore, convertObjectFromGooglePlay, convertObjectToAppStore, convertObjectToGooglePlay, getAsoDataDir, getProductsDir, getPublicDir, getPullDataDir, getPushDataDir, googlePlayToAppStore, googlePlayToUnified, googlePlayToUnifiedBatch, isAppStoreLocale, isAppStoreMultilingual, isGooglePlayLocale, isGooglePlayMultilingual, isSupportedLocale, loadAsoFromConfig, saveAsoToAsoDir, saveAsoToConfig, unifiedToAppStore, unifiedToAppStoreBatch, unifiedToBothPlatforms, unifiedToGooglePlay, unifiedToGooglePlayBatch };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pabal-web-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MCP server for ASO data management with shared types and utilities",
|
|
6
6
|
"author": "skyu",
|
|
@@ -39,7 +39,9 @@
|
|
|
39
39
|
"prepublishOnly": "npm run build"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
+
"@googleapis/androidpublisher": "^33.2.0",
|
|
42
43
|
"@modelcontextprotocol/sdk": "^1.22.0",
|
|
44
|
+
"appstore-connect-sdk": "^1.3.2",
|
|
43
45
|
"zod": "^3.25.76",
|
|
44
46
|
"zod-to-json-schema": "^3.25.0"
|
|
45
47
|
},
|