sbuilder-mcp 0.76.4 → 0.78.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 +30 -0
- package/CHANGELOG.vi.md +30 -0
- package/README.md +5 -5
- package/README.vi.md +5 -5
- package/dist/catalog/api.generated.js +17805 -5286
- package/dist/catalog/appscaffolds.generated.js +1 -1
- package/dist/catalog/deadkeys.generated.js +3 -3
- package/dist/catalog/element-search.js +178 -13
- package/dist/catalog/elements.generated.js +370 -91
- package/dist/catalog/formrules.generated.js +29 -0
- package/dist/catalog/shapes.generated.js +4483 -1593
- package/dist/catalog/source.generated.js +2 -2
- package/dist/catalog/translations.generated.js +11 -2
- package/dist/domains/site/builder.js +42 -3
- package/dist/domains/site/findings.js +28 -3
- package/dist/domains/site/formdoc.js +185 -0
- package/dist/domains/site/navhref.js +77 -3
- package/dist/domains/site/readiness.js +1 -2
- package/dist/domains/site/review.js +188 -7
- package/dist/domains/site/validate.js +3 -0
- package/dist/domains/site/writecheck.js +309 -0
- package/dist/mcp/notices.js +8 -0
- package/dist/server.js +6 -6
- package/dist/tools/importpage.js +1 -1
- package/dist/tools/live.js +45 -24
- package/dist/tools/overlay.js +2 -2
- package/dist/tools/page.js +362 -134
- package/dist/tools/store.js +77 -16
- package/dist/tools/undo.js +70 -15
- package/dist/transport/http.js +21 -0
- package/dist/transport/pages.js +1 -1
- package/package.json +1 -1
package/dist/tools/live.js
CHANGED
|
@@ -2,6 +2,7 @@ import { randomBytes } from 'node:crypto';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { text, images } from '../mcp/response.js';
|
|
4
4
|
import { BINDING_SOURCES, ELEMENTS } from '../catalog/elements.generated.js';
|
|
5
|
+
import { bindableFields } from '../domains/site/writecheck.js';
|
|
5
6
|
import { previewUrl } from '../vision/preview.js';
|
|
6
7
|
import { uploadMedia } from '../transport/media.js';
|
|
7
8
|
import { request } from '../transport/http.js';
|
|
@@ -26,7 +27,7 @@ import { RealtimeSocket } from '../transport/socket.js';
|
|
|
26
27
|
import { LiveSession } from '../live/session.js';
|
|
27
28
|
import { refuseAppBlockInterior } from '../domains/site/builder.js';
|
|
28
29
|
import { soft } from '../domains/site/guard.js';
|
|
29
|
-
import { hrefPatches, hasPurchaseBinding, PRODUCT_ACTION_BINDING_ID, } from '../domains/site/navhref.js';
|
|
30
|
+
import { hrefPatches, hasPurchaseBinding, liveEventTable, PAYLOAD_NEEDS, PRODUCT_ACTION_BINDING_ID, } from '../domains/site/navhref.js';
|
|
30
31
|
import { offscreenNodes, offscreenRootOf } from '../domains/site/offscreen.js';
|
|
31
32
|
import { siteToken } from './credentialpick.js';
|
|
32
33
|
import { searchStock, SearchUnavailable, NO_SEARCH_NEXT } from '../transport/stock.js';
|
|
@@ -70,6 +71,24 @@ export function bindNode(doc, id, source, field, action, guard) {
|
|
|
70
71
|
throw new Error(`sbuilder: a binding field must be "specials.<key>", not "${field}". The renderer ignores ` +
|
|
71
72
|
'every other namespace, so the binding would be stored and never applied.');
|
|
72
73
|
}
|
|
74
|
+
// A FIELD THE ELEMENT NEVER PAINTS. `BOUND_SPECIALS` is read off each
|
|
75
|
+
// element's html.go, so a `specials.boundImage` binding on a text-dataset is
|
|
76
|
+
// stored, saved, published and rendered as placeholder text forever. Soft:
|
|
77
|
+
// the list is the catalog's copy of the renderer. The element's own generated
|
|
78
|
+
// binding fields count too — `boundProductId` is read by the runtime, not
|
|
79
|
+
// html.go, and every default product binding carries it.
|
|
80
|
+
const type = node.data?.type ?? '';
|
|
81
|
+
const key = field.slice(dot + 1);
|
|
82
|
+
const allowed = bindableFields(type);
|
|
83
|
+
if (action === undefined && allowed.length) {
|
|
84
|
+
if (!allowed.includes(key)) {
|
|
85
|
+
soft(guard, () => {
|
|
86
|
+
throw new Error(`sbuilder: a ${type} renders a binding only into ${allowed.map((k) => `specials.${k}`).join(', ')} ` +
|
|
87
|
+
`— "${field}" would be stored and never painted. Allowed fields: ${allowed.join(', ')}. ` +
|
|
88
|
+
'Bind one of those, or swap to an element that shows this value (sb_catalog_search).');
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
73
92
|
// A PURCHASE BINDING, which is what makes a button add to the cart.
|
|
74
93
|
//
|
|
75
94
|
// It is not an ordinary binding and cannot be written as one: the renderer
|
|
@@ -109,23 +128,6 @@ export function bindNode(doc, id, source, field, action, guard) {
|
|
|
109
128
|
},
|
|
110
129
|
];
|
|
111
130
|
}
|
|
112
|
-
/**
|
|
113
|
-
* The click-action allow-list that is LIVE for this node.
|
|
114
|
-
*
|
|
115
|
-
* `activeEvents` in the platform, whose whole rule is one line in
|
|
116
|
-
* `ActionTrait.vue`: `return action ? def.binding_events : def.events`. A
|
|
117
|
-
* purchase control is a different kind of control — an unbound button navigates,
|
|
118
|
-
* a bound one hands off to the cart or the checkout — and the two sets are
|
|
119
|
-
* mutually exclusive, because "add this product, then go to an arbitrary URL" is
|
|
120
|
-
* not a thing the cart runtime can express.
|
|
121
|
-
*/
|
|
122
|
-
function liveEventTable(type, node) {
|
|
123
|
-
const meta = ELEMENTS[type];
|
|
124
|
-
if (!meta?.events)
|
|
125
|
-
return undefined;
|
|
126
|
-
const bound = (node.bindings ?? []).some((b) => b?.id === PRODUCT_ACTION_BINDING_ID);
|
|
127
|
-
return (bound && meta.bindingEvents) || meta.events;
|
|
128
|
-
}
|
|
129
131
|
/**
|
|
130
132
|
* Put a click action on a node, or take one off.
|
|
131
133
|
*
|
|
@@ -172,7 +174,7 @@ export function setEvent(doc, id, trigger, action, payload, guard) {
|
|
|
172
174
|
...project(next),
|
|
173
175
|
];
|
|
174
176
|
}
|
|
175
|
-
const table = liveEventTable(
|
|
177
|
+
const table = liveEventTable(doc.doc.nodes, id);
|
|
176
178
|
if (!table) {
|
|
177
179
|
throw new Error(`sbuilder: a ${node.data.type} declares no click actions, so an event on it would be ` +
|
|
178
180
|
'stored and never fired. Elements that do: ' +
|
|
@@ -192,6 +194,21 @@ export function setEvent(doc, id, trigger, action, payload, guard) {
|
|
|
192
194
|
: '') +
|
|
193
195
|
`Allowed: ${allowed.join(', ')}.`);
|
|
194
196
|
}
|
|
197
|
+
// A NAVIGATION WITH NOWHERE TO GO. The renderer turns a sole go_to_url /
|
|
198
|
+
// open_page click into `<a href>` from `payload.url` ALONE (navhref.ts) — an
|
|
199
|
+
// `open_page` with only a page id projects nothing, since neither renderer
|
|
200
|
+
// can resolve one — and a `popup` with no `payload.id` publishes a call with
|
|
201
|
+
// no target that opens nothing. All three store, save and publish clean.
|
|
202
|
+
const need = PAYLOAD_NEEDS[action];
|
|
203
|
+
if (need) {
|
|
204
|
+
const v = (payload ?? {})[need.key];
|
|
205
|
+
if (typeof v !== 'string' || v === '') {
|
|
206
|
+
soft(guard, () => {
|
|
207
|
+
throw new Error(`sbuilder: "${action}" needs payload.${need.key} — without it the click ${need.why}. ` +
|
|
208
|
+
`Expected payload: ${need.shape}.`);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
195
212
|
const value = { id: `ev_${action}`, name: trigger, action, payload: payload ?? {} };
|
|
196
213
|
if (at >= 0) {
|
|
197
214
|
const next = events.map((e, i) => (i === at ? value : e));
|
|
@@ -258,7 +275,7 @@ export function joinRoom(ctx, session, siteId) {
|
|
|
258
275
|
const socket = new RealtimeSocket(`${wsBase}/api/realtime/ws?site=${encodeURIComponent(siteId)}`, tokenFn);
|
|
259
276
|
const live = new LiveSession(socket, {
|
|
260
277
|
onRemote: (patches) => session.applyRemote(patches),
|
|
261
|
-
onDesync: (reason) => session.
|
|
278
|
+
onDesync: (reason) => session.roomDesync(reason),
|
|
262
279
|
onSource: (pageId, rev, peerId) => session.sourceSaved(pageId, rev, peerId),
|
|
263
280
|
onMaster: (kind, id, op, rev) => session.masterSaved(kind, id, op, rev),
|
|
264
281
|
});
|
|
@@ -661,8 +678,10 @@ export function registerLiveTools(server, ctx, session) {
|
|
|
661
678
|
const guard = { force, forced: [] };
|
|
662
679
|
const patches = setEvent(d, id, trigger ?? 'click', action, payload, guard);
|
|
663
680
|
const forced = guard.forced.length ? { forced: guard.forced } : {};
|
|
664
|
-
if (dry_run !== false)
|
|
665
|
-
|
|
681
|
+
if (dry_run !== false) {
|
|
682
|
+
const refuse = session.wouldRefuse(patches);
|
|
683
|
+
return text({ dry_run: true, patches, ...(refuse ? { would_refuse: refuse } : {}), ...forced });
|
|
684
|
+
}
|
|
666
685
|
await session.applyAndSave(patches);
|
|
667
686
|
return text({ node: id, trigger: trigger ?? 'click', action, rev: d.rev, ...forced });
|
|
668
687
|
});
|
|
@@ -692,8 +711,10 @@ export function registerLiveTools(server, ctx, session) {
|
|
|
692
711
|
const guard = { force, forced: [] };
|
|
693
712
|
const patches = bindNode(d, id, source, field, action, guard);
|
|
694
713
|
const forced = guard.forced.length ? { forced: guard.forced } : {};
|
|
695
|
-
if (dry_run !== false)
|
|
696
|
-
|
|
714
|
+
if (dry_run !== false) {
|
|
715
|
+
const refuse = session.wouldRefuse(patches);
|
|
716
|
+
return text({ dry_run: true, patches, ...(refuse ? { would_refuse: refuse } : {}), ...forced });
|
|
717
|
+
}
|
|
697
718
|
await session.applyAndSave(patches);
|
|
698
719
|
return text({ bound: id, source, field, ...(action ? { action } : {}), rev: d.rev, ...forced });
|
|
699
720
|
});
|
package/dist/tools/overlay.js
CHANGED
|
@@ -323,7 +323,7 @@ export async function ensureCartDrawer(ctx, session, siteId, dryRun) {
|
|
|
323
323
|
note: 'Nothing was sent. Re-call with dry_run:false to create it; it shows on every page.',
|
|
324
324
|
};
|
|
325
325
|
}
|
|
326
|
-
const open = session.
|
|
326
|
+
const open = session.page();
|
|
327
327
|
const onPage = open?.siteId === siteId;
|
|
328
328
|
if (onPage)
|
|
329
329
|
await session.applyAndSave([]);
|
|
@@ -365,7 +365,7 @@ export async function relocalizeCartDrawer(ctx, session, siteId, dryRun) {
|
|
|
365
365
|
const raw = settings.settings?.locale;
|
|
366
366
|
// No locale means no language to move the WORDS to; the thumbnail needs none.
|
|
367
367
|
const language = typeof raw === 'string' && raw ? cartSeedLocale(raw) : null;
|
|
368
|
-
const open = session.
|
|
368
|
+
const open = session.page();
|
|
369
369
|
const composed = open?.siteId === siteId
|
|
370
370
|
? Object.values(session.current().doc.nodes).find((n) => n.specials?.overlayId === cart.id)
|
|
371
371
|
: undefined;
|