sbuilder-mcp 0.28.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/CHANGELOG.vi.md +19 -0
- package/README.md +2 -1
- package/README.vi.md +2 -1
- package/dist/catalog/api.generated.js +22 -6
- package/dist/catalog/element-search.js +7 -1
- package/dist/catalog/elements.generated.js +34 -0
- package/dist/domains/site/patterns.js +2 -0
- package/dist/domains/site/vocabulary.js +88 -1
- package/dist/server.js +2 -0
- package/dist/tools/live.js +85 -23
- package/dist/tools/page.js +21 -7
- package/dist/tools/theme.js +136 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,25 @@ All notable changes to this project are documented in this file.
|
|
|
6
6
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
7
7
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
8
8
|
|
|
9
|
+
## [0.30.0] - 2026-09-10
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- A new tool, sb_theme, reads the site's colour tokens and text styles and can now write them: `colors` and `text_styles` patch the saved theme document, keeping every field you do not name, and an unknown token id or style slug is refused with the real ones listed. A site that has never saved a theme reads back the starter theme rather than an empty document.
|
|
13
|
+
- sb_traits_for now describes `config.animation` on every element that offers it (73 of 111), naming the four ways a write silently renders nothing: it must be an object rather than a bare string, `active: true` is required, the `type` value is spelled with underscores, and the write is base-only.
|
|
14
|
+
- sb_set now warns when a `config.animation` write will not animate: a value that is not an object, a missing `active: true`, an unrecognised `type`, or an unrecognised `easing` (which falls back to a curve you did not choose) are each named individually.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- sb_set now routes a `config.animation` write to the base breakpoint instead of a per-breakpoint slot the renderer never reads, closing the fourth silent way to lose the animation.
|
|
18
|
+
|
|
19
|
+
## [0.29.0] - 2026-09-10
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- sb_media_upload's `pick` argument now accepts several ids at once, so stocking a new site's empty library from one search result takes one upload call instead of one per photo; a picked id the search did not return refuses the whole call rather than uploading part of it.
|
|
23
|
+
- A layout pattern now declares how many picture slots it has, and sb_template_use reports how many the site's library could fill and names the sb_media_upload calls (query, then pick with several ids) that fill the rest.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- sb_template_use's result field for a library with too few images is now `pictures`, naming the exact shortfall (slots wanted vs. images offered) instead of the old `images` field, which only ever said the library was completely empty.
|
|
27
|
+
|
|
9
28
|
## [0.28.0] - 2026-09-10
|
|
10
29
|
|
|
11
30
|
### Added
|
package/CHANGELOG.vi.md
CHANGED
|
@@ -6,6 +6,25 @@ Mọi thay đổi đáng chú ý của dự án được ghi lại trong file n
|
|
|
6
6
|
Định dạng dựa trên [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
7
7
|
và dự án tuân theo [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
8
8
|
|
|
9
|
+
## [0.30.0] - 2026-09-10
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Một tool mới, sb_theme, đọc bảng màu và text style của site và giờ có thể ghi chúng: `colors` và `text_styles` patch vào theme document đã lưu, giữ nguyên mọi trường không được nêu tên, còn một token id hay style slug không tồn tại sẽ bị từ chối kèm danh sách các id thật. Một site chưa từng lưu theme sẽ đọc lại theme mặc định (starter) thay vì một document rỗng.
|
|
13
|
+
- sb_traits_for giờ mô tả `config.animation` trên mọi element có cung cấp nó (73 trong 111 element), nêu rõ bốn cách khiến việc ghi âm thầm không tạo ra hiệu ứng nào: nó phải là một object thay vì một chuỗi đơn thuần, `active: true` là bắt buộc, giá trị `type` được viết bằng dấu gạch dưới, và việc ghi chỉ áp dụng ở base breakpoint.
|
|
14
|
+
- sb_set giờ cảnh báo khi một lệnh ghi `config.animation` sẽ không tạo hiệu ứng: giá trị không phải object, thiếu `active: true`, `type` không nhận diện được, hoặc `easing` không nhận diện được (sẽ rơi về một đường cong bạn không hề chọn) — mỗi trường hợp được nêu tên riêng.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- sb_set giờ định tuyến lệnh ghi `config.animation` vào breakpoint base thay vì một vị trí theo từng breakpoint mà renderer không bao giờ đọc tới, khép lại cách thứ tư khiến hiệu ứng bị mất một cách âm thầm.
|
|
18
|
+
|
|
19
|
+
## [0.29.0] - 2026-09-10
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- Tham số `pick` của sb_media_upload giờ nhận nhiều id cùng lúc, nên việc lấp đầy thư viện trống của một site mới từ một lần tìm kiếm chỉ cần một lệnh upload thay vì một lệnh cho mỗi ảnh; một id được chọn mà kết quả tìm kiếm không trả về sẽ từ chối toàn bộ lệnh thay vì chỉ upload một phần.
|
|
23
|
+
- Một layout dựng sẵn giờ khai báo số ô chứa ảnh nó có, và sb_template_use báo lại thư viện của site lấp được bao nhiêu ô cùng tên các lệnh sb_media_upload (query, rồi pick với nhiều id) để lấp phần còn lại.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- Trường kết quả của sb_template_use khi thư viện thiếu ảnh giờ là `pictures`, nêu đúng phần còn thiếu (số ô cần vs. số ảnh có) thay cho trường `images` cũ, vốn chỉ báo khi thư viện hoàn toàn trống.
|
|
27
|
+
|
|
9
28
|
## [0.28.0] - 2026-09-10
|
|
10
29
|
|
|
11
30
|
### Added
|
package/README.md
CHANGED
|
@@ -101,13 +101,14 @@ in this client: one would put the very key the platform exists to hold back into
|
|
|
101
101
|
| `sb_publish` | Compile the draft into the live page (cascades to shared globals) |
|
|
102
102
|
| `sb_review` | Every defect a visitor would see, each with its fix, plus the five gaps between this store and a paid order |
|
|
103
103
|
| `sb_media_list` | The site's media library |
|
|
104
|
-
| `sb_media_upload` | Add an image and get its URL — a local path, a URL the platform fetches, or a SEARCH for real photographs you read and pick from |
|
|
104
|
+
| `sb_media_upload` | Add an image and get its URL — a local path, a URL the platform fetches, or a SEARCH for real photographs you read and pick from, one or several at a time |
|
|
105
105
|
| `sb_live_join` | Join the editor's live-edit room as a visible peer — edits then appear live |
|
|
106
106
|
| `sb_look` | Save, render, and return screenshots plus measured node boxes and layout defects measured on the render |
|
|
107
107
|
| `sb_event` | Give a node a click action — open the cart, go to a page, open a pop-up |
|
|
108
108
|
| `sb_bind` | Bind a node's content to real store data, or make a button add to the cart |
|
|
109
109
|
| `sb_import` | Read a page from any public URL and add its structure and content to the open page as real elements, styled with THIS page's own tokens — a translation, not a clone |
|
|
110
110
|
| `sb_import_site` | Read a WHOLE site from one URL — its sitemap, or the links on that page — and give each page found its own draft page here, built from this site's tokens |
|
|
111
|
+
| `sb_theme` | Read or patch the site's palette and type scale — the layer every style preset resolves from, so one token repaints every page |
|
|
111
112
|
| `sb_store` | Run a store flow that must happen in a fixed order — the four writes that make a working checkout, or any of the platform's 17 form templates (login, register, forgot, contact, subscribe …) with its own field document |
|
|
112
113
|
| `sb_undo` | Put back what a PUT replaced — the platform has no page history or restore, so this is the only way back |
|
|
113
114
|
|
package/README.vi.md
CHANGED
|
@@ -98,13 +98,14 @@ nhét ngược lại vào mọi bản cài.
|
|
|
98
98
|
| `sb_publish` | Biên dịch bản nháp thành trang live (lan sang global dùng chung) |
|
|
99
99
|
| `sb_review` | Mọi khiếm khuyết người xem sẽ thấy, kèm lệnh sửa từng cái, và năm khoảng trống chắn giữa cửa hàng với một đơn đã thanh toán |
|
|
100
100
|
| `sb_media_list` | Thư viện ảnh của site |
|
|
101
|
-
| `sb_media_upload` | Thêm ảnh và lấy URL — file trên máy, một URL để nền tảng tự tải, hoặc TÌM ảnh chụp thật để đọc mô tả rồi chọn |
|
|
101
|
+
| `sb_media_upload` | Thêm ảnh và lấy URL — file trên máy, một URL để nền tảng tự tải, hoặc TÌM ảnh chụp thật để đọc mô tả rồi chọn, một hoặc nhiều tấm một lượt |
|
|
102
102
|
| `sb_live_join` | Vào phòng live-edit của editor như một peer nhìn thấy được — sửa gì hiện ngay |
|
|
103
103
|
| `sb_look` | Lưu, render, trả về ảnh chụp kèm box đo được của node và lỗi bố cục đo trên bản render |
|
|
104
104
|
| `sb_event` | Gắn click action cho một node — mở giỏ, sang trang, mở pop-up |
|
|
105
105
|
| `sb_bind` | Gắn nội dung một node vào dữ liệu cửa hàng thật, hoặc biến một nút thành nút thêm vào giỏ |
|
|
106
106
|
| `sb_import` | Đọc một trang từ URL công khai bất kỳ và thêm cấu trúc + nội dung của nó vào trang đang mở dưới dạng element thật, mang token của CHÍNH trang này — là dịch lại, không phải sao chép |
|
|
107
107
|
| `sb_import_site` | Đọc CẢ website từ một URL — sitemap của nó, hoặc các link trên trang đó — và tạo cho mỗi trang tìm được một trang nháp riêng ở đây, dựng bằng token của site này |
|
|
108
|
+
| `sb_theme` | Đọc hoặc vá bảng màu và thang chữ của site — tầng mà mọi style preset phân giải từ đó, nên một token thay áo cho mọi trang |
|
|
108
109
|
| `sb_store` | Chạy một luồng cửa hàng bắt buộc đúng thứ tự — bốn lệnh ghi tạo nên trang thanh toán, hoặc gieo bất kỳ template nào trong 17 form của nền tảng (login, register, forgot, contact, subscribe …) kèm field document của nó |
|
|
109
110
|
| `sb_undo` | Trả lại thứ mà một lệnh PUT đã ghi đè — nền tảng không có lịch sử trang hay restore, nên đây là đường về duy nhất |
|
|
110
111
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const SWAGGER_SOURCE = {
|
|
2
2
|
"operations": 501,
|
|
3
|
-
"definitions":
|
|
4
|
-
"bodyCarrying":
|
|
3
|
+
"definitions": 105,
|
|
4
|
+
"bodyCarrying": 179,
|
|
5
5
|
"bodyUndescribed": 64,
|
|
6
6
|
"generatedFrom": "server/docs/swagger.json"
|
|
7
7
|
};
|
|
@@ -12958,18 +12958,25 @@ export const API_OPERATIONS = [
|
|
|
12958
12958
|
"tags": [
|
|
12959
12959
|
"templates"
|
|
12960
12960
|
],
|
|
12961
|
-
"summary": "
|
|
12961
|
+
"summary": "Create a site from a template",
|
|
12962
12962
|
"params": [
|
|
12963
12963
|
{
|
|
12964
12964
|
"name": "siteId",
|
|
12965
12965
|
"in": "path",
|
|
12966
12966
|
"required": true,
|
|
12967
12967
|
"type": "string",
|
|
12968
|
-
"description": "
|
|
12968
|
+
"description": "Template site to start from"
|
|
12969
|
+
},
|
|
12970
|
+
{
|
|
12971
|
+
"name": "body",
|
|
12972
|
+
"in": "body",
|
|
12973
|
+
"required": true,
|
|
12974
|
+
"type": "object",
|
|
12975
|
+
"description": "The new site's name"
|
|
12969
12976
|
}
|
|
12970
12977
|
],
|
|
12971
|
-
"bodyDescribed":
|
|
12972
|
-
"bodyRef":
|
|
12978
|
+
"bodyDescribed": true,
|
|
12979
|
+
"bodyRef": "internal_templates_rest.useBody",
|
|
12973
12980
|
"credential": "siteScoped"
|
|
12974
12981
|
},
|
|
12975
12982
|
{
|
|
@@ -17923,5 +17930,14 @@ export const API_DEFINITIONS = {
|
|
|
17923
17930
|
"type": "integer"
|
|
17924
17931
|
}
|
|
17925
17932
|
}
|
|
17933
|
+
},
|
|
17934
|
+
"internal_templates_rest.useBody": {
|
|
17935
|
+
"type": "object",
|
|
17936
|
+
"properties": {
|
|
17937
|
+
"name": {
|
|
17938
|
+
"description": "Name is the new site's name. Required rather than defaulted to the\ntemplate's own name: a default would quietly produce two sites called\n\"Fashion starter\" for a user who pressed the button twice.",
|
|
17939
|
+
"type": "string"
|
|
17940
|
+
}
|
|
17941
|
+
}
|
|
17926
17942
|
}
|
|
17927
17943
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ELEMENTS, TRAIT_WRITES } from './elements.generated.js';
|
|
2
|
-
import { vocabulariesForWrites } from '../domains/site/vocabulary.js';
|
|
2
|
+
import { animationVocabulary, vocabulariesForWrites } from '../domains/site/vocabulary.js';
|
|
3
3
|
import { neverTranslatedOn, translatableSpecials } from '../domains/site/translate.js';
|
|
4
4
|
/** Eight to choose from; the hints for the chosen one come with sb_traits_for. */
|
|
5
5
|
export const DEFAULT_CATALOG_LIMIT = 8;
|
|
@@ -125,6 +125,12 @@ export function traitsFor(type, control) {
|
|
|
125
125
|
const vocab = vocabulariesForWrites([...keys]);
|
|
126
126
|
return Object.keys(vocab).length ? { config_values: vocab } : {};
|
|
127
127
|
})(),
|
|
128
|
+
// THE ENTRANCE ANIMATION, for the 73 element types that offer it. It does
|
|
129
|
+
// not ride in `config_values` because it is not a word — it is an OBJECT
|
|
130
|
+
// with a required gate, and the three ways to get it wrong all render
|
|
131
|
+
// NOTHING rather than something else. An element that does not offer the
|
|
132
|
+
// control says nothing, so this is silent on the other 38.
|
|
133
|
+
...(el.controls.includes('animation') ? { animation: animationVocabulary() } : {}),
|
|
128
134
|
isContainer: el.isContainer,
|
|
129
135
|
isRootOnly: el.isRootOnly,
|
|
130
136
|
childAllows: el.childAllows,
|
|
@@ -36188,6 +36188,7 @@ export const BASE_ONLY_CONFIG = [
|
|
|
36188
36188
|
"collectionId",
|
|
36189
36189
|
"collectionType",
|
|
36190
36190
|
"quantity",
|
|
36191
|
+
"animation",
|
|
36191
36192
|
"rowLimit"
|
|
36192
36193
|
];
|
|
36193
36194
|
/**
|
|
@@ -36198,3 +36199,36 @@ export const BASE_ONLY_CONFIG = [
|
|
|
36198
36199
|
export const BASE_ONLY_EXCEPTIONS = [
|
|
36199
36200
|
"quantity-button:iconSize"
|
|
36200
36201
|
];
|
|
36202
|
+
/**
|
|
36203
|
+
* The ENTRANCE ANIMATION's vocabulary — config.animation, offered by 73 of the
|
|
36204
|
+
* 111 element types and describable by nothing until now.
|
|
36205
|
+
*
|
|
36206
|
+
* Three ways to miss, all silent (AnimationTypeOf answers "" and no keyframes,
|
|
36207
|
+
* no rule and no error are emitted, through save, publish and render):
|
|
36208
|
+
* - it is an OBJECT, not a string: {active, type, easing, delay, duration}
|
|
36209
|
+
* - active:true is REQUIRED; a stored type is deliberately NOT consent,
|
|
36210
|
+
* because the panel keeps the type when the switch goes off
|
|
36211
|
+
* - type is a keyframe key spelled with UNDERSCORES: fade_in, never fade-in
|
|
36212
|
+
*
|
|
36213
|
+
* easing is the mild one: an unrecognised value falls back to "ease".
|
|
36214
|
+
*
|
|
36215
|
+
* It is also BASE-ONLY (see BASE_ONLY_CONFIG) — render/css.go emits it into the
|
|
36216
|
+
* base lane because the config object is read with no responsive merge.
|
|
36217
|
+
*/
|
|
36218
|
+
export const ANIMATION = {
|
|
36219
|
+
"types": [
|
|
36220
|
+
"fade_in",
|
|
36221
|
+
"slide_down",
|
|
36222
|
+
"slide_up",
|
|
36223
|
+
"zoom_in"
|
|
36224
|
+
],
|
|
36225
|
+
"easings": [
|
|
36226
|
+
"ease",
|
|
36227
|
+
"ease-in",
|
|
36228
|
+
"ease-out",
|
|
36229
|
+
"linear"
|
|
36230
|
+
],
|
|
36231
|
+
"easingFallback": "ease",
|
|
36232
|
+
"durationDefault": 0.5,
|
|
36233
|
+
"readBy": "AnimationTypeOf + CompileEntranceAnimationCSS"
|
|
36234
|
+
};
|
|
@@ -93,6 +93,7 @@ export const LAYOUT_PATTERNS = [
|
|
|
93
93
|
id: 'sb_hero_split',
|
|
94
94
|
name: 'Hero — hai cột',
|
|
95
95
|
use: 'Mở đầu trang: tiêu đề, một câu, nút hành động bên trái; chỗ cho ảnh bên phải',
|
|
96
|
+
images: 1,
|
|
96
97
|
build: (t, pool) => {
|
|
97
98
|
const used = new Set();
|
|
98
99
|
return section([
|
|
@@ -149,6 +150,7 @@ export const LAYOUT_PATTERNS = [
|
|
|
149
150
|
{
|
|
150
151
|
id: 'sb_gallery',
|
|
151
152
|
name: 'Dải ảnh',
|
|
153
|
+
images: 6,
|
|
152
154
|
use: 'Một hàng ảnh cuốn dòng, lấy từ thư viện ảnh của chính site',
|
|
153
155
|
build: (t, pool) => {
|
|
154
156
|
const used = new Set();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CONFIG_VALUES } from '../../catalog/elements.generated.js';
|
|
1
|
+
import { ANIMATION, CONFIG_VALUES } from '../../catalog/elements.generated.js';
|
|
2
2
|
/** The vocabulary for a config key, or null when the key has no fixed one. */
|
|
3
3
|
export function vocabularyFor(key) {
|
|
4
4
|
return CONFIG_VALUES[key] ?? null;
|
|
@@ -55,3 +55,90 @@ export function unknownValueNote(key, value) {
|
|
|
55
55
|
: '') +
|
|
56
56
|
'.');
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* THE ENTRANCE ANIMATION, WHICH 73 OF 111 ELEMENTS OFFER AND NOTHING DESCRIBED.
|
|
60
|
+
*
|
|
61
|
+
* Same family as the vocabularies above and worse in one way: there are THREE
|
|
62
|
+
* ways to miss and all of them are silent, because `AnimationTypeOf` answers ""
|
|
63
|
+
* and `render/css.go` then emits no keyframes, no rule and no error — through
|
|
64
|
+
* save, publish and render.
|
|
65
|
+
*
|
|
66
|
+
* - IT IS AN OBJECT, not the string the control's name invites.
|
|
67
|
+
* `readAnimConfig` asserts `map[string]interface{}`, so a bare
|
|
68
|
+
* `"fade_in"` is the zero value and the node never animates.
|
|
69
|
+
* - `active: true` IS REQUIRED, and a stored `type` is deliberately NOT
|
|
70
|
+
* consent. The platform's own comment gives the reason: the panel keeps
|
|
71
|
+
* `type` when the switch goes off, so switching back restores the choice —
|
|
72
|
+
* "treating a stored type as consent would animate a node the author had
|
|
73
|
+
* explicitly turned off".
|
|
74
|
+
* - THE TYPE IS UNDERSCORED. `AnimKeyframes`'s comment flags it outright:
|
|
75
|
+
* "keyed by the STORED value (`fade_in`, not `fade-in`)". `fade-in` is the
|
|
76
|
+
* spelling every other web tool uses and the one an agent reaches for.
|
|
77
|
+
*
|
|
78
|
+
* `easing` is the mild case and is reported differently: an unrecognised value
|
|
79
|
+
* falls back to `ease`, so the animation still runs — it is a wrong answer, not
|
|
80
|
+
* a missing one.
|
|
81
|
+
*
|
|
82
|
+
* And it is BASE-ONLY, which is a fourth way to lose it — but that one is
|
|
83
|
+
* ROUTED rather than warned about, by `baseonly.ts`, because the platform's
|
|
84
|
+
* ledger names the key and `sb_set` can simply write it to the right layer.
|
|
85
|
+
*/
|
|
86
|
+
export const ANIMATION_VOCAB = ANIMATION;
|
|
87
|
+
/**
|
|
88
|
+
* Everything a caller needs to write config.animation correctly, in ONE LINE.
|
|
89
|
+
*
|
|
90
|
+
* It rides on 73 of the 111 elements, and `sb_traits_for` is the result an agent
|
|
91
|
+
* reads before every styling decision — so the first version of this, a
|
|
92
|
+
* six-field object, was 400 bytes of dilution on two thirds of the catalog and
|
|
93
|
+
* pushed the search-result budget over its ceiling. The budget test was right:
|
|
94
|
+
* the four facts fit in a sentence, and a sentence is what a reader takes in
|
|
95
|
+
* anyway. The long form lives in `animationNote`, which fires at the moment the
|
|
96
|
+
* mistake is actually made.
|
|
97
|
+
*/
|
|
98
|
+
export function animationVocabulary() {
|
|
99
|
+
return (`config.animation is an OBJECT: { active: true, type: ${ANIMATION.types.join('|')}, ` +
|
|
100
|
+
`easing?: ${ANIMATION.easings.join('|')}, delay?, duration? }. active:true is REQUIRED ` +
|
|
101
|
+
'(a type alone is not consent), the type is UNDERSCORED, and it is base-only. Anything ' +
|
|
102
|
+
'else renders no animation at all, with no error.');
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The warning for a `config.animation` write that will not animate.
|
|
106
|
+
*
|
|
107
|
+
* A WARNING rather than a refusal, for the reason `unknownValueNote` records:
|
|
108
|
+
* the platform STORES whatever it is given. But unlike a normalised string,
|
|
109
|
+
* every miss here renders NOTHING rather than something else — so the note says
|
|
110
|
+
* what will happen, not merely what the value is not.
|
|
111
|
+
*/
|
|
112
|
+
export function animationNote(value) {
|
|
113
|
+
const has = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
114
|
+
const types = ANIMATION.types.join(', ');
|
|
115
|
+
if (!has(value)) {
|
|
116
|
+
return (`config.animation is an OBJECT, not a ${typeof value === 'string' ? 'string' : typeof value}: ` +
|
|
117
|
+
`{ active: true, type: "<one of ${types}>", easing, delay, duration }. The renderer reads it ` +
|
|
118
|
+
'as map[string]interface{}, so this value is the zero value and the node will not animate — ' +
|
|
119
|
+
'stored, saved and published with no error at any step.');
|
|
120
|
+
}
|
|
121
|
+
const notes = [];
|
|
122
|
+
if (value.active !== true) {
|
|
123
|
+
notes.push('active:true is missing, and a type alone is deliberately not consent — the panel keeps the ' +
|
|
124
|
+
'type when the switch goes off, so the renderer emits nothing without it');
|
|
125
|
+
}
|
|
126
|
+
const t = value.type;
|
|
127
|
+
if (typeof t !== 'string' || !ANIMATION.types.includes(t)) {
|
|
128
|
+
const near = typeof t === 'string' ? t.replace(/-/g, '_') : '';
|
|
129
|
+
notes.push(`type ${JSON.stringify(t)} is not one of ${types}` +
|
|
130
|
+
(near && ANIMATION.types.includes(near)
|
|
131
|
+
? ` — the stored spelling uses UNDERSCORES, so you want "${near}"`
|
|
132
|
+
: '') +
|
|
133
|
+
'; an unrecognised type emits no keyframes and no rule');
|
|
134
|
+
}
|
|
135
|
+
const e = value.easing;
|
|
136
|
+
if (e !== undefined && (typeof e !== 'string' || !ANIMATION.easings.includes(e))) {
|
|
137
|
+
// The mild one: the animation still RUNS, wearing the wrong curve.
|
|
138
|
+
notes.push(`easing ${JSON.stringify(e)} is outside ${ANIMATION.easings.join(', ')}, so the renderer ` +
|
|
139
|
+
`uses "${ANIMATION.easingFallback}" — the animation runs, with a curve you did not choose`);
|
|
140
|
+
}
|
|
141
|
+
if (!notes.length)
|
|
142
|
+
return null;
|
|
143
|
+
return `config.animation will not do what this says. ${notes.join('. ')}.`;
|
|
144
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -13,6 +13,7 @@ import { registerLiveTools } from './tools/live.js';
|
|
|
13
13
|
import { registerStoreTools } from './tools/store.js';
|
|
14
14
|
import { registerImportTools } from './tools/importpage.js';
|
|
15
15
|
import { registerUndoTools } from './tools/undo.js';
|
|
16
|
+
import { registerThemeTools } from './tools/theme.js';
|
|
16
17
|
/**
|
|
17
18
|
* Sent on every handshake, so it is short, and says nothing a tool description
|
|
18
19
|
* already says. Counts come from the generated source records, never literals.
|
|
@@ -70,6 +71,7 @@ export function createServer(ctx = buildContext()) {
|
|
|
70
71
|
const pageSession = registerPageTools(server, ctx);
|
|
71
72
|
registerLiveTools(server, ctx, pageSession);
|
|
72
73
|
registerStoreTools(server, ctx);
|
|
74
|
+
registerThemeTools(server, ctx);
|
|
73
75
|
registerImportTools(server, ctx, pageSession);
|
|
74
76
|
registerUndoTools(server, ctx);
|
|
75
77
|
return server;
|
package/dist/tools/live.js
CHANGED
|
@@ -405,14 +405,18 @@ export function registerLiveTools(server, ctx, session) {
|
|
|
405
405
|
server.registerTool('sb_media_upload', {
|
|
406
406
|
description: 'Put an image into the media library and get its URL back, ready for sb_set. Takes a ' +
|
|
407
407
|
'local path, a URL, or a SEARCH — `query` returns real photographs with their own ' +
|
|
408
|
-
'descriptions, and `pick` uploads the one you chose
|
|
408
|
+
'descriptions, and `pick` uploads the one you chose, or several at once to stock a ' +
|
|
409
|
+
'site you just built. The only way to add an image.',
|
|
409
410
|
inputSchema: {
|
|
410
411
|
site_id: z.string().optional(),
|
|
411
412
|
path: z.string().optional().describe('A file on this machine'),
|
|
412
413
|
url: z.string().optional().describe('Fetched, then uploaded'),
|
|
413
414
|
query: z.string().optional().describe('Search real photographs; read the descriptions, then pick'),
|
|
414
415
|
orientation: z.enum(['landscape', 'portrait', 'square']).optional(),
|
|
415
|
-
pick: z
|
|
416
|
+
pick: z
|
|
417
|
+
.union([z.number().int(), z.array(z.number().int())])
|
|
418
|
+
.optional()
|
|
419
|
+
.describe('The id of the search result to upload — or several ids, which stocks a site in one call'),
|
|
416
420
|
name: z.string().optional(),
|
|
417
421
|
folder_id: z.string().optional(),
|
|
418
422
|
dry_run: z.boolean().optional(),
|
|
@@ -446,48 +450,106 @@ export function registerLiveTools(server, ctx, session) {
|
|
|
446
450
|
}
|
|
447
451
|
throw e;
|
|
448
452
|
}
|
|
449
|
-
|
|
450
|
-
|
|
453
|
+
// STOCKING A SITE IS WHY THIS TAKES SEVERAL. One search answers with
|
|
454
|
+
// eight photographs and a gallery band wants six of them — and a site
|
|
455
|
+
// this server has just built has an EMPTY library, so every picture
|
|
456
|
+
// slot in every pattern is a sentence until somebody fills it. One
|
|
457
|
+
// pick per call made that twelve round trips for one band, which is
|
|
458
|
+
// how a correct rule becomes a rule nobody follows.
|
|
459
|
+
//
|
|
460
|
+
// It does NOT weaken rule 7. What that rule protects is that somebody
|
|
461
|
+
// LOOKED: reading eight descriptions and choosing six is the same act
|
|
462
|
+
// of choosing as reading eight and choosing one. What it forbids is
|
|
463
|
+
// uploading a hit nobody read, and no `pick` still uploads nothing.
|
|
464
|
+
const wanted = pick === undefined ? [] : Array.isArray(pick) ? pick : [pick];
|
|
465
|
+
const chosen = wanted
|
|
466
|
+
.map((id) => photos.find((p) => p.id === id))
|
|
467
|
+
.filter((p) => p !== undefined);
|
|
468
|
+
const absent = wanted.filter((id) => !photos.some((p) => p.id === id));
|
|
469
|
+
// A PARTIAL PICK IS REFUSED WHOLE rather than half-uploaded. The caller
|
|
470
|
+
// named a set; delivering some of it and reporting the rest as a note
|
|
471
|
+
// leaves them to work out which slots they can still fill.
|
|
472
|
+
if (chosen.length === 0 || absent.length > 0) {
|
|
451
473
|
return text({
|
|
452
|
-
...(
|
|
474
|
+
...(absent.length === 1 ? { no_such_pick: absent[0] } : {}),
|
|
475
|
+
...(absent.length > 1 ? { no_such_picks: absent } : {}),
|
|
453
476
|
found: photos.map((p) => ({
|
|
454
477
|
pick: p.id,
|
|
455
478
|
shows: p.alt || '(the photographer left no description)',
|
|
456
479
|
size: `${p.width}x${p.height}`,
|
|
457
480
|
by: p.photographer,
|
|
458
481
|
})),
|
|
459
|
-
next: 'Read what each one SHOWS, then re-call with pick:<id> and dry_run:false
|
|
460
|
-
|
|
482
|
+
next: 'Read what each one SHOWS, then re-call with pick:<id> and dry_run:false — or ' +
|
|
483
|
+
'pick:[<id>,<id>,…] to take several in one call, which is what fills a gallery ' +
|
|
484
|
+
"band. Each photo is uploaded into this site's own library, never hotlinked.",
|
|
461
485
|
licence: ctx.notices.once('stock_licence', 'These are Pexels photographs: free for commercial use, with attribution ' +
|
|
462
486
|
'appreciated rather than required, so a storefront can carry one without printing ' +
|
|
463
487
|
'a credit line nobody asked for. The photographer comes back with each result if ' +
|
|
464
488
|
'you want to credit anyway.'),
|
|
465
489
|
});
|
|
466
490
|
}
|
|
491
|
+
// ONE PICK KEEPS THE SHAPE IT HAS ALWAYS HAD. A caller that asked for
|
|
492
|
+
// one photograph gets one answer about one photograph; only a caller
|
|
493
|
+
// that asked for several is handed a list to read.
|
|
494
|
+
const solo = chosen.length === 1 ? chosen[0] : undefined;
|
|
467
495
|
if (dry_run !== false) {
|
|
468
496
|
return text({
|
|
469
497
|
dry_run: true,
|
|
470
|
-
would_upload: chosen.url,
|
|
471
|
-
shows: chosen.alt,
|
|
472
|
-
by: chosen.photographer,
|
|
498
|
+
would_upload: solo ? solo.url : chosen.map((c) => c.url),
|
|
499
|
+
shows: solo ? solo.alt : chosen.map((c) => c.alt),
|
|
500
|
+
by: solo ? solo.photographer : chosen.map((c) => c.photographer),
|
|
473
501
|
into: site_id,
|
|
474
502
|
note: 'Nothing was sent. Re-call with dry_run:false to upload.',
|
|
475
503
|
});
|
|
476
504
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
505
|
+
// NOT ATOMIC, AND IT MUST NOT PRETEND TO BE. Each photo is its own
|
|
506
|
+
// upload, so the fifth failing does not un-upload the four that
|
|
507
|
+
// landed — throwing here would leave the caller with four images in
|
|
508
|
+
// the library, no idea which, and an error that names none of them.
|
|
509
|
+
// The same reasoning `sb_import_site` records for its per-page report.
|
|
510
|
+
const uploaded = [];
|
|
511
|
+
const failed = [];
|
|
512
|
+
for (const one of chosen) {
|
|
513
|
+
try {
|
|
514
|
+
const asset = await uploadMedia(ctx, site_id, {
|
|
515
|
+
url: one.url,
|
|
516
|
+
// THE DESCRIPTION BECOMES THE NAME, so the library is searchable
|
|
517
|
+
// by what the photographs show and the alt on the page means
|
|
518
|
+
// something. A caller-supplied name can only speak for ONE photo,
|
|
519
|
+
// so it is honoured only when one was picked.
|
|
520
|
+
name: (solo ? name : undefined) ?? one.alt ?? undefined,
|
|
521
|
+
folderId: folder_id,
|
|
522
|
+
});
|
|
523
|
+
uploaded.push({
|
|
524
|
+
asset,
|
|
525
|
+
shows: one.alt,
|
|
526
|
+
credit: { by: one.photographer, profile: one.photographer_url, photo: one.page_url },
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
catch (e) {
|
|
530
|
+
failed.push({ pick: one.id, why: e.message });
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (solo) {
|
|
534
|
+
const only = uploaded[0];
|
|
535
|
+
if (!only)
|
|
536
|
+
throw new Error(`sbuilder: ${failed[0]?.why ?? 'the upload failed'}`);
|
|
537
|
+
const asset = only.asset;
|
|
538
|
+
return text({
|
|
539
|
+
asset,
|
|
540
|
+
shows: solo.alt,
|
|
541
|
+
credit: only.credit,
|
|
542
|
+
next: asset.url
|
|
543
|
+
? `Use it: sb_set id "<node>", namespace specials, keys { "src": ${JSON.stringify(asset.url)} }`
|
|
544
|
+
: 'Uploaded, but the server returned no url — read it back with sb_media_list.',
|
|
545
|
+
});
|
|
546
|
+
}
|
|
484
547
|
return text({
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
: 'Uploaded, but the server returned no url — read it back with sb_media_list.',
|
|
548
|
+
uploaded,
|
|
549
|
+
...(failed.length ? { failed } : {}),
|
|
550
|
+
next: `${uploaded.length} in this site's library now. A layout pattern reads the library ` +
|
|
551
|
+
'when it builds, so sb_template_use will put these into its picture slots — or set ' +
|
|
552
|
+
'one on a node directly with sb_set namespace specials, keys { "src": "<url>" }.',
|
|
491
553
|
});
|
|
492
554
|
}
|
|
493
555
|
if (!path && !url) {
|
package/dist/tools/page.js
CHANGED
|
@@ -8,7 +8,7 @@ import { baseOnlyNote } from '../domains/site/baseonly.js';
|
|
|
8
8
|
import { detachNote, presetIdOf, presetLayer } from '../domains/site/theme.js';
|
|
9
9
|
import { inertHintsFor } from '../domains/site/inert.js';
|
|
10
10
|
import { hasSeed, seedDocument, seedSummary, seededTypes } from '../domains/site/storepage.js';
|
|
11
|
-
import { unknownValueNote } from '../domains/site/vocabulary.js';
|
|
11
|
+
import { animationNote, unknownValueNote } from '../domains/site/vocabulary.js';
|
|
12
12
|
import { skinLevelNote } from '../domains/site/fieldskin.js';
|
|
13
13
|
import { siteTheme } from '../domains/site/theme-fetch.js';
|
|
14
14
|
import { request, redact } from '../transport/http.js';
|
|
@@ -520,10 +520,15 @@ export function registerPageTools(server, ctx) {
|
|
|
520
520
|
if (e.namespace !== 'config')
|
|
521
521
|
continue;
|
|
522
522
|
for (const [k, v] of Object.entries(e.keys)) {
|
|
523
|
-
|
|
523
|
+
// THE ENTRANCE ANIMATION IS ITS OWN QUESTION, because it is an object
|
|
524
|
+
// rather than a word and every way of missing it renders NOTHING
|
|
525
|
+
// rather than a normalised something. Keyed on the whole value: two
|
|
526
|
+
// nodes given the same wrong animation deserve one answer, and two
|
|
527
|
+
// given different wrong ones deserve two.
|
|
528
|
+
const n = k === 'animation' ? animationNote(v) : unknownValueNote(k, v);
|
|
524
529
|
if (!n)
|
|
525
530
|
continue;
|
|
526
|
-
const once = ctx.notices.once(`config-value:${k}=${
|
|
531
|
+
const once = ctx.notices.once(`config-value:${k}=${JSON.stringify(v)}`, n);
|
|
527
532
|
if (once)
|
|
528
533
|
valueNotes.push(once);
|
|
529
534
|
}
|
|
@@ -794,6 +799,7 @@ export function registerPageTools(server, ctx) {
|
|
|
794
799
|
into: page_id,
|
|
795
800
|
tokens_from: fromTheme ? "this site's theme — the page has no look of its own yet" : 'this page',
|
|
796
801
|
images_available: pool.length,
|
|
802
|
+
...(pattern.images ? { pictures_wanted: pattern.images } : {}),
|
|
797
803
|
note: 'Composed against THIS page\'s tokens, not copied — the same heading ink, button ' +
|
|
798
804
|
'fill and section padding the page already uses. Pass dry_run:false to add it.',
|
|
799
805
|
});
|
|
@@ -805,11 +811,19 @@ export function registerPageTools(server, ctx) {
|
|
|
805
811
|
nodes: ids.length,
|
|
806
812
|
into: page_id,
|
|
807
813
|
rev: doc.rev,
|
|
808
|
-
|
|
814
|
+
// A SHORT LIBRARY IS THE ORDINARY STATE OF A SITE THIS SERVER BUILT,
|
|
815
|
+
// not an edge case — nothing has been uploaded yet, so every picture
|
|
816
|
+
// slot in every pattern is a sentence. Saying "there are no images"
|
|
817
|
+
// and stopping leaves the agent to rediscover the fix; saying how
|
|
818
|
+
// many this band wanted and naming the two calls that fill them is
|
|
819
|
+
// the same fact with the next step attached.
|
|
820
|
+
...(pattern.images && pool.length < pattern.images
|
|
809
821
|
? {
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
'the
|
|
822
|
+
pictures: `This band has ${pattern.images} picture slot${pattern.images > 1 ? 's' : ''} and the ` +
|
|
823
|
+
`library offered ${pool.length}; the rest say so in words rather than showing a grey ` +
|
|
824
|
+
'box. Fill them: sb_media_upload query:"<what the band should show>" reads back real ' +
|
|
825
|
+
'photographs with their own descriptions, then pick:[…] uploads the ones you chose in ' +
|
|
826
|
+
'one call. Re-run this pattern afterwards and it takes them.',
|
|
813
827
|
}
|
|
814
828
|
: {}),
|
|
815
829
|
...(fromTheme
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { text } from '../mcp/response.js';
|
|
3
|
+
import { request } from '../transport/http.js';
|
|
4
|
+
import { siteToken } from './credentialpick.js';
|
|
5
|
+
import { siteFor } from './context.js';
|
|
6
|
+
import { STARTER_THEME } from '../domains/site/theme.js';
|
|
7
|
+
/** The site's theme, or the starter when it has never saved one. */
|
|
8
|
+
async function readTheme(ctx, siteId) {
|
|
9
|
+
const got = (await request({
|
|
10
|
+
base: ctx.base,
|
|
11
|
+
method: 'GET',
|
|
12
|
+
path: `/api/sites/${encodeURIComponent(siteId)}/theme`,
|
|
13
|
+
token: siteToken(ctx),
|
|
14
|
+
fetchImpl: ctx.fetchImpl,
|
|
15
|
+
}));
|
|
16
|
+
// A NEW SITE ANSWERS 200 {"theme": null} — the platform's own comment calls
|
|
17
|
+
// that "the NORMAL first-visit state, not a failure". Starting from the
|
|
18
|
+
// starter rather than from `{}` is what makes the first write store a
|
|
19
|
+
// COMPLETE theme instead of a palette with one token in it.
|
|
20
|
+
if (got?.theme && typeof got.theme === 'object')
|
|
21
|
+
return { theme: got.theme, origin: 'site' };
|
|
22
|
+
return { theme: structuredClone(STARTER_THEME), origin: 'starter' };
|
|
23
|
+
}
|
|
24
|
+
export function registerThemeTools(server, ctx) {
|
|
25
|
+
server.registerTool('sb_theme', {
|
|
26
|
+
description: "The site's palette and type scale — the layer every element's style preset resolves " +
|
|
27
|
+
'from, so one token repaints every page at once. Call it with nothing to read what the ' +
|
|
28
|
+
'site actually has. `colors` and `text_styles` PATCH the saved document: what you do not ' +
|
|
29
|
+
'name is kept.',
|
|
30
|
+
inputSchema: {
|
|
31
|
+
site_id: z.string().optional(),
|
|
32
|
+
colors: z
|
|
33
|
+
.record(z.string())
|
|
34
|
+
.optional()
|
|
35
|
+
.describe('Token id -> CSS colour, e.g. { "heading": "#2E2A3B", "primary": "#E8557A" }'),
|
|
36
|
+
text_styles: z
|
|
37
|
+
.record(z.record(z.string()))
|
|
38
|
+
.optional()
|
|
39
|
+
.describe('Text style slug -> base declarations, e.g. { "h1": { "fontSize": "48px" } }'),
|
|
40
|
+
dry_run: z.boolean().optional(),
|
|
41
|
+
},
|
|
42
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
43
|
+
}, async ({ site_id: given, colors, text_styles, dry_run }) => {
|
|
44
|
+
const site_id = siteFor(ctx, given);
|
|
45
|
+
const { theme, origin } = await readTheme(ctx, site_id);
|
|
46
|
+
// READ. What the site actually paints from, which is what rule 0 wants
|
|
47
|
+
// before anything is decided.
|
|
48
|
+
if (!colors && !text_styles) {
|
|
49
|
+
return text({
|
|
50
|
+
origin: origin === 'site'
|
|
51
|
+
? 'this site, saved'
|
|
52
|
+
: 'the STARTER theme — this site has never saved one, so this is what it renders ' +
|
|
53
|
+
'with and what a first write would be built from',
|
|
54
|
+
version: theme.version,
|
|
55
|
+
colors: Object.fromEntries((theme.colors ?? []).map((c) => [c.id, c.value])),
|
|
56
|
+
text_styles: Object.fromEntries((theme.textStyles ?? []).map((t) => [t.slug, t.base ?? {}])),
|
|
57
|
+
schemes: (theme.schemes ?? []).map((s) => s.id),
|
|
58
|
+
presets: (theme.presets ?? []).length,
|
|
59
|
+
note: ctx.notices.once('theme_leverage', 'A style preset compiles to a class rule BENEATH a node\'s own values, so every node ' +
|
|
60
|
+
'that has not been given a literal follows these tokens. Changing one here is the ' +
|
|
61
|
+
'cheapest way to restyle a whole site — and a literal written with sb_set detaches ' +
|
|
62
|
+
'that node from it permanently.'),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
// PATCH. Unknown ids are REFUSED rather than added: a token nothing
|
|
66
|
+
// resolves from is a value the platform stores and no renderer reads,
|
|
67
|
+
// which is the silent-failure family this server exists to close — and a
|
|
68
|
+
// typo would land there rather than on the colour the caller meant.
|
|
69
|
+
const changes = [];
|
|
70
|
+
const known = new Set((theme.colors ?? []).map((c) => c.id));
|
|
71
|
+
const unknown = Object.keys(colors ?? {}).filter((id) => !known.has(id));
|
|
72
|
+
if (unknown.length) {
|
|
73
|
+
throw new Error(`sbuilder: no colour token named ${unknown.map((u) => JSON.stringify(u)).join(', ')} on ` +
|
|
74
|
+
`this site. Its tokens are: ${[...known].join(', ')}. Every style preset resolves ` +
|
|
75
|
+
'through these ids, so a new one would be stored and read by nothing.');
|
|
76
|
+
}
|
|
77
|
+
for (const [id, value] of Object.entries(colors ?? {})) {
|
|
78
|
+
const token = theme.colors.find((c) => c.id === id);
|
|
79
|
+
if (token.value === value)
|
|
80
|
+
continue;
|
|
81
|
+
changes.push({ what: `colors.${id}`, from: token.value, to: value });
|
|
82
|
+
token.value = value;
|
|
83
|
+
}
|
|
84
|
+
const slugs = new Set((theme.textStyles ?? []).map((t) => t.slug));
|
|
85
|
+
const badSlugs = Object.keys(text_styles ?? {}).filter((s) => !slugs.has(s));
|
|
86
|
+
if (badSlugs.length) {
|
|
87
|
+
throw new Error(`sbuilder: no text style named ${badSlugs.map((b) => JSON.stringify(b)).join(', ')} on ` +
|
|
88
|
+
`this site. Its slugs are: ${[...slugs].join(', ')}. A preset names a style by SLUG ` +
|
|
89
|
+
'and the compiled --wb-ts-<slug>-<prop> variables are built from it.');
|
|
90
|
+
}
|
|
91
|
+
for (const [slug, decls] of Object.entries(text_styles ?? {})) {
|
|
92
|
+
const style = theme.textStyles.find((t) => t.slug === slug);
|
|
93
|
+
style.base = style.base ?? {};
|
|
94
|
+
for (const [prop, value] of Object.entries(decls)) {
|
|
95
|
+
const before = style.base[prop] ?? '(unset)';
|
|
96
|
+
if (before === value)
|
|
97
|
+
continue;
|
|
98
|
+
changes.push({ what: `textStyles.${slug}.${prop}`, from: before, to: value });
|
|
99
|
+
style.base[prop] = value;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (!changes.length) {
|
|
103
|
+
return text({ unchanged: true, note: 'Every value named already holds that value.' });
|
|
104
|
+
}
|
|
105
|
+
// The document goes back WHOLE, so this can only ever be true — asserted
|
|
106
|
+
// anyway, because the one failure this endpoint cannot take back is a
|
|
107
|
+
// theme with nothing in it.
|
|
108
|
+
if (!theme.colors?.length || !theme.presets?.length) {
|
|
109
|
+
throw new Error('sbuilder: refusing to save a theme with no colours or no presets.');
|
|
110
|
+
}
|
|
111
|
+
if (dry_run !== false) {
|
|
112
|
+
return text({
|
|
113
|
+
dry_run: true,
|
|
114
|
+
would_change: changes,
|
|
115
|
+
on: site_id,
|
|
116
|
+
built_from: origin === 'site' ? "this site's saved theme" : 'the starter theme',
|
|
117
|
+
note: 'Nothing was sent. This is SITE-WIDE: every page, and every node that has not been ' +
|
|
118
|
+
'given a literal of its own. Re-call with dry_run:false.',
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
await request({
|
|
122
|
+
base: ctx.base,
|
|
123
|
+
method: 'PUT',
|
|
124
|
+
path: `/api/sites/${encodeURIComponent(site_id)}/theme`,
|
|
125
|
+
token: siteToken(ctx),
|
|
126
|
+
body: { theme },
|
|
127
|
+
fetchImpl: ctx.fetchImpl,
|
|
128
|
+
});
|
|
129
|
+
return text({
|
|
130
|
+
changed: changes,
|
|
131
|
+
on: site_id,
|
|
132
|
+
next: 'Republish the pages that should show it — a theme is compiled into each page\'s ' +
|
|
133
|
+
'stylesheet, so a saved page keeps the old palette until it is published again.',
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sbuilder-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"description": "MCP server that designs and operates a Store Builder site — pages, data, theme and publish — through the platform's own API and live-edit protocol.",
|
|
5
5
|
"mcpName": "io.github.vuluu2k/sbuilder-mcp",
|
|
6
6
|
"type": "module",
|