@charisol/plexo-mcp 1.0.6 → 1.0.8
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/blogTools.js +690 -0
- package/dist/client/plexoClient.js +220 -0
- package/dist/index.js +235 -3
- package/package.json +1 -1
- package/src/blogTools.ts +695 -0
- package/src/client/plexoClient.ts +276 -0
- package/src/index.ts +242 -3
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BLOG_TOOL_NAMES = exports.BLOG_TOOL_DEFS = void 0;
|
|
4
|
+
exports.handleBlogToolCall = handleBlogToolCall;
|
|
5
|
+
const BLOG_LAYOUT_EXAMPLE = `
|
|
6
|
+
EXAMPLE — a single-post layout with a hero image, title, meta line, and content:
|
|
7
|
+
{
|
|
8
|
+
"body": {
|
|
9
|
+
"style": { "backgroundColor": "#ffffff", "color": "#1e293b", "fontFamily": "Inter, sans-serif", "htmlTitle": "Blog Post" },
|
|
10
|
+
"rows": [
|
|
11
|
+
{ "id": "row-hero", "style": { "paddingTop": "0px" }, "columns": [
|
|
12
|
+
{ "id": "col-hero", "width": "100%", "elements": [
|
|
13
|
+
{ "id": "el-featured", "type": "blog_featured_image", "style": { "width": "100%", "maxHeight": "420px", "objectFit": "cover" }, "attributes": {} }
|
|
14
|
+
] }
|
|
15
|
+
] },
|
|
16
|
+
{ "id": "row-meta", "style": { "paddingTop": "32px", "paddingBottom": "8px" }, "columns": [
|
|
17
|
+
{ "id": "col-title", "width": "100%", "elements": [
|
|
18
|
+
{ "id": "el-title", "type": "blog_title", "style": { "fontSize": "40px", "fontWeight": "800" }, "attributes": {} },
|
|
19
|
+
{ "id": "el-authordate", "type": "blog_author", "style": { "fontSize": "14px", "color": "#64748b", "display": "inline-block", "marginRight": "12px" }, "attributes": {} },
|
|
20
|
+
{ "id": "el-date", "type": "blog_date", "style": { "fontSize": "14px", "color": "#64748b", "display": "inline-block" }, "attributes": {} }
|
|
21
|
+
] }
|
|
22
|
+
] },
|
|
23
|
+
{ "id": "row-body", "style": { "paddingTop": "24px", "paddingBottom": "48px" }, "columns": [
|
|
24
|
+
{ "id": "col-body", "width": "100%", "elements": [
|
|
25
|
+
{ "id": "el-content", "type": "blog_content", "style": { "fontSize": "17px", "lineHeight": "1.7" }, "attributes": {} },
|
|
26
|
+
{ "id": "el-comments", "type": "blog_comments", "style": { "marginTop": "48px" }, "attributes": {} }
|
|
27
|
+
] }
|
|
28
|
+
] }
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
EXAMPLE — a listing layout with a 3-column post grid:
|
|
34
|
+
{
|
|
35
|
+
"body": {
|
|
36
|
+
"style": { "backgroundColor": "#ffffff", "color": "#1e293b", "fontFamily": "Inter, sans-serif", "htmlTitle": "Blog" },
|
|
37
|
+
"rows": [
|
|
38
|
+
{ "id": "row-heading", "style": { "paddingTop": "48px", "paddingBottom": "16px" }, "columns": [
|
|
39
|
+
{ "id": "col-heading", "width": "100%", "elements": [
|
|
40
|
+
{ "id": "el-heading", "type": "heading", "style": { "fontSize": "36px", "fontWeight": "800", "textAlign": "center" }, "attributes": { "text": "Latest Posts" } }
|
|
41
|
+
] }
|
|
42
|
+
] },
|
|
43
|
+
{ "id": "row-list", "style": { "paddingBottom": "48px" }, "columns": [
|
|
44
|
+
{ "id": "col-list", "width": "100%", "elements": [
|
|
45
|
+
{ "id": "el-postlist", "type": "blog_post_list", "style": { "gridColumns": "3" }, "attributes": {} }
|
|
46
|
+
] }
|
|
47
|
+
] }
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
}`;
|
|
51
|
+
exports.BLOG_TOOL_DEFS = [
|
|
52
|
+
{
|
|
53
|
+
name: "get_blog_layout",
|
|
54
|
+
description: `Fetches a blog site's current custom layout (its designJson) for either the single-post page or the listing/index page — or reports that none has been designed yet (the site falls back to the default built-in blog theme until one is). Call this BEFORE design_blog_layout if you want to edit an existing layout rather than start blank, since design_blog_layout replaces the ENTIRE designJson.`,
|
|
55
|
+
inputSchema: {
|
|
56
|
+
type: "object",
|
|
57
|
+
properties: {
|
|
58
|
+
templateId: { type: "string", description: "The blog site's home page template id (a root/parentless landing page — see list_landing_pages). BlogSite rows are always scoped to this id, never a sub-page." },
|
|
59
|
+
kind: { type: "string", enum: ["post", "listing"], description: "Which layout to fetch: 'post' for the single blog post page, 'listing' for the blog index/archive page." },
|
|
60
|
+
},
|
|
61
|
+
required: ["templateId", "kind"],
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "design_blog_layout",
|
|
66
|
+
description: `Designs (creates if none exists yet, or fully replaces if one does) the custom builder layout for a blog site's single-post page or listing/index page. This is how the AI can give a blog its own branded look instead of the default built-in theme.
|
|
67
|
+
|
|
68
|
+
Uses the SAME fully-hydrated layout schema as publish_landing_page (rows/columns/elements) — designJson MUST be { "body": { "style": {...}, "rows": [...] } }, no shorthand. In addition to the ordinary element types, this layout may (and for the required one below, MUST) use these blog placeholder marker types, each resolved per-post/per-listing at render time and never carrying real content or meaningful attributes (still include "attributes": {}):
|
|
69
|
+
- blog_title, blog_content, blog_featured_image, blog_date, blog_author, blog_categories, blog_comments — for kind 'post' only. blog_content is REQUIRED (the layout is otherwise ignored and the site falls back to the default theme) and should appear exactly once; blog_title should also normally appear exactly once; the rest are optional, usually single-use.
|
|
70
|
+
- blog_post_list — for kind 'listing' only. This is REQUIRED for a listing layout (same fallback behavior if missing) and is the ONLY blog placeholder valid on this kind — it renders the whole paginated grid/list of posts itself, so blog_title/blog_content/etc. do not belong on a listing layout.
|
|
71
|
+
This replaces the ENTIRE designJson — call get_blog_layout first to fetch the current one if you want to edit rather than replace.
|
|
72
|
+
${BLOG_LAYOUT_EXAMPLE}`,
|
|
73
|
+
inputSchema: {
|
|
74
|
+
type: "object",
|
|
75
|
+
properties: {
|
|
76
|
+
templateId: { type: "string", description: "The blog site's home page template id (see list_landing_pages)." },
|
|
77
|
+
kind: { type: "string", enum: ["post", "listing"], description: "Which layout to design: 'post' for the single blog post page, 'listing' for the blog index/archive page." },
|
|
78
|
+
name: { type: "string", description: "Optional name for the underlying layout template (shown in the dashboard). Defaults to 'Blog Post Layout'/'Blog Listing Layout'." },
|
|
79
|
+
designJson: {
|
|
80
|
+
type: "object",
|
|
81
|
+
description: "The FULL Plexo layout schema — see this tool's description for the required marker element and a worked example per kind.",
|
|
82
|
+
required: ["body"],
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
required: ["templateId", "kind", "designJson"],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "reset_blog_layout",
|
|
90
|
+
description: "Detaches a blog site's custom post or listing layout, reverting it to the default built-in blog theme. The layout Template itself is NOT deleted — it stays editable and can be reattached later by calling design_blog_layout again (which reuses it rather than creating a new one).",
|
|
91
|
+
inputSchema: {
|
|
92
|
+
type: "object",
|
|
93
|
+
properties: {
|
|
94
|
+
templateId: { type: "string", description: "The blog site's home page template id." },
|
|
95
|
+
kind: { type: "string", enum: ["post", "listing"], description: "Which layout to detach." },
|
|
96
|
+
},
|
|
97
|
+
required: ["templateId", "kind"],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "get_blog_settings",
|
|
102
|
+
description: "Fetches a blog site's settings: whether the blog is enabled/live, its title/description, posts-per-page, whether it replaces the site's homepage, and its appearance (accent color, font preset, logo, header image, comments toggle).",
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: "object",
|
|
105
|
+
properties: { templateId: { type: "string", description: "The blog site's home page template id." } },
|
|
106
|
+
required: ["templateId"],
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "update_blog_settings",
|
|
111
|
+
description: "Updates a blog site's settings. The blog is NOT publicly visible at /blog until 'enabled' is set to true — call this with enabled: true before publishing posts if the user wants the blog live. Only the fields provided are changed; omit any field to leave it as-is.",
|
|
112
|
+
inputSchema: {
|
|
113
|
+
type: "object",
|
|
114
|
+
properties: {
|
|
115
|
+
templateId: { type: "string", description: "The blog site's home page template id." },
|
|
116
|
+
enabled: { type: "boolean", description: "Whether the blog is publicly visible at /blog." },
|
|
117
|
+
title: { type: "string", description: "Blog title (shown in headers/SEO)." },
|
|
118
|
+
description: { type: "string", description: "Blog description/tagline." },
|
|
119
|
+
postsPerPage: { type: "number", description: "Posts per listing page, 1-50." },
|
|
120
|
+
showOnHomepage: { type: "boolean", description: "If true, the blog listing replaces the site's own root page ('/') instead of living at '/blog'." },
|
|
121
|
+
accentColor: { type: "string", description: "Hex accent color for the default theme (e.g. '#6d28d9'). Ignored if a custom design_blog_layout has been designed." },
|
|
122
|
+
fontPreset: { type: "string", enum: ["sans", "serif", "modern", "elegant"], description: "Font preset for the default theme." },
|
|
123
|
+
logoUrl: { type: "string", description: "Logo image URL for the default theme." },
|
|
124
|
+
headerImageUrl: { type: "string", description: "Header/banner image URL for the default theme." },
|
|
125
|
+
commentsEnabled: { type: "boolean", description: "Sitewide comments master switch (a post's own commentsEnabled must also be true for its comments to show)." },
|
|
126
|
+
},
|
|
127
|
+
required: ["templateId"],
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: "list_blog_posts",
|
|
132
|
+
description: "Lists a blog site's posts (up to 200, most recently updated first), with optional status/search/category filters.",
|
|
133
|
+
inputSchema: {
|
|
134
|
+
type: "object",
|
|
135
|
+
properties: {
|
|
136
|
+
templateId: { type: "string", description: "The blog site's home page template id." },
|
|
137
|
+
status: { type: "string", enum: ["DRAFT", "SCHEDULED", "PUBLISHED", "ARCHIVED"], description: "Optional status filter." },
|
|
138
|
+
search: { type: "string", description: "Optional case-insensitive title search." },
|
|
139
|
+
categorySlug: { type: "string", description: "Optional category slug filter (see list_blog_categories)." },
|
|
140
|
+
},
|
|
141
|
+
required: ["templateId"],
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: "get_blog_post",
|
|
146
|
+
description: "Fetches one blog post's full details (content, SEO fields, categories, tags, author) by id or slug.",
|
|
147
|
+
inputSchema: {
|
|
148
|
+
type: "object",
|
|
149
|
+
properties: {
|
|
150
|
+
templateId: { type: "string", description: "The blog site's home page template id." },
|
|
151
|
+
postId: { type: "string", description: "The post's id. Provide this or slug." },
|
|
152
|
+
slug: { type: "string", description: "The post's URL slug. Provide this or postId." },
|
|
153
|
+
},
|
|
154
|
+
required: ["templateId"],
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: "create_blog_post",
|
|
159
|
+
description: `Creates and saves a new blog post. Content is written as plain semantic HTML (contentHtml) — <p>, <h2>-<h4>, <ul>/<ol>/<li>, <a href>, <img src alt>, <strong>, <em>, <blockquote> — NOT raw ProseMirror/Tiptap JSON; the server converts it automatically. A DRAFT post is saved but not publicly visible until its status is PUBLISHED (or the blog itself isn't enabled yet — see update_blog_settings) — set status to "PUBLISHED" to publish immediately, or omit it to save as a draft.
|
|
160
|
+
|
|
161
|
+
categoryNames/tagNames/authorName are plain names, not ids — matching ones are reused, unmatched ones are created automatically (same as typing a new category/tag/author into the dashboard editor). Use list_blog_categories/list_blog_tags/list_blog_authors first only if you need to see what already exists.`,
|
|
162
|
+
inputSchema: {
|
|
163
|
+
type: "object",
|
|
164
|
+
properties: {
|
|
165
|
+
templateId: { type: "string", description: "The blog site's home page template id." },
|
|
166
|
+
title: { type: "string", description: "Post title." },
|
|
167
|
+
slug: { type: "string", description: "Optional URL slug. Auto-generated from the title if omitted; auto-suffixed on collision." },
|
|
168
|
+
excerpt: { type: "string", description: "Optional short summary shown on the listing page. Auto-generated from content if omitted." },
|
|
169
|
+
contentHtml: { type: "string", description: "Post body as semantic HTML (see this tool's description for supported tags)." },
|
|
170
|
+
featuredImageUrl: { type: "string", description: "Optional featured/hero image URL." },
|
|
171
|
+
featuredImageAlt: { type: "string", description: "Optional alt text for the featured image." },
|
|
172
|
+
status: { type: "string", enum: ["DRAFT", "SCHEDULED", "PUBLISHED", "ARCHIVED"], description: "Defaults to DRAFT if omitted." },
|
|
173
|
+
scheduledAt: { type: "string", description: "ISO datetime to auto-publish at — only used when status is SCHEDULED." },
|
|
174
|
+
metaTitle: { type: "string", description: "Optional SEO meta title. Falls back to the post title." },
|
|
175
|
+
metaDescription: { type: "string", description: "Optional SEO meta description. Falls back to the excerpt." },
|
|
176
|
+
ogImageUrl: { type: "string", description: "Optional Open Graph share image URL. Falls back to the featured image." },
|
|
177
|
+
noindex: { type: "boolean", description: "If true, adds a noindex meta tag (hides the post from search engines)." },
|
|
178
|
+
authorName: { type: "string", description: "Optional byline name — reused if an author with this name exists, created otherwise." },
|
|
179
|
+
categoryNames: { type: "array", items: { type: "string" }, description: "Optional category names — reused/created as needed." },
|
|
180
|
+
tagNames: { type: "array", items: { type: "string" }, description: "Optional tag names — reused/created as needed." },
|
|
181
|
+
},
|
|
182
|
+
required: ["templateId", "title"],
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: "update_blog_post",
|
|
187
|
+
description: "Edits an existing blog post in place. Only the fields provided are changed — omit any field to leave it as-is. Same contentHtml/categoryNames/tagNames/authorName conventions as create_blog_post (plain HTML in, names not ids).",
|
|
188
|
+
inputSchema: {
|
|
189
|
+
type: "object",
|
|
190
|
+
properties: {
|
|
191
|
+
templateId: { type: "string", description: "The blog site's home page template id." },
|
|
192
|
+
postId: { type: "string", description: "The post to update." },
|
|
193
|
+
title: { type: "string" },
|
|
194
|
+
slug: { type: "string" },
|
|
195
|
+
excerpt: { type: "string" },
|
|
196
|
+
contentHtml: { type: "string", description: "Post body as semantic HTML — replaces the entire body if provided." },
|
|
197
|
+
featuredImageUrl: { type: "string" },
|
|
198
|
+
featuredImageAlt: { type: "string" },
|
|
199
|
+
status: { type: "string", enum: ["DRAFT", "SCHEDULED", "PUBLISHED", "ARCHIVED"] },
|
|
200
|
+
scheduledAt: { type: "string", description: "ISO datetime — only used when status is SCHEDULED." },
|
|
201
|
+
metaTitle: { type: "string" },
|
|
202
|
+
metaDescription: { type: "string" },
|
|
203
|
+
ogImageUrl: { type: "string" },
|
|
204
|
+
noindex: { type: "boolean" },
|
|
205
|
+
authorName: { type: "string", description: "Byline name — reused/created as needed." },
|
|
206
|
+
categoryNames: { type: "array", items: { type: "string" }, description: "FULL replacement category list — reused/created as needed." },
|
|
207
|
+
tagNames: { type: "array", items: { type: "string" }, description: "FULL replacement tag list — reused/created as needed." },
|
|
208
|
+
},
|
|
209
|
+
required: ["templateId", "postId"],
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: "delete_blog_post",
|
|
214
|
+
description: "Permanently deletes a blog post. Cannot be undone — confirm with the user first.",
|
|
215
|
+
inputSchema: {
|
|
216
|
+
type: "object",
|
|
217
|
+
properties: {
|
|
218
|
+
templateId: { type: "string", description: "The blog site's home page template id." },
|
|
219
|
+
postId: { type: "string", description: "The post to delete." },
|
|
220
|
+
},
|
|
221
|
+
required: ["templateId", "postId"],
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
name: "list_blog_categories",
|
|
226
|
+
description: "Lists a blog site's categories.",
|
|
227
|
+
inputSchema: {
|
|
228
|
+
type: "object",
|
|
229
|
+
properties: { templateId: { type: "string", description: "The blog site's home page template id." } },
|
|
230
|
+
required: ["templateId"],
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: "create_blog_category",
|
|
235
|
+
description: "Creates a blog category. Categories/tags/authors are usually better created implicitly via create_blog_post/update_blog_post's categoryNames — use this directly only when you need to set up a category (e.g. with a parent) before any post references it.",
|
|
236
|
+
inputSchema: {
|
|
237
|
+
type: "object",
|
|
238
|
+
properties: {
|
|
239
|
+
templateId: { type: "string", description: "The blog site's home page template id." },
|
|
240
|
+
name: { type: "string", description: "Category name." },
|
|
241
|
+
description: { type: "string", description: "Optional category description." },
|
|
242
|
+
parentName: { type: "string", description: "Optional parent category name, to nest this under it." },
|
|
243
|
+
},
|
|
244
|
+
required: ["templateId", "name"],
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: "delete_blog_category",
|
|
249
|
+
description: "Deletes a blog category. Posts keep their other categories/tags — only this category's assignment goes away.",
|
|
250
|
+
inputSchema: {
|
|
251
|
+
type: "object",
|
|
252
|
+
properties: {
|
|
253
|
+
templateId: { type: "string", description: "The blog site's home page template id." },
|
|
254
|
+
categoryId: { type: "string", description: "The category's id. Provide this or name." },
|
|
255
|
+
name: { type: "string", description: "The category's name (case-insensitive). Provide this or categoryId." },
|
|
256
|
+
},
|
|
257
|
+
required: ["templateId"],
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
name: "list_blog_tags",
|
|
262
|
+
description: "Lists a blog site's tags.",
|
|
263
|
+
inputSchema: {
|
|
264
|
+
type: "object",
|
|
265
|
+
properties: { templateId: { type: "string", description: "The blog site's home page template id." } },
|
|
266
|
+
required: ["templateId"],
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: "create_blog_tag",
|
|
271
|
+
description: "Creates a blog tag. Usually better created implicitly via create_blog_post/update_blog_post's tagNames — use this directly only when you need the tag to exist before any post references it.",
|
|
272
|
+
inputSchema: {
|
|
273
|
+
type: "object",
|
|
274
|
+
properties: {
|
|
275
|
+
templateId: { type: "string", description: "The blog site's home page template id." },
|
|
276
|
+
name: { type: "string", description: "Tag name." },
|
|
277
|
+
},
|
|
278
|
+
required: ["templateId", "name"],
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: "delete_blog_tag",
|
|
283
|
+
description: "Deletes a blog tag. Posts keep their other tags — only this tag's assignment goes away.",
|
|
284
|
+
inputSchema: {
|
|
285
|
+
type: "object",
|
|
286
|
+
properties: {
|
|
287
|
+
templateId: { type: "string", description: "The blog site's home page template id." },
|
|
288
|
+
tagId: { type: "string", description: "The tag's id. Provide this or name." },
|
|
289
|
+
name: { type: "string", description: "The tag's name (case-insensitive). Provide this or tagId." },
|
|
290
|
+
},
|
|
291
|
+
required: ["templateId"],
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
name: "list_blog_authors",
|
|
296
|
+
description: "Lists a blog site's authors/bylines.",
|
|
297
|
+
inputSchema: {
|
|
298
|
+
type: "object",
|
|
299
|
+
properties: { templateId: { type: "string", description: "The blog site's home page template id." } },
|
|
300
|
+
required: ["templateId"],
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
name: "create_blog_author",
|
|
305
|
+
description: "Creates a blog author/byline (a guest byline, not a login-capable account — WordPress-style). Usually better created implicitly via create_blog_post/update_blog_post's authorName — use this directly only when you need to set a bio/avatar before any post references it.",
|
|
306
|
+
inputSchema: {
|
|
307
|
+
type: "object",
|
|
308
|
+
properties: {
|
|
309
|
+
templateId: { type: "string", description: "The blog site's home page template id." },
|
|
310
|
+
name: { type: "string", description: "Author display name." },
|
|
311
|
+
bio: { type: "string", description: "Optional author bio." },
|
|
312
|
+
avatarUrl: { type: "string", description: "Optional avatar image URL." },
|
|
313
|
+
},
|
|
314
|
+
required: ["templateId", "name"],
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
name: "delete_blog_author",
|
|
319
|
+
description: "Deletes a blog author. Their posts are kept — they just lose the byline (same as removing a category/tag).",
|
|
320
|
+
inputSchema: {
|
|
321
|
+
type: "object",
|
|
322
|
+
properties: {
|
|
323
|
+
templateId: { type: "string", description: "The blog site's home page template id." },
|
|
324
|
+
authorId: { type: "string", description: "The author's id. Provide this or name." },
|
|
325
|
+
name: { type: "string", description: "The author's name (case-insensitive). Provide this or authorId." },
|
|
326
|
+
},
|
|
327
|
+
required: ["templateId"],
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
name: "list_blog_comments",
|
|
332
|
+
description: "Lists a blog site's comments (up to 200, most recent first), across all posts. Every comment starts PENDING and is invisible to visitors until moderated APPROVED.",
|
|
333
|
+
inputSchema: {
|
|
334
|
+
type: "object",
|
|
335
|
+
properties: {
|
|
336
|
+
templateId: { type: "string", description: "The blog site's home page template id." },
|
|
337
|
+
status: { type: "string", enum: ["PENDING", "APPROVED", "SPAM", "REJECTED"], description: "Optional status filter." },
|
|
338
|
+
},
|
|
339
|
+
required: ["templateId"],
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
name: "moderate_blog_comment",
|
|
344
|
+
description: "Approves, rejects, or marks a comment as spam (or resets it to pending).",
|
|
345
|
+
inputSchema: {
|
|
346
|
+
type: "object",
|
|
347
|
+
properties: {
|
|
348
|
+
templateId: { type: "string", description: "The blog site's home page template id." },
|
|
349
|
+
commentId: { type: "string", description: "The comment to moderate." },
|
|
350
|
+
status: { type: "string", enum: ["PENDING", "APPROVED", "SPAM", "REJECTED"], description: "New moderation status." },
|
|
351
|
+
},
|
|
352
|
+
required: ["templateId", "commentId", "status"],
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
name: "delete_blog_comment",
|
|
357
|
+
description: "Permanently deletes a comment (and any replies nested under it, cascading).",
|
|
358
|
+
inputSchema: {
|
|
359
|
+
type: "object",
|
|
360
|
+
properties: {
|
|
361
|
+
templateId: { type: "string", description: "The blog site's home page template id." },
|
|
362
|
+
commentId: { type: "string", description: "The comment to delete." },
|
|
363
|
+
},
|
|
364
|
+
required: ["templateId", "commentId"],
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
];
|
|
368
|
+
exports.BLOG_TOOL_NAMES = new Set(exports.BLOG_TOOL_DEFS.map((t) => t.name));
|
|
369
|
+
function requireKind(value) {
|
|
370
|
+
if (value !== "post" && value !== "listing")
|
|
371
|
+
throw new Error('kind must be "post" or "listing".');
|
|
372
|
+
return value;
|
|
373
|
+
}
|
|
374
|
+
function normalizeStringArray(value) {
|
|
375
|
+
if (value === undefined)
|
|
376
|
+
return undefined;
|
|
377
|
+
if (Array.isArray(value))
|
|
378
|
+
return value.map((v) => String(v).trim()).filter(Boolean);
|
|
379
|
+
if (typeof value === "string")
|
|
380
|
+
return value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
381
|
+
return [];
|
|
382
|
+
}
|
|
383
|
+
/** Walks a designJson tree (including nested rows inside a column) looking for any
|
|
384
|
+
* element of the given type — used to warn when a required blog_content/blog_post_list
|
|
385
|
+
* marker is missing, without needing the server to hand back compiled HTML. */
|
|
386
|
+
function designJsonHasElementType(designJson, elementType) {
|
|
387
|
+
const rows = designJson?.body?.rows;
|
|
388
|
+
if (!Array.isArray(rows))
|
|
389
|
+
return false;
|
|
390
|
+
const visitRows = (rowList) => rowList.some((row) => {
|
|
391
|
+
const columns = Array.isArray(row?.columns) ? row.columns : [];
|
|
392
|
+
return columns.some((col) => {
|
|
393
|
+
const elements = Array.isArray(col?.elements) ? col.elements : [];
|
|
394
|
+
if (elements.some((el) => el?.type === elementType))
|
|
395
|
+
return true;
|
|
396
|
+
const nestedRows = Array.isArray(col?.nestedRows) ? col.nestedRows : [];
|
|
397
|
+
return nestedRows.length > 0 && visitRows(nestedRows);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
return visitRows(rows);
|
|
401
|
+
}
|
|
402
|
+
async function resolveAuthorIdViaClient(client, templateId, authorName) {
|
|
403
|
+
if (authorName === undefined)
|
|
404
|
+
return undefined;
|
|
405
|
+
if (authorName === null)
|
|
406
|
+
return null;
|
|
407
|
+
const name = String(authorName).trim();
|
|
408
|
+
if (!name)
|
|
409
|
+
return null;
|
|
410
|
+
const { authors } = await client.listBlogAuthors(templateId);
|
|
411
|
+
const existing = (authors || []).find((a) => a.name.toLowerCase() === name.toLowerCase());
|
|
412
|
+
if (existing)
|
|
413
|
+
return existing.id;
|
|
414
|
+
const { author } = await client.createBlogAuthor(templateId, { name });
|
|
415
|
+
return author.id;
|
|
416
|
+
}
|
|
417
|
+
async function resolveCategoryIdsViaClient(client, templateId, names) {
|
|
418
|
+
if (names === undefined)
|
|
419
|
+
return undefined;
|
|
420
|
+
if (names.length === 0)
|
|
421
|
+
return [];
|
|
422
|
+
const { categories } = await client.listBlogCategories(templateId);
|
|
423
|
+
const known = [...(categories || [])];
|
|
424
|
+
const ids = [];
|
|
425
|
+
for (const name of names) {
|
|
426
|
+
const existing = known.find((c) => c.name.toLowerCase() === name.toLowerCase());
|
|
427
|
+
if (existing) {
|
|
428
|
+
ids.push(existing.id);
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
const { category } = await client.createBlogCategory(templateId, { name });
|
|
432
|
+
known.push(category);
|
|
433
|
+
ids.push(category.id);
|
|
434
|
+
}
|
|
435
|
+
return ids;
|
|
436
|
+
}
|
|
437
|
+
async function resolveTagIdsViaClient(client, templateId, names) {
|
|
438
|
+
if (names === undefined)
|
|
439
|
+
return undefined;
|
|
440
|
+
if (names.length === 0)
|
|
441
|
+
return [];
|
|
442
|
+
const { tags } = await client.listBlogTags(templateId);
|
|
443
|
+
const known = [...(tags || [])];
|
|
444
|
+
const ids = [];
|
|
445
|
+
for (const name of names) {
|
|
446
|
+
const existing = known.find((t) => t.name.toLowerCase() === name.toLowerCase());
|
|
447
|
+
if (existing) {
|
|
448
|
+
ids.push(existing.id);
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
const { tag } = await client.createBlogTag(templateId, name);
|
|
452
|
+
known.push(tag);
|
|
453
|
+
ids.push(tag.id);
|
|
454
|
+
}
|
|
455
|
+
return ids;
|
|
456
|
+
}
|
|
457
|
+
async function handleBlogToolCall(name, args, client) {
|
|
458
|
+
const templateId = args?.templateId;
|
|
459
|
+
switch (name) {
|
|
460
|
+
case "get_blog_layout": {
|
|
461
|
+
const kind = requireKind(args.kind);
|
|
462
|
+
const { site } = await client.getBlogSite(templateId);
|
|
463
|
+
const layoutTemplateId = kind === "post" ? site?.postLayoutTemplateId : site?.listingLayoutTemplateId;
|
|
464
|
+
if (!layoutTemplateId) {
|
|
465
|
+
return {
|
|
466
|
+
exists: false,
|
|
467
|
+
kind,
|
|
468
|
+
message: "No custom layout designed yet for this kind — call design_blog_layout to create one (the site uses the default built-in theme until then).",
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
const template = await client.getTemplate(layoutTemplateId);
|
|
472
|
+
const requiredType = kind === "post" ? "blog_content" : "blog_post_list";
|
|
473
|
+
return {
|
|
474
|
+
exists: true,
|
|
475
|
+
kind,
|
|
476
|
+
layoutTemplateId,
|
|
477
|
+
name: template.name,
|
|
478
|
+
designJson: template.designJson,
|
|
479
|
+
hasRequiredMarker: designJsonHasElementType(template.designJson, requiredType),
|
|
480
|
+
editableUrl: template.editableUrl,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
case "design_blog_layout": {
|
|
484
|
+
const kind = requireKind(args.kind);
|
|
485
|
+
let designJson = args.designJson;
|
|
486
|
+
if (typeof designJson === "string") {
|
|
487
|
+
try {
|
|
488
|
+
designJson = JSON.parse(designJson);
|
|
489
|
+
}
|
|
490
|
+
catch {
|
|
491
|
+
designJson = null;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (!designJson || typeof designJson !== "object") {
|
|
495
|
+
throw new Error("designJson object is required to design a blog layout.");
|
|
496
|
+
}
|
|
497
|
+
const ensured = await client.ensureBlogLayout(templateId, kind);
|
|
498
|
+
const layoutTemplateId = ensured.layoutTemplateId;
|
|
499
|
+
const updated = await client.updateTemplate({ templateId: layoutTemplateId, name: args.name, designJson });
|
|
500
|
+
const requiredType = kind === "post" ? "blog_content" : "blog_post_list";
|
|
501
|
+
const hasRequired = designJsonHasElementType(designJson, requiredType);
|
|
502
|
+
return {
|
|
503
|
+
success: true,
|
|
504
|
+
layoutTemplateId,
|
|
505
|
+
kind,
|
|
506
|
+
editableUrl: updated.editableUrl,
|
|
507
|
+
...(hasRequired
|
|
508
|
+
? {}
|
|
509
|
+
: { warning: `This layout has no "${requiredType}" element yet — until you add one, the public site falls back to the default blog theme instead of using this custom layout.` }),
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
case "reset_blog_layout": {
|
|
513
|
+
const kind = requireKind(args.kind);
|
|
514
|
+
await client.resetBlogLayout(templateId, kind);
|
|
515
|
+
return {
|
|
516
|
+
success: true,
|
|
517
|
+
kind,
|
|
518
|
+
message: "Layout detached — the site now uses the default built-in blog theme. The layout template itself was not deleted; call design_blog_layout again to reattach or start a new one.",
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
case "get_blog_settings":
|
|
522
|
+
return await client.getBlogSite(templateId);
|
|
523
|
+
case "update_blog_settings": {
|
|
524
|
+
const { templateId: _templateId, ...patch } = args;
|
|
525
|
+
if (Object.keys(patch).length === 0) {
|
|
526
|
+
throw new Error("Provide at least one setting to update (enabled, title, description, postsPerPage, showOnHomepage, accentColor, fontPreset, logoUrl, headerImageUrl, commentsEnabled).");
|
|
527
|
+
}
|
|
528
|
+
const result = await client.updateBlogSite(templateId, patch);
|
|
529
|
+
return { success: true, ...result };
|
|
530
|
+
}
|
|
531
|
+
case "list_blog_posts": {
|
|
532
|
+
let categoryId;
|
|
533
|
+
if (typeof args.categorySlug === "string" && args.categorySlug.trim()) {
|
|
534
|
+
const { categories } = await client.listBlogCategories(templateId);
|
|
535
|
+
const match = (categories || []).find((c) => c.slug === args.categorySlug.trim());
|
|
536
|
+
categoryId = match?.id;
|
|
537
|
+
}
|
|
538
|
+
return await client.listBlogPosts(templateId, { status: args.status, search: args.search, categoryId });
|
|
539
|
+
}
|
|
540
|
+
case "get_blog_post": {
|
|
541
|
+
let postId = typeof args.postId === "string" ? args.postId.trim() : "";
|
|
542
|
+
if (!postId && typeof args.slug === "string" && args.slug.trim()) {
|
|
543
|
+
const { posts } = await client.listBlogPosts(templateId, {});
|
|
544
|
+
const match = (posts || []).find((p) => p.slug === args.slug.trim());
|
|
545
|
+
if (!match)
|
|
546
|
+
throw new Error(`Post not found (slug "${args.slug}").`);
|
|
547
|
+
postId = match.id;
|
|
548
|
+
}
|
|
549
|
+
if (!postId)
|
|
550
|
+
throw new Error("postId or slug is required.");
|
|
551
|
+
return await client.getBlogPost(templateId, postId);
|
|
552
|
+
}
|
|
553
|
+
case "create_blog_post": {
|
|
554
|
+
const authorId = await resolveAuthorIdViaClient(client, templateId, args.authorName);
|
|
555
|
+
const categoryIds = await resolveCategoryIdsViaClient(client, templateId, normalizeStringArray(args.categoryNames));
|
|
556
|
+
const tagIds = await resolveTagIdsViaClient(client, templateId, normalizeStringArray(args.tagNames));
|
|
557
|
+
const payload = {
|
|
558
|
+
title: args.title,
|
|
559
|
+
slug: args.slug,
|
|
560
|
+
excerpt: args.excerpt,
|
|
561
|
+
contentHtml: args.contentHtml,
|
|
562
|
+
featuredImageUrl: args.featuredImageUrl,
|
|
563
|
+
featuredImageAlt: args.featuredImageAlt,
|
|
564
|
+
status: args.status,
|
|
565
|
+
scheduledAt: args.scheduledAt,
|
|
566
|
+
metaTitle: args.metaTitle,
|
|
567
|
+
metaDescription: args.metaDescription,
|
|
568
|
+
ogImageUrl: args.ogImageUrl,
|
|
569
|
+
noindex: args.noindex,
|
|
570
|
+
authorId,
|
|
571
|
+
categoryIds,
|
|
572
|
+
tagIds,
|
|
573
|
+
};
|
|
574
|
+
const result = await client.createBlogPost(templateId, payload);
|
|
575
|
+
return { success: true, postId: result.post?.id, slug: result.post?.slug, status: result.post?.status };
|
|
576
|
+
}
|
|
577
|
+
case "update_blog_post": {
|
|
578
|
+
const postId = typeof args.postId === "string" ? args.postId.trim() : "";
|
|
579
|
+
if (!postId)
|
|
580
|
+
throw new Error("postId is required.");
|
|
581
|
+
const payload = {};
|
|
582
|
+
const passthroughKeys = [
|
|
583
|
+
"title", "slug", "excerpt", "contentHtml", "featuredImageUrl", "featuredImageAlt",
|
|
584
|
+
"status", "scheduledAt", "metaTitle", "metaDescription", "ogImageUrl", "noindex",
|
|
585
|
+
];
|
|
586
|
+
for (const key of passthroughKeys) {
|
|
587
|
+
if (args[key] !== undefined)
|
|
588
|
+
payload[key] = args[key];
|
|
589
|
+
}
|
|
590
|
+
if (args.authorName !== undefined)
|
|
591
|
+
payload.authorId = await resolveAuthorIdViaClient(client, templateId, args.authorName);
|
|
592
|
+
if (args.categoryNames !== undefined)
|
|
593
|
+
payload.categoryIds = await resolveCategoryIdsViaClient(client, templateId, normalizeStringArray(args.categoryNames));
|
|
594
|
+
if (args.tagNames !== undefined)
|
|
595
|
+
payload.tagIds = await resolveTagIdsViaClient(client, templateId, normalizeStringArray(args.tagNames));
|
|
596
|
+
const result = await client.updateBlogPost(templateId, postId, payload);
|
|
597
|
+
return { success: true, postId: result.post?.id, slug: result.post?.slug, status: result.post?.status };
|
|
598
|
+
}
|
|
599
|
+
case "delete_blog_post": {
|
|
600
|
+
if (!args.postId)
|
|
601
|
+
throw new Error("postId is required.");
|
|
602
|
+
await client.deleteBlogPost(templateId, args.postId);
|
|
603
|
+
return { success: true, deletedPostId: args.postId };
|
|
604
|
+
}
|
|
605
|
+
case "list_blog_categories":
|
|
606
|
+
return await client.listBlogCategories(templateId);
|
|
607
|
+
case "create_blog_category": {
|
|
608
|
+
let parentId;
|
|
609
|
+
if (typeof args.parentName === "string" && args.parentName.trim()) {
|
|
610
|
+
const { categories } = await client.listBlogCategories(templateId);
|
|
611
|
+
const match = (categories || []).find((c) => c.name.toLowerCase() === args.parentName.trim().toLowerCase());
|
|
612
|
+
if (!match)
|
|
613
|
+
throw new Error(`Parent category "${args.parentName}" not found.`);
|
|
614
|
+
parentId = match.id;
|
|
615
|
+
}
|
|
616
|
+
const result = await client.createBlogCategory(templateId, { name: args.name, description: args.description, parentId });
|
|
617
|
+
return { success: true, category: result.category };
|
|
618
|
+
}
|
|
619
|
+
case "delete_blog_category": {
|
|
620
|
+
let categoryId = typeof args.categoryId === "string" ? args.categoryId.trim() : "";
|
|
621
|
+
if (!categoryId && typeof args.name === "string" && args.name.trim()) {
|
|
622
|
+
const { categories } = await client.listBlogCategories(templateId);
|
|
623
|
+
const match = (categories || []).find((c) => c.name.toLowerCase() === args.name.trim().toLowerCase());
|
|
624
|
+
if (!match)
|
|
625
|
+
throw new Error(`Category "${args.name}" not found.`);
|
|
626
|
+
categoryId = match.id;
|
|
627
|
+
}
|
|
628
|
+
if (!categoryId)
|
|
629
|
+
throw new Error("categoryId or name is required.");
|
|
630
|
+
await client.deleteBlogCategory(templateId, categoryId);
|
|
631
|
+
return { success: true, deletedCategoryId: categoryId };
|
|
632
|
+
}
|
|
633
|
+
case "list_blog_tags":
|
|
634
|
+
return await client.listBlogTags(templateId);
|
|
635
|
+
case "create_blog_tag": {
|
|
636
|
+
const result = await client.createBlogTag(templateId, args.name);
|
|
637
|
+
return { success: true, tag: result.tag };
|
|
638
|
+
}
|
|
639
|
+
case "delete_blog_tag": {
|
|
640
|
+
let tagId = typeof args.tagId === "string" ? args.tagId.trim() : "";
|
|
641
|
+
if (!tagId && typeof args.name === "string" && args.name.trim()) {
|
|
642
|
+
const { tags } = await client.listBlogTags(templateId);
|
|
643
|
+
const match = (tags || []).find((t) => t.name.toLowerCase() === args.name.trim().toLowerCase());
|
|
644
|
+
if (!match)
|
|
645
|
+
throw new Error(`Tag "${args.name}" not found.`);
|
|
646
|
+
tagId = match.id;
|
|
647
|
+
}
|
|
648
|
+
if (!tagId)
|
|
649
|
+
throw new Error("tagId or name is required.");
|
|
650
|
+
await client.deleteBlogTag(templateId, tagId);
|
|
651
|
+
return { success: true, deletedTagId: tagId };
|
|
652
|
+
}
|
|
653
|
+
case "list_blog_authors":
|
|
654
|
+
return await client.listBlogAuthors(templateId);
|
|
655
|
+
case "create_blog_author": {
|
|
656
|
+
const result = await client.createBlogAuthor(templateId, { name: args.name, bio: args.bio, avatarUrl: args.avatarUrl });
|
|
657
|
+
return { success: true, author: result.author };
|
|
658
|
+
}
|
|
659
|
+
case "delete_blog_author": {
|
|
660
|
+
let authorId = typeof args.authorId === "string" ? args.authorId.trim() : "";
|
|
661
|
+
if (!authorId && typeof args.name === "string" && args.name.trim()) {
|
|
662
|
+
const { authors } = await client.listBlogAuthors(templateId);
|
|
663
|
+
const match = (authors || []).find((a) => a.name.toLowerCase() === args.name.trim().toLowerCase());
|
|
664
|
+
if (!match)
|
|
665
|
+
throw new Error(`Author "${args.name}" not found.`);
|
|
666
|
+
authorId = match.id;
|
|
667
|
+
}
|
|
668
|
+
if (!authorId)
|
|
669
|
+
throw new Error("authorId or name is required.");
|
|
670
|
+
await client.deleteBlogAuthor(templateId, authorId);
|
|
671
|
+
return { success: true, deletedAuthorId: authorId };
|
|
672
|
+
}
|
|
673
|
+
case "list_blog_comments":
|
|
674
|
+
return await client.listBlogComments(templateId, args.status);
|
|
675
|
+
case "moderate_blog_comment": {
|
|
676
|
+
if (!args.commentId || !args.status)
|
|
677
|
+
throw new Error("commentId and status are required.");
|
|
678
|
+
const result = await client.moderateBlogComment(templateId, args.commentId, args.status);
|
|
679
|
+
return { success: true, comment: result.comment };
|
|
680
|
+
}
|
|
681
|
+
case "delete_blog_comment": {
|
|
682
|
+
if (!args.commentId)
|
|
683
|
+
throw new Error("commentId is required.");
|
|
684
|
+
await client.deleteBlogComment(templateId, args.commentId);
|
|
685
|
+
return { success: true, deletedCommentId: args.commentId };
|
|
686
|
+
}
|
|
687
|
+
default:
|
|
688
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
689
|
+
}
|
|
690
|
+
}
|