From 6eeb8b90872d909860aaa8cd99f42ecbc6614019 Mon Sep 17 00:00:00 2001 From: WordVibe Date: Tue, 22 Sep 2026 01:59:54 +0300 Subject: [PATCH] feat: add wordvibe_build_page (Word Page Builder over MCP) Wraps the site tool wpb_build_page: build or update a Word Page from {kind, html} sections. The page lands as a native Word Page (payload in post meta, builder-rendered), so it stays editable and undoable in the builder. Also documents the builder tools in the bundled skill and README. --- README.md | 1 + __init__.py | 1 + plugin.yaml | 1 + schemas.py | 43 ++++++++++++++++++++++++++++++++++ skills/wordvibe-sites/SKILL.md | 19 +++++++++++++++ tools.py | 38 ++++++++++++++++++++++++++++++ 6 files changed, 103 insertions(+) diff --git a/README.md b/README.md index 155312b..4065e98 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ Debug a plugin that won't load with `HERMES_PLUGINS_DEBUG=1 hermes plugins list` | `wordvibe_write_file` | Overwrite a file (the site snapshots the previous version first) | | `wordvibe_edit_file` | Targeted `search` → `replace` edit (safer than a full write) | | `wordvibe_search` | Codebase search across themes/plugins/core, plain text or regex | +| `wordvibe_build_page` | Build/update a **Word Page Builder** page from `{kind, html}` sections (native in the builder) | Slash command for when you don't want to spend a model turn: diff --git a/__init__.py b/__init__.py index 5c9a8fd..4085fd7 100644 --- a/__init__.py +++ b/__init__.py @@ -65,6 +65,7 @@ def register(ctx): ("wordvibe_write_file", schemas.WRITE_FILE, tools.write_file), ("wordvibe_edit_file", schemas.EDIT_FILE, tools.edit_file), ("wordvibe_search", schemas.SEARCH, tools.search), + ("wordvibe_build_page", schemas.BUILD_PAGE, tools.build_page), ] for name, schema, handler in registry: diff --git a/plugin.yaml b/plugin.yaml index 13a0e19..096f111 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -16,6 +16,7 @@ provides_tools: - wordvibe_write_file - wordvibe_edit_file - wordvibe_search + - wordvibe_build_page requires_env: - name: WORDVIBE_SITE_URL description: "WordPress site URL (e.g. https://mysite.com). MCP Server Mode must be enabled in WordVibe → Settings → MCP." diff --git a/schemas.py b/schemas.py index c967f69..559eb94 100644 --- a/schemas.py +++ b/schemas.py @@ -13,6 +13,49 @@ _SITE = { ), } +BUILD_PAGE = { + "name": "wordvibe_build_page", + "description": ( + "Build or update a page in the site's Word Page Builder (WPB) from section HTML. Each " + "section is wrapped automatically in
, " + "so the page opens natively in the builder and stays editable/undoable there. Use this to " + "publish a landing page, or to rebuild a page's sections, without a browser." + ), + "parameters": { + "type": "object", + "properties": { + "title": {"type": "string", "description": "Page title."}, + "sections": { + "type": "array", + "description": ( + "Ordered list of sections. Each item: {kind, html} — kind is a kebab-case section " + "name (hero, features, pricing, testimonials, faq, cta, footer, gallery, contact, " + "about, services, custom); html is the section's inner HTML (inline styles are fine)." + ), + "items": { + "type": "object", + "properties": { + "kind": {"type": "string", "description": "Section kind, e.g. 'hero'."}, + "html": {"type": "string", "description": "Inner HTML for the section."}, + }, + "required": ["kind", "html"], + }, + }, + "slug": {"type": "string", "description": "URL slug (optional)."}, + "status": { + "type": "string", + "description": "draft, publish, pending or private (default: draft).", + }, + "post_id": { + "type": "integer", + "description": "Existing page ID to update; omit to create a new page.", + }, + "site": _SITE, + }, + "required": ["title", "sections"], + }, +} + SITES = { "name": "wordvibe_sites", "description": ( diff --git a/skills/wordvibe-sites/SKILL.md b/skills/wordvibe-sites/SKILL.md index 412e0ec..1062dd4 100644 --- a/skills/wordvibe-sites/SKILL.md +++ b/skills/wordvibe-sites/SKILL.md @@ -48,8 +48,27 @@ configured site is the default; when several are configured, pass `site` (name o | Database lookups | `wordvibe_call(tool="db_query", arguments={"sql":"SELECT COUNT(*) FROM wp_posts"})` | | Security sweep | `wordvibe_call(tool="security_scan_installed")` | | Roll back an edit | `wordvibe_call(tool="rollback_list")` → `rollback_restore` | +| Build a page | `wordvibe_build_page(title=…, sections=[{kind, html}], status="publish")` | | Site basics | `wordvibe_call(tool="site_info")`, `theme_list`, `plugin_list` | +## Building pages (Word Page Builder) + +`wordvibe_build_page` creates or updates a **Word Page** — a page the site's Word Page Builder owns and +renders from its own stored payload, so it opens natively in the builder (edit, undo, redo all work). + +- One call builds the whole page: `title`, `sections: [{kind, html}]`, optional `slug`, `status`, `post_id`. +- Section kinds: `hero`, `features`, `pricing`, `testimonials`, `faq`, `cta`, `footer`, `gallery`, + `contact`, `about`, `services`, `custom`. Each becomes `
`. +- Write inline styles rather than inventing class names — the builder preserves what you give it, but + the site's stylesheet won't contain classes that don't exist. +- Editing an existing page: `wordvibe_tools(filter="wpb")` lists the builder tools, then use + `wordvibe_call` with `wpb_get_page` / `wpb_get_page_full`, `wpb_replace_section_html`, + `wpb_add_section_to_page`, `wpb_delete_section`, `wpb_generate_page` (AI), `wpb_chat_edit_page` (AI, one + instruction per call), `wpb_list_pages`. +- Needs the page builder present in the site's build (WordVibe 1.0.x cloud/ide). Where it was stripped, + the tools answer "The Word Page Builder is not available in this build of the plugin." — say so instead + of faking it with a plain page. + ## Multi-site With `WORDVIBE_MCP_SITES` configured, name the target explicitly on every call — do not let a default diff --git a/tools.py b/tools.py index 34caf2a..16422af 100644 --- a/tools.py +++ b/tools.py @@ -212,3 +212,41 @@ def search(args: dict, **kwargs) -> str: if args.get("regex"): payload["regex"] = True return _call_tool(args.get("site") or "", "codebase_search", payload) + + +def build_page(args: dict, **kwargs) -> str: + """Build/update a Word Page Builder page (wraps the site's wpb_build_page tool).""" + title = str(args.get("title") or "").strip() + if not title: + return _err("Missing 'title'.") + + sections = args.get("sections") + if isinstance(sections, str): + try: + sections = json.loads(sections) + except Exception as exc: # noqa: BLE001 + return _err(f"'sections' must be a JSON array: {exc}") + if not isinstance(sections, list) or not sections: + return _err("'sections' must be a non-empty array of {kind, html}.") + + clean = [] + for item in sections: + if isinstance(item, str): + clean.append({"kind": "custom", "html": item}) + continue + if not isinstance(item, dict) or "html" not in item: + return _err("Each section needs {kind, html}.") + clean.append({"kind": str(item.get("kind") or "custom"), "html": str(item.get("html") or "")}) + + payload = {"title": title, "sections": clean} + for key in ("slug", "status"): + val = str(args.get(key) or "").strip() + if val: + payload[key] = val + if args.get("post_id"): + try: + payload["post_id"] = int(args["post_id"]) + except Exception: # noqa: BLE001 + return _err("'post_id' must be an integer.") + + return _call_tool(args.get("site") or "", "wpb_build_page", payload)