sbuilder-mcp 0.61.0 → 0.61.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,15 @@ 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.61.1] - 2026-09-17
10
+
11
+ ### Added
12
+ - `sb_connect` now reports how old the generated catalog's platform knowledge is, in words (e.g. "45 days old"), along with the web_builder commit it was generated from, so a stale install reads as staleness instead of as a missing element or key.
13
+ - `sb_set`'s precondition warning now also covers `filterMatch: "all"` on filter elements, which only takes effect where a product can hold several of a source's values.
14
+
15
+ ### Fixed
16
+ - `sb_set`'s precondition warning no longer reports a false positive when a sibling `specials` key is simply unset, since a `null` clause now correctly means "not set" instead of being compared as a literal value.
17
+
9
18
  ## [0.61.0] - 2026-09-16
10
19
 
11
20
  ### Added
package/CHANGELOG.vi.md CHANGED
@@ -6,6 +6,15 @@ 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.61.1] - 2026-09-17
10
+
11
+ ### Added
12
+ - `sb_connect` giờ báo cáo kiến thức nền tảng của catalog được sinh ra đã cũ đến mức nào, bằng lời (ví dụ "45 days old"), cùng với commit web_builder mà nó được sinh ra từ đó, để một bản cài đặt lỗi thời được đọc đúng là lỗi thời thay vì bị hiểu nhầm là thiếu element hay key.
13
+ - Cảnh báo tiền điều kiện của `sb_set` giờ bao phủ thêm `filterMatch: "all"` trên các element bộ lọc, giá trị này chỉ có tác dụng ở nơi một sản phẩm có thể mang nhiều giá trị của một nguồn.
14
+
15
+ ### Fixed
16
+ - Cảnh báo tiền điều kiện của `sb_set` không còn báo sai (false positive) khi một key `specials` liền kề đơn giản là chưa được thiết lập, vì một clause `null` giờ được hiểu đúng là "chưa thiết lập" thay vì bị so sánh như một giá trị literal.
17
+
9
18
  ## [0.61.0] - 2026-09-16
10
19
 
11
20
  ### Added
@@ -1,6 +1,6 @@
1
1
  export const DEAD_KEY_SOURCE = {
2
2
  "files": 4065,
3
- "identifiers": 77969,
3
+ "identifiers": 77979,
4
4
  "seededKeys": 388,
5
5
  "dead": 0
6
6
  };
@@ -36905,6 +36905,38 @@ export const WRITE_PRECONDITIONS = [
36905
36905
  }
36906
36906
  ],
36907
36907
  "otherwise": "no extra row is drawn. It is the way BACK OUT of a browse tree — a link to the category the page is serving — so it needs the page to be followed and the rows to be links. Under in-place filtering, \"everything\" is already what an untouched filter shows."
36908
+ },
36909
+ {
36910
+ "key": "filterMatch",
36911
+ "value": "all",
36912
+ "types": [
36913
+ "filter-checkbox",
36914
+ "filter-radio",
36915
+ "filter-color",
36916
+ "filter-tag"
36917
+ ],
36918
+ "requires": [
36919
+ {
36920
+ "key": "filterSource",
36921
+ "anyOf": [
36922
+ "category",
36923
+ "attribute",
36924
+ "tag",
36925
+ "blog_category",
36926
+ "course_tag"
36927
+ ]
36928
+ },
36929
+ {
36930
+ "key": "filterBehavior",
36931
+ "anyOf": [
36932
+ "filter",
36933
+ "navigate",
36934
+ "",
36935
+ null
36936
+ ]
36937
+ }
36938
+ ],
36939
+ "otherwise": "the filter combines ticked values with OR, as it always did. Both predicates IGNORE the mode outside these cases rather than honouring it — a product holds ONE brand, one price and one stock state, so \"match all\" on two ticked values could only return an empty shelf, and a stale URL degrades to the ordinary OR instead of to a blank page."
36908
36940
  }
36909
36941
  ];
36910
36942
  export const HOVER_HOMES = {
@@ -0,0 +1,7 @@
1
+ // GENERATED by scripts/gen-catalog.ts — do not edit by hand.
2
+ // The platform commit every other generated file here describes.
3
+ export const PLATFORM_SOURCE = {
4
+ "commit": "d6a68b77a7a00a6a9ca66da5161d57e845aae628",
5
+ "committedAt": "2026-09-17T00:01:02+07:00",
6
+ "dirty": false
7
+ };
@@ -414,6 +414,27 @@ export function deadKeyNote(namespace, key) {
414
414
  * not have, and a newer deployment may honour a combination this catalog was
415
415
  * generated before. The sentence says what the PAGE does, never "invalid".
416
416
  */
417
+ /**
418
+ * One clause against one stored value — a MIRROR of `holds` in
419
+ * schema/src/filters/preconditions.ts, and the two must agree.
420
+ *
421
+ * `null` in a clause means NOT SET, which is a real state rather than a hole:
422
+ * a node written before a key existed does not carry it, and both renderers
423
+ * then read their own fallback. Without this mapping the two copies disagreed
424
+ * within an hour of being written — this one called a perfectly good tag filter
425
+ * broken, which is a FALSE POSITIVE and worse than the silence the whole
426
+ * mechanism replaced, because a caller warned about correct work stops reading
427
+ * the warnings.
428
+ *
429
+ * It is a copy because this package has no runtime dependency on the schema —
430
+ * the table arrives as generated data. The cases that separate the two are
431
+ * asserted on both sides (`test/write-preconditions.test.ts` here,
432
+ * `schema/test/filter-preconditions.test.ts` there) rather than left to the
433
+ * reading.
434
+ */
435
+ function holdsClause(c, stored) {
436
+ return c.anyOf.includes(stored === undefined ? null : stored);
437
+ }
417
438
  export function preconditionNotes(type, specials) {
418
439
  const out = [];
419
440
  for (const p of WRITE_PRECONDITIONS) {
@@ -421,13 +442,15 @@ export function preconditionNotes(type, specials) {
421
442
  continue;
422
443
  if (specials[p.key] !== p.value)
423
444
  continue;
424
- const missing = p.requires.filter((c) => !c.anyOf.includes(specials[c.key]));
445
+ const missing = p.requires.filter((c) => !holdsClause(c, specials[c.key]));
425
446
  if (missing.length === 0)
426
447
  continue;
427
448
  const held = missing
428
449
  .map((c) => {
429
450
  const now = specials[c.key];
430
- const want = c.anyOf.map((v) => JSON.stringify(v)).join(' or ');
451
+ const want = c.anyOf
452
+ .map((v) => (v === null ? 'to be left unset' : JSON.stringify(v)))
453
+ .join(' or ');
431
454
  // An ABSENT key is not `null`, and saying so would send a caller looking
432
455
  // for a null they never wrote. The platform's own default applies here,
433
456
  // which is a different thing to fix than a wrong value.
@@ -2,6 +2,26 @@ import { z } from 'zod';
2
2
  import { request } from '../transport/http.js';
3
3
  import { text } from '../mcp/response.js';
4
4
  import { API_OPERATIONS } from '../catalog/api.generated.js';
5
+ import { PLATFORM_SOURCE } from '../catalog/source.generated.js';
6
+ /**
7
+ * The provenance line, or '' when the generator had no git to ask.
8
+ *
9
+ * Reads as an AGE rather than a commit id, because the age is the actionable
10
+ * half — nobody can tell whether `d6a68b7` is recent, and everybody can tell
11
+ * whether four months is. The id rides along for the person who then has to
12
+ * find it.
13
+ */
14
+ export function catalogAge(now = new Date()) {
15
+ const { commit, committedAt, dirty } = PLATFORM_SOURCE;
16
+ if (!commit || !committedAt)
17
+ return '';
18
+ const days = Math.floor((now.getTime() - new Date(committedAt).getTime()) / 86_400_000);
19
+ const age = days <= 1 ? 'today' : days < 45 ? `${days} days old` : `${Math.floor(days / 30)} months old`;
20
+ return (`Built from web_builder ${commit.slice(0, 9)} (${committedAt.slice(0, 10)}, ${age})` +
21
+ (dirty ? ', from an UNCOMMITTED tree — it may describe work no deployment has' : '') +
22
+ `. The platform reports no version of its own, so nothing here can compare the two: if an ` +
23
+ `element or key you expect is missing, regenerate rather than working around it.`);
24
+ }
5
25
  /**
6
26
  * Log in and report what this server can actually reach.
7
27
  *
@@ -26,6 +46,7 @@ export async function connect(ctx, args) {
26
46
  ...(ctx.siteId ? { site: ctx.siteId } : {}),
27
47
  ...(ctx.siteId && ctx.siteName ? { site_name: ctx.siteName } : {}),
28
48
  operations: API_OPERATIONS.length,
49
+ ...(catalogAge() ? { catalog: catalogAge() } : {}),
29
50
  note: ctx.siteId
30
51
  ? `Connected with an API key alone, on site ${ctx.siteId} (SB_SITE)${ctx.siteName ? `, the store called ${JSON.stringify(ctx.siteName)}` : ''}. Every tool ` +
31
52
  'defaults to it, so site_id is optional. Set SB_EMAIL and SB_PASSWORD as well if ' +
@@ -71,6 +92,7 @@ export async function connect(ctx, args) {
71
92
  sites: (listed?.sites ?? []).map((s) => ({ id: s.id, name: s.name })),
72
93
  api_key: ctx.apiKey ? 'present' : 'missing',
73
94
  operations: API_OPERATIONS.length,
95
+ ...(catalogAge() ? { catalog: catalogAge() } : {}),
74
96
  };
75
97
  if (!ctx.apiKey) {
76
98
  result.note =
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbuilder-mcp",
3
- "version": "0.61.0",
3
+ "version": "0.61.1",
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",