stylegallery 0.1.10 → 0.1.12
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/AGENTS.md +2 -2
- package/GOVERNANCE.md +1 -1
- package/README.md +38 -35
- package/consumer-reference/agent-native/README.md +5 -5
- package/consumer-reference/agent-native/v2/material-registry.json +1 -1
- package/design-engineering/loading-and-feedback.md +1 -0
- package/design-engineering/native-interaction-contracts.md +2 -0
- package/design-engineering/state-management/patterns/latest-request-wins.md +2 -0
- package/design-engineering/state-management/patterns/optimistic-overlay.md +1 -0
- package/design-engineering/state-management/patterns/single-flight.md +2 -0
- package/design-engineering/state-management/patterns/unsaved-navigation.md +2 -0
- package/design-engineering/state-management/patterns/url-state.md +1 -0
- package/design-engineering/state-management/recipes/delete-undo.md +1 -0
- package/design-engineering/text-input-and-internationalization.md +1 -0
- package/expression/directions/nocturne-editorial.md +1 -0
- package/expression/directions/warm-print.md +1 -0
- package/expression/techniques/gradient-atmosphere.md +1 -0
- package/expression/techniques/grain-and-texture.md +1 -0
- package/expression/techniques/webgl-hero.md +1 -0
- package/guides/decision-tree.md +2 -0
- package/guides/webpage-generation-workflow.md +2 -0
- package/log.md +29 -0
- package/motion/accessible-motion.md +2 -0
- package/motion/interruption-and-retargeting.md +2 -0
- package/motion/rendering-and-performance.md +1 -0
- package/motion/techniques/kinetic-type.md +1 -0
- package/motion/techniques/scroll-choreography.md +2 -0
- package/package.json +14 -8
- package/patterns/centering/center.md +1 -0
- package/patterns/centering/super-center.md +2 -0
- package/patterns/containment/box.md +1 -0
- package/patterns/containment/clamped-card.md +1 -0
- package/patterns/containment/content-limiter.md +2 -0
- package/patterns/containment/fluid-styles.md +1 -0
- package/patterns/grid-repetition/card-grid.md +2 -0
- package/patterns/grid-repetition/columns.md +1 -0
- package/patterns/grid-repetition/dense-grid.md +1 -0
- package/patterns/grid-repetition/grid-wrapper.md +1 -0
- package/patterns/grid-repetition/masonry-approx.md +1 -0
- package/patterns/grid-repetition/page-grid.md +1 -0
- package/patterns/grid-repetition/ram-grid.md +2 -0
- package/patterns/grid-repetition/twelve-span-grid.md +1 -0
- package/patterns/in-line-grouping/badge-list.md +1 -0
- package/patterns/in-line-grouping/breadcrumb.md +1 -0
- package/patterns/in-line-grouping/cluster.md +1 -0
- package/patterns/in-line-grouping/deconstructed-pancake.md +1 -0
- package/patterns/in-line-grouping/pagination.md +1 -0
- package/patterns/in-line-grouping/reel.md +2 -0
- package/patterns/in-line-grouping/split-nav.md +1 -0
- package/patterns/in-line-grouping/tab-strip.md +1 -0
- package/patterns/in-line-grouping/wrap-row.md +1 -0
- package/patterns/media-fit/frame.md +2 -0
- package/patterns/media-fit/icon-frame.md +1 -0
- package/patterns/overlay-exception/imposter.md +1 -0
- package/patterns/overlay-exception/overlay-stack.md +1 -0
- package/patterns/split-sidebar/list-detail.md +1 -0
- package/patterns/split-sidebar/main-with-rail.md +1 -0
- package/patterns/split-sidebar/media-object.md +1 -0
- package/patterns/split-sidebar/sidebar.md +2 -0
- package/patterns/split-sidebar/split-screen.md +1 -0
- package/patterns/split-sidebar/sticky-aside.md +2 -0
- package/patterns/split-sidebar/supporting-pane.md +1 -0
- package/patterns/split-sidebar/switcher.md +2 -0
- package/patterns/stacking/feed.md +1 -0
- package/patterns/stacking/line-up.md +2 -0
- package/patterns/stacking/stack.md +1 -0
- package/patterns/stacking/step-nav.md +1 -0
- package/patterns/viewport-shell/cover.md +1 -0
- package/patterns/viewport-shell/fixed-sidenav-shell.md +2 -0
- package/patterns/viewport-shell/holy-grail.md +2 -0
- package/patterns/viewport-shell/panel-layout.md +1 -0
- package/patterns/viewport-shell/scroll-body-shell.md +2 -0
- package/patterns/viewport-shell/sticky-footer.md +2 -0
- package/patterns/viewport-shell/sticky-header.md +2 -0
- package/platform-guides/preferences-and-accessibility.md +1 -0
- package/recipes/article-page.md +1 -0
- package/recipes/command-surface.md +1 -0
- package/recipes/dashboard.md +1 -0
- package/recipes/form-flow.md +1 -0
- package/recipes/homepage.md +2 -0
- package/recipes/list-detail.md +1 -0
- package/recipes/saas-settings.md +1 -0
- package/scripts/agent-native/v2/material-cli-adapter.mjs +7 -5
- package/scripts/agent-native/v2/material-queries.mjs +21 -0
- package/scripts/agent-native/v2/material-search-v3.mjs +409 -0
- package/scripts/compiler/README.md +16 -16
- package/scripts/compiler/mcp.mjs +38 -2
- package/scripts/legacy-sg-material-mcp.mjs +9 -0
- package/scripts/legacy-sg-material.mjs +4 -0
- package/scripts/pattern-data.mjs +409 -0
- package/scripts/sg-entry.mjs +2 -2
- package/scripts/sg-material-mcp.mjs +1 -1
- package/scripts/stylegallery-cli.mjs +130 -0
- package/skills/stylegallery-search/SKILL.md +34 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
// Search v3: readable IDs, BM25F field ranking, section-aware read.
|
|
2
|
+
// Offline and deterministic: no model, no network, stable tie-breaks.
|
|
3
|
+
import { deepFreeze } from "../canonical-json.mjs";
|
|
4
|
+
import { MaterialQueryError, materialCorpus, tokenizeMaterialText } from "./material-queries.mjs";
|
|
5
|
+
|
|
6
|
+
export const SEARCH_SCHEMA_VERSION = "3.0";
|
|
7
|
+
export const SEARCH_DEFAULT_LIMIT = 5;
|
|
8
|
+
export const SEARCH_MAX_LIMIT = 50;
|
|
9
|
+
export const SEARCH_SNIPPET_CHARS = 500;
|
|
10
|
+
export const SEARCH_BUDGET_BYTES = 16 * 1024;
|
|
11
|
+
export const READ_MAX_CHARS = 64 * 1024;
|
|
12
|
+
const QUERY_MAX_BYTES = 4096;
|
|
13
|
+
|
|
14
|
+
// Field weight (w) and length normalization (b) for BM25F. `ko` holds Hangul
|
|
15
|
+
// 2-grams taken only from Korean aliases, questions, and titles.
|
|
16
|
+
export const SEARCH_FIELDS = deepFreeze({
|
|
17
|
+
title: { w: 4, b: 0.3 },
|
|
18
|
+
aliases: { w: 4, b: 0.3 },
|
|
19
|
+
description: { w: 2.5, b: 0.5 },
|
|
20
|
+
questions: { w: 2.5, b: 0.5 },
|
|
21
|
+
path: { w: 2, b: 0.3 },
|
|
22
|
+
headings: { w: 1.5, b: 0.6 },
|
|
23
|
+
body: { w: 1, b: 0.75 },
|
|
24
|
+
ko: { w: 1.5, b: 0.3 },
|
|
25
|
+
});
|
|
26
|
+
export const SEARCH_K1 = 1.2;
|
|
27
|
+
|
|
28
|
+
const STOPWORDS = new Set(("a an and are as at be by can do does for from how i in into is it its me my of on or should "
|
|
29
|
+
+ "so that the this to use using what when where which with you your").split(" "));
|
|
30
|
+
const HANGUL = /^[\p{Script=Hangul}]+$/u;
|
|
31
|
+
const KO_PREFIX = "\u0001ko:";
|
|
32
|
+
|
|
33
|
+
function fail(code, message) { throw new MaterialQueryError(code, message); }
|
|
34
|
+
function compare(left, right) { return left < right ? -1 : left > right ? 1 : 0; }
|
|
35
|
+
|
|
36
|
+
function stem(token) {
|
|
37
|
+
if (!/^[a-z]+$/.test(token) || token.length <= 3) return token;
|
|
38
|
+
if (token.endsWith("ies") && token.length > 4) return `${token.slice(0, -3)}y`;
|
|
39
|
+
if (/(?:ss|us|is)$/.test(token)) return token;
|
|
40
|
+
if (/(?:sh|ch|x|z)es$/.test(token)) return token.slice(0, -2);
|
|
41
|
+
if (token.endsWith("s")) return token.slice(0, -1);
|
|
42
|
+
let base = token;
|
|
43
|
+
if (token.length > 5 && token.endsWith("ing")) base = token.slice(0, -3);
|
|
44
|
+
else if (token.length > 4 && token.endsWith("ed") && !token.endsWith("eed")) base = token.slice(0, -2);
|
|
45
|
+
else return token;
|
|
46
|
+
return /([b-df-hj-km-rtv-y])\1$/.test(base) ? base.slice(0, -1) : base;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Terms for ranking: tokenizer words, stopwords removed, light English plural stemming. */
|
|
50
|
+
export function searchTerms(value) {
|
|
51
|
+
return tokenizeMaterialText(value).filter((token) => !STOPWORDS.has(token)).map(stem);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function hangulBigrams(value) {
|
|
55
|
+
const grams = [];
|
|
56
|
+
for (const token of tokenizeMaterialText(value)) {
|
|
57
|
+
if (!HANGUL.test(token)) continue;
|
|
58
|
+
const chars = [...token];
|
|
59
|
+
if (chars.length === 1) grams.push(`${KO_PREFIX}${token}`);
|
|
60
|
+
for (let index = 0; index + 1 < chars.length; index += 1) grams.push(`${KO_PREFIX}${chars[index]}${chars[index + 1]}`);
|
|
61
|
+
}
|
|
62
|
+
return grams;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function parseFrontmatter(text) {
|
|
66
|
+
const match = /^---\n([\s\S]*?)\n---(?:\n|$)/.exec(text);
|
|
67
|
+
const fields = {};
|
|
68
|
+
if (!match) return { fields, bodyStart: 0, bodyLine: 1 };
|
|
69
|
+
for (const line of match[1].split("\n")) {
|
|
70
|
+
const index = line.indexOf(":");
|
|
71
|
+
if (index <= 0 || /^\s/.test(line)) continue;
|
|
72
|
+
fields[line.slice(0, index).trim()] = line.slice(index + 1).trim();
|
|
73
|
+
}
|
|
74
|
+
return { fields, bodyStart: match[0].length, bodyLine: match[0].split("\n").length - (match[0].endsWith("\n") ? 1 : 0) + 1 };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function unquote(value = "") { return value.replace(/^(["'])(.*)\1$/, "$2"); }
|
|
78
|
+
|
|
79
|
+
/** Parse a JSON flow list such as ["sticky header", "헤더 고정"]; anything else yields []. */
|
|
80
|
+
export function frontmatterList(value) {
|
|
81
|
+
if (typeof value !== "string" || !value.startsWith("[")) return [];
|
|
82
|
+
try {
|
|
83
|
+
const parsed = JSON.parse(value);
|
|
84
|
+
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
85
|
+
} catch { return []; }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function headingSlug(text) {
|
|
89
|
+
return text.normalize("NFKC").toLowerCase().replace(/[`*_~[\]()]/g, "").replace(/[^\p{L}\p{N}\s-]/gu, "").trim().replace(/\s+/g, "-");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Headings outside fenced code, with stable unique slugs and 1-based line ranges. */
|
|
93
|
+
export function parseSections(text) {
|
|
94
|
+
const lines = text.split("\n");
|
|
95
|
+
const { bodyLine } = parseFrontmatter(text);
|
|
96
|
+
const sections = [];
|
|
97
|
+
const seen = new Map();
|
|
98
|
+
let fence = null;
|
|
99
|
+
for (let index = bodyLine - 1; index < lines.length; index += 1) {
|
|
100
|
+
const line = lines[index];
|
|
101
|
+
const fenceMatch = /^\s*(```|~~~)/.exec(line);
|
|
102
|
+
if (fenceMatch) {
|
|
103
|
+
if (fence === null) fence = fenceMatch[1];
|
|
104
|
+
else if (fence === fenceMatch[1]) fence = null;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (fence !== null) continue;
|
|
108
|
+
const heading = /^(#{1,6})\s+(.+?)\s*#*\s*$/.exec(line);
|
|
109
|
+
if (!heading) continue;
|
|
110
|
+
const base = headingSlug(heading[2]) || "section";
|
|
111
|
+
const count = seen.get(base) ?? 0;
|
|
112
|
+
seen.set(base, count + 1);
|
|
113
|
+
sections.push({ level: heading[1].length, title: heading[2], slug: count === 0 ? base : `${base}-${count + 1}`, line_start: index + 1 });
|
|
114
|
+
}
|
|
115
|
+
for (let index = 0; index < sections.length; index += 1) {
|
|
116
|
+
const next = sections.slice(index + 1).find((candidate) => candidate.level <= sections[index].level);
|
|
117
|
+
sections[index].line_end = next ? next.line_start - 1 : lines.length;
|
|
118
|
+
}
|
|
119
|
+
return sections;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const DOMAIN_ROOTS = ["layout", "motion", "design-engineering", "platform-guides", "design-terminology", "expression"];
|
|
123
|
+
function slugPart(value) { return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""); }
|
|
124
|
+
|
|
125
|
+
function longSlug(repositoryPath, domain) {
|
|
126
|
+
let rest = repositoryPath.replace(/\.md$/, "");
|
|
127
|
+
if (rest.startsWith(`${domain}/`)) rest = rest.slice(domain.length + 1);
|
|
128
|
+
rest = rest.replace(/(^|\/)index$/, "") || "index";
|
|
129
|
+
return slugPart(rest.replaceAll("/", "-"));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function shortSlug(repositoryPath, domain) {
|
|
133
|
+
const parts = repositoryPath.replace(/\.md$/, "").split("/");
|
|
134
|
+
let base = parts.at(-1);
|
|
135
|
+
if (base === "index") base = parts.length > 1 ? parts.at(-2) : "index";
|
|
136
|
+
if (base === domain && DOMAIN_ROOTS.includes(domain) && parts.length === 2) base = "index";
|
|
137
|
+
return slugPart(base);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Readable IDs `sg:<domain>/<slug>`: file basename, or the path-derived slug when basenames collide. */
|
|
141
|
+
export function readableIds(records) {
|
|
142
|
+
const groups = new Map();
|
|
143
|
+
for (const record of records) {
|
|
144
|
+
const key = `${record.domain}/${shortSlug(record.repository_path, record.domain)}`;
|
|
145
|
+
groups.set(key, [...(groups.get(key) ?? []), record]);
|
|
146
|
+
}
|
|
147
|
+
const ids = new Map();
|
|
148
|
+
for (const [key, members] of groups) {
|
|
149
|
+
for (const record of members) ids.set(record.repository_path, members.length === 1 ? `sg:${key}` : `sg:${record.domain}/${longSlug(record.repository_path, record.domain)}`);
|
|
150
|
+
}
|
|
151
|
+
const unique = new Set(ids.values());
|
|
152
|
+
if (unique.size !== ids.size) fail("search_id_collision", "readable search IDs are not unique");
|
|
153
|
+
return ids;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function counts(terms) {
|
|
157
|
+
const map = new Map();
|
|
158
|
+
for (const term of terms) map.set(term, (map.get(term) ?? 0) + 1);
|
|
159
|
+
return map;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function buildDocument(entry, id) {
|
|
163
|
+
const { record, identity, text } = entry;
|
|
164
|
+
const { fields: meta, bodyStart } = parseFrontmatter(text);
|
|
165
|
+
const body = text.slice(bodyStart);
|
|
166
|
+
const sections = parseSections(text);
|
|
167
|
+
const title = unquote(meta.title) || sections[0]?.title || record.repository_path;
|
|
168
|
+
const description = unquote(meta.description);
|
|
169
|
+
const aliases = frontmatterList(meta.aliases);
|
|
170
|
+
const questions = frontmatterList(meta.questions);
|
|
171
|
+
const fieldText = {
|
|
172
|
+
title, description, aliases: aliases.join("\n"), questions: questions.join("\n"),
|
|
173
|
+
path: record.repository_path.replace(/[/.\-_]/g, " "), headings: sections.map((section) => section.title).join("\n"), body,
|
|
174
|
+
};
|
|
175
|
+
const fields = {};
|
|
176
|
+
for (const [name, value] of Object.entries(fieldText)) {
|
|
177
|
+
const terms = searchTerms(value);
|
|
178
|
+
fields[name] = { tf: counts(terms), length: terms.length };
|
|
179
|
+
}
|
|
180
|
+
const ko = hangulBigrams([...aliases, ...questions, title].join("\n"));
|
|
181
|
+
fields.ko = { tf: counts(ko), length: ko.length };
|
|
182
|
+
return { id, record, identity, text, title, description, aliases, questions, sections, fields };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Build the ranked index once per corpus read. */
|
|
186
|
+
export function buildSearchIndex(corpus) {
|
|
187
|
+
const ids = readableIds(corpus.documents.map(({ record }) => record));
|
|
188
|
+
const documents = corpus.documents.map((entry) => buildDocument(entry, ids.get(entry.record.repository_path)))
|
|
189
|
+
.sort((left, right) => compare(left.id, right.id));
|
|
190
|
+
const averages = {};
|
|
191
|
+
for (const name of Object.keys(SEARCH_FIELDS)) {
|
|
192
|
+
averages[name] = documents.reduce((sum, document) => sum + document.fields[name].length, 0) / Math.max(1, documents.length) || 1;
|
|
193
|
+
}
|
|
194
|
+
return { manifestVersionId: corpus.manifestVersionId, documents, averages, byId: new Map(documents.map((document) => [document.id, document])) };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function queryTerms(query, queryEn) {
|
|
198
|
+
const text = [query, queryEn].filter(Boolean).join("\n");
|
|
199
|
+
return [...new Set([...searchTerms(text), ...hangulBigrams(query)])].sort();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function scoreDocument(index, document, terms, idf) {
|
|
203
|
+
let score = 0;
|
|
204
|
+
const matched = new Set();
|
|
205
|
+
let koTotal = 0;
|
|
206
|
+
let koHits = 0;
|
|
207
|
+
for (const term of terms) {
|
|
208
|
+
if (term.startsWith(KO_PREFIX)) {
|
|
209
|
+
koTotal += 1;
|
|
210
|
+
if (document.fields.ko.tf.has(term)) koHits += 1;
|
|
211
|
+
}
|
|
212
|
+
let weighted = 0;
|
|
213
|
+
for (const [name, { w, b }] of Object.entries(SEARCH_FIELDS)) {
|
|
214
|
+
const isKo = term.startsWith(KO_PREFIX);
|
|
215
|
+
if (isKo !== (name === "ko")) continue;
|
|
216
|
+
const field = document.fields[name];
|
|
217
|
+
const tf = field.tf.get(term) ?? 0;
|
|
218
|
+
if (tf === 0) continue;
|
|
219
|
+
matched.add(name);
|
|
220
|
+
weighted += (w * tf) / (1 - b + (b * field.length) / index.averages[name]);
|
|
221
|
+
}
|
|
222
|
+
if (weighted > 0) score += idf.get(term) * (weighted / (SEARCH_K1 + weighted));
|
|
223
|
+
}
|
|
224
|
+
// A stray shared syllable pair is not a match: 2-gram-only hits need half the query's 2-grams.
|
|
225
|
+
if (matched.size === 1 && matched.has("ko") && koHits * 2 < koTotal) return { score: 0, matched };
|
|
226
|
+
return { score: Math.round(score * 1e6) / 1e6, matched };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function inverseDocumentFrequency(index, terms) {
|
|
230
|
+
const total = index.documents.length;
|
|
231
|
+
return new Map(terms.map((term) => {
|
|
232
|
+
const df = index.documents.reduce((count, document) => count + Number(Object.values(document.fields).some((field) => field.tf.has(term))), 0);
|
|
233
|
+
return [term, Math.log(1 + (total - df + 0.5) / (df + 0.5))];
|
|
234
|
+
}));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function bestSnippet(document, terms) {
|
|
238
|
+
const lines = document.text.split("\n");
|
|
239
|
+
const termSet = new Set(terms.filter((term) => !term.startsWith(KO_PREFIX)));
|
|
240
|
+
const lineHits = (line) => searchTerms(line).filter((term) => termSet.has(term)).length;
|
|
241
|
+
// A section's own text stops at the next heading of any level.
|
|
242
|
+
const ownEnd = (section) => {
|
|
243
|
+
const next = document.sections.find((candidate) => candidate.line_start > section.line_start);
|
|
244
|
+
return next ? next.line_start - 1 : section.line_end;
|
|
245
|
+
};
|
|
246
|
+
let best = null;
|
|
247
|
+
for (const section of document.sections) {
|
|
248
|
+
const own = lines.slice(section.line_start, ownEnd(section));
|
|
249
|
+
const hits = lineHits(section.title) * 2 + own.reduce((sum, line) => sum + lineHits(line), 0);
|
|
250
|
+
if (hits > 0 && (!best || hits > best.hits)) best = { section, hits };
|
|
251
|
+
}
|
|
252
|
+
const section = best?.section ?? document.sections.find((candidate) => candidate.level > 1) ?? document.sections[0];
|
|
253
|
+
if (!section) return null;
|
|
254
|
+
const start = section.line_start + 1;
|
|
255
|
+
const end = ownEnd(section);
|
|
256
|
+
let first = start;
|
|
257
|
+
for (let line = start; line <= end; line += 1) if (lineHits(lines[line - 1]) > 0) { first = line; break; }
|
|
258
|
+
while (first <= end && lines[first - 1].trim() === "") first += 1;
|
|
259
|
+
let text = "";
|
|
260
|
+
let last = first - 1;
|
|
261
|
+
for (let line = first; line <= end && text.length < SEARCH_SNIPPET_CHARS; line += 1) {
|
|
262
|
+
text += `${text ? "\n" : ""}${lines[line - 1]}`;
|
|
263
|
+
last = line;
|
|
264
|
+
}
|
|
265
|
+
text = text.trimEnd();
|
|
266
|
+
if (text.length > SEARCH_SNIPPET_CHARS) text = `${[...text].slice(0, SEARCH_SNIPPET_CHARS - 1).join("")}…`;
|
|
267
|
+
const headingPath = [];
|
|
268
|
+
for (const candidate of document.sections) {
|
|
269
|
+
if (candidate.line_start > section.line_start) break;
|
|
270
|
+
while (headingPath.length && headingPath.at(-1).level >= candidate.level) headingPath.pop();
|
|
271
|
+
headingPath.push(candidate);
|
|
272
|
+
}
|
|
273
|
+
return { section, snippet: { heading_path: headingPath.map((item) => item.title), line_start: first, line_end: Math.max(first, last), text } };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function validateSearchInput(input) {
|
|
277
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) fail("material_input_invalid", "input must be an object");
|
|
278
|
+
const allowed = new Set(["query", "query_en", "limit", "paths_only", "response_format"]);
|
|
279
|
+
if (Object.keys(input).some((key) => !allowed.has(key))) fail("material_input_unknown", "input contains an unsupported field");
|
|
280
|
+
for (const key of ["query", "query_en"]) {
|
|
281
|
+
const value = input[key];
|
|
282
|
+
if (value === undefined && key === "query_en") continue;
|
|
283
|
+
if (typeof value !== "string") fail("material_query_invalid", `${key} must be a string`);
|
|
284
|
+
if (Buffer.byteLength(value, "utf8") > QUERY_MAX_BYTES) fail("material_query_oversized", `${key} exceeds 4096 UTF-8 bytes`);
|
|
285
|
+
}
|
|
286
|
+
if (tokenizeMaterialText(input.query, { query: true }).length === 0) fail("material_query_empty", "query must contain a Unicode word token");
|
|
287
|
+
const limit = input.limit ?? SEARCH_DEFAULT_LIMIT;
|
|
288
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > SEARCH_MAX_LIMIT) fail("search_limit_invalid", `limit must be an integer from 1 to ${SEARCH_MAX_LIMIT}`);
|
|
289
|
+
if (input.paths_only !== undefined && typeof input.paths_only !== "boolean") fail("material_paths_only_invalid", "paths_only must be a boolean");
|
|
290
|
+
const format = input.response_format ?? "concise";
|
|
291
|
+
if (!["concise", "detailed"].includes(format)) fail("search_response_format_invalid", "response_format must be concise or detailed");
|
|
292
|
+
return { query: input.query, queryEn: input.query_en, limit, pathsOnly: input.paths_only === true, detailed: format === "detailed" };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Rank documents for a query. Pure over an index; exported for evaluation. */
|
|
296
|
+
export function rankSearchIndex(index, query, queryEn) {
|
|
297
|
+
const terms = queryTerms(query, queryEn);
|
|
298
|
+
const idf = inverseDocumentFrequency(index, terms);
|
|
299
|
+
const ranked = [];
|
|
300
|
+
for (const document of index.documents) {
|
|
301
|
+
const { score, matched } = scoreDocument(index, document, terms, idf);
|
|
302
|
+
if (score > 0) ranked.push({ document, score, matched });
|
|
303
|
+
}
|
|
304
|
+
ranked.sort((left, right) => right.score - left.score || compare(left.document.id, right.document.id));
|
|
305
|
+
return { terms, ranked };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function readCommand(document, section) {
|
|
309
|
+
return `stylegallery read ${document.id}${section && section.level > 1 ? ` --section ${section.slug}` : ""}`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function searchIndex(index, input) {
|
|
313
|
+
const normalized = validateSearchInput(input);
|
|
314
|
+
const { terms, ranked } = rankSearchIndex(index, normalized.query, normalized.queryEn);
|
|
315
|
+
const selected = ranked.slice(0, normalized.limit);
|
|
316
|
+
const query = { text: normalized.query, ...(normalized.queryEn ? { query_en: normalized.queryEn } : {}) };
|
|
317
|
+
const page = { returned: selected.length, total: ranked.length, next_cursor: null };
|
|
318
|
+
if (normalized.pathsOnly) {
|
|
319
|
+
return deepFreeze({ schema_version: SEARCH_SCHEMA_VERSION, query, snapshot: index.manifestVersionId, paths: selected.map(({ document }) => document.record.repository_path), page });
|
|
320
|
+
}
|
|
321
|
+
const results = selected.map(({ document, score, matched }) => {
|
|
322
|
+
const best = bestSnippet(document, terms);
|
|
323
|
+
return {
|
|
324
|
+
id: document.id,
|
|
325
|
+
path: document.record.repository_path,
|
|
326
|
+
title: document.title,
|
|
327
|
+
...(document.description ? { description: document.description } : {}),
|
|
328
|
+
domain: document.record.domain,
|
|
329
|
+
matched_fields: [...matched].sort(),
|
|
330
|
+
snippets: best ? [best.snippet] : [],
|
|
331
|
+
next: { read: readCommand(document, best?.section) },
|
|
332
|
+
...(normalized.detailed ? { score, source_ref: document.record.stable_ref, version_id: document.record.version_id, material_ref: document.identity.stable_ref } : {}),
|
|
333
|
+
};
|
|
334
|
+
});
|
|
335
|
+
const envelope = { schema_version: SEARCH_SCHEMA_VERSION, query, snapshot: index.manifestVersionId, results, page, coverage: { truncated: false } };
|
|
336
|
+
if (normalized.detailed) envelope.ranking = { method: "BM25F", k1: SEARCH_K1, fields: SEARCH_FIELDS, terms };
|
|
337
|
+
while (Buffer.byteLength(JSON.stringify(envelope), "utf8") > SEARCH_BUDGET_BYTES && envelope.results.length > 1) {
|
|
338
|
+
envelope.results.pop();
|
|
339
|
+
envelope.page.returned = envelope.results.length;
|
|
340
|
+
envelope.coverage.truncated = true;
|
|
341
|
+
}
|
|
342
|
+
if (results.length === 0) {
|
|
343
|
+
envelope.hint = /[\p{Script=Hangul}]/u.test(normalized.query) && !normalized.queryEn
|
|
344
|
+
? "No match. Retry with query_en: an English rewrite of the question (e.g. --query-en \"sticky header\")."
|
|
345
|
+
: "No match. Try fewer or broader words, or browse a domain with stylegallery discover.";
|
|
346
|
+
}
|
|
347
|
+
return deepFreeze(envelope);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function resolveDocument(index, reference) {
|
|
351
|
+
if (typeof reference !== "string" || reference.length === 0) fail("read_reference_required", "reference is required");
|
|
352
|
+
const direct = index.byId.get(reference);
|
|
353
|
+
if (direct) return direct;
|
|
354
|
+
const clean = reference.replace(/^\.\//, "");
|
|
355
|
+
const found = index.documents.find((document) => document.record.repository_path === clean
|
|
356
|
+
|| document.record.stable_ref === reference || document.identity.stable_ref === reference);
|
|
357
|
+
if (!found) fail("read_reference_not_found", "no admitted document has this id, path, or StableRef; run stylegallery search first");
|
|
358
|
+
return found;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function validateReadInput(input) {
|
|
362
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) fail("material_input_invalid", "input must be an object");
|
|
363
|
+
const allowed = new Set(["reference", "section", "outline"]);
|
|
364
|
+
if (Object.keys(input).some((key) => !allowed.has(key))) fail("material_input_unknown", "input contains an unsupported field");
|
|
365
|
+
if (input.section !== undefined && (typeof input.section !== "string" || input.section.length === 0)) fail("read_section_invalid", "section must be a heading slug");
|
|
366
|
+
if (input.outline !== undefined && typeof input.outline !== "boolean") fail("read_outline_invalid", "outline must be a boolean");
|
|
367
|
+
if (input.section !== undefined && input.outline === true) fail("read_mode_conflict", "use either section or outline");
|
|
368
|
+
return input;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function readIndex(index, input) {
|
|
372
|
+
const normalized = validateReadInput(input);
|
|
373
|
+
const document = resolveDocument(index, normalized.reference);
|
|
374
|
+
const head = { schema_version: SEARCH_SCHEMA_VERSION, id: document.id, path: document.record.repository_path, title: document.title, snapshot: index.manifestVersionId, version_id: document.record.version_id };
|
|
375
|
+
const outline = document.sections.map(({ level, title, slug, line_start, line_end }) => ({ level, title, slug, line_start, line_end }));
|
|
376
|
+
if (normalized.outline) return deepFreeze({ ...head, outline, next: { read: outline[1] ? `stylegallery read ${document.id} --section ${outline[1].slug}` : `stylegallery read ${document.id}` } });
|
|
377
|
+
const lines = document.text.split("\n");
|
|
378
|
+
if (normalized.section !== undefined) {
|
|
379
|
+
const section = document.sections.find((candidate) => candidate.slug === normalized.section);
|
|
380
|
+
if (!section) fail("read_section_not_found", `section not found; valid slugs: ${document.sections.map((item) => item.slug).join(", ")}`);
|
|
381
|
+
return deepFreeze({ ...head, section: { title: section.title, slug: section.slug, line_start: section.line_start, line_end: section.line_end }, content: lines.slice(section.line_start - 1, section.line_end).join("\n") });
|
|
382
|
+
}
|
|
383
|
+
const truncated = document.text.length > READ_MAX_CHARS;
|
|
384
|
+
return deepFreeze({
|
|
385
|
+
...head, content: truncated ? document.text.slice(0, READ_MAX_CHARS) : document.text, truncated,
|
|
386
|
+
...(truncated ? { hint: `content truncated at ${READ_MAX_CHARS} characters; read one part with --section (see --outline)` } : {}),
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Load the admitted corpus with source checks and build the index. */
|
|
391
|
+
export function loadSearchIndex(options) {
|
|
392
|
+
return buildSearchIndex(materialCorpus(options));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const PUBLIC_CODES = new Set(["material_input_invalid", "material_input_unknown", "material_query_invalid", "material_query_oversized",
|
|
396
|
+
"material_query_empty", "search_limit_invalid", "material_paths_only_invalid", "search_response_format_invalid", "read_reference_required",
|
|
397
|
+
"read_reference_not_found", "read_section_invalid", "read_outline_invalid", "read_mode_conflict", "read_section_not_found", "search_id_collision"]);
|
|
398
|
+
|
|
399
|
+
/** Run `search` or `read` and wrap it in the ok/failures envelope shared by CLI and MCP. */
|
|
400
|
+
export function invokeSearchOperation(operation, input, options) {
|
|
401
|
+
try {
|
|
402
|
+
const index = loadSearchIndex(options);
|
|
403
|
+
const result = operation === "search" ? searchIndex(index, input) : readIndex(index, input);
|
|
404
|
+
return deepFreeze({ ok: true, operation, result });
|
|
405
|
+
} catch (error) {
|
|
406
|
+
const known = typeof error?.code === "string" && (PUBLIC_CODES.has(error.code) || error.code.startsWith("material_"));
|
|
407
|
+
return deepFreeze({ ok: false, operation, failures: [{ code: known ? error.code : `${operation}_failed`, message: known ? error.message : `${operation} failed` }] });
|
|
408
|
+
}
|
|
409
|
+
}
|
|
@@ -8,11 +8,11 @@ description: Run a separately installed website compiler through the SG CLI and
|
|
|
8
8
|
|
|
9
9
|
Primary role: compiler setup and execution reference.
|
|
10
10
|
|
|
11
|
-
Use `
|
|
11
|
+
Use `stylegallery compile`, `stylegallery timeline`, and the other commands below directly. The `stylegallery-mcp` server exposes the same names alongside `discover`, `resolve`, `claims`, `context`, `ops`, and `retrieve`. Compiler operations can write files, open websites, and invoke a model; each tool declares those effects. The original [v1 knowledge interface](../../consumer-reference/agent-native/README.md) remains available separately.
|
|
12
12
|
|
|
13
13
|
## Setup
|
|
14
14
|
|
|
15
|
-
Install StyleGallery with Node.js 22 or newer using `npm install --global stylegallery`. For an unreleased StyleGallery checkout, run commands from that checkout as `bun run
|
|
15
|
+
Install StyleGallery with Node.js 22 or newer using `npm install --global stylegallery`. For an unreleased StyleGallery checkout, run commands from that checkout as `bun run stylegallery -- <command>` instead. The compiler is a [separate private repository](https://github.com/changeroa/site-compiler); an account with access must install it once:
|
|
16
16
|
|
|
17
17
|
```sh
|
|
18
18
|
gh repo clone changeroa/site-compiler /absolute/path/to/site-compiler
|
|
@@ -20,32 +20,32 @@ cd /absolute/path/to/site-compiler
|
|
|
20
20
|
bun install --frozen-lockfile --ignore-scripts
|
|
21
21
|
bunx playwright install chromium
|
|
22
22
|
export SG_COMPILER_ROOT=/absolute/path/to/site-compiler
|
|
23
|
-
|
|
23
|
+
stylegallery discover --format json
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
Set `SG_COMPILER_ROOT` in your shell profile for CLI use and in your MCP server environment for MCP use. A CLI invocation can override it with `--compiler-root /absolute/path/to/site-compiler`. `discover` preserves the knowledge response and adds a top-level `compiler` object containing installation status, the actual Git revision, tracked changes, and available operations. Missing compiler setup leaves knowledge queries usable.
|
|
27
27
|
|
|
28
|
-
The verified upstream revision is declared in [scripts/compiler/operations.mjs](operations.mjs). `matches_verified_revision` is true only for that revision without tracked modifications; a mismatch is reported but does not block execution. Use that revision when reproducing the verified workflow. To use a newer upstream version, update your compiler checkout with `git pull --ff-only` and `npm ci`, then inspect `
|
|
28
|
+
The verified upstream revision is declared in [scripts/compiler/operations.mjs](operations.mjs). `matches_verified_revision` is true only for that revision without tracked modifications; a mismatch is reported but does not block execution. Use that revision when reproducing the verified workflow. To use a newer upstream version, update your compiler checkout with `git pull --ff-only` and `npm ci`, then inspect `stylegallery discover` and `stylegallery workflow`; SG does not fetch or upgrade code during a tool call. The compiler source, browser binaries, captured frames, and private pack data are not included in the StyleGallery npm package.
|
|
29
29
|
|
|
30
30
|
## CLI
|
|
31
31
|
|
|
32
32
|
Run these from the consumer project's working directory:
|
|
33
33
|
|
|
34
34
|
```sh
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
stylegallery workflow
|
|
36
|
+
stylegallery workflow --document readme
|
|
37
|
+
stylegallery compile --url https://example.com --out ./captures/example
|
|
38
|
+
stylegallery timeline --url https://example.com --out ./captures/example --step 500 --grow
|
|
39
|
+
stylegallery transcribe --in ./captures --only example
|
|
40
|
+
stylegallery sale --in ./captures/example --out ./captures/example/sale
|
|
41
|
+
stylegallery gate --in ./captures/example --sale ./captures/example/sale --json ./captures/example/qa-report.json
|
|
42
|
+
stylegallery build --packs ./captures --out ./builds --only example
|
|
43
|
+
stylegallery shots --builds ./builds --slug example
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
`compile` captures scenes and frames; it does not produce a SceneBook by itself. A SceneBook describes scene geometry and content; its Timeline companion describes observed scroll behavior. `transcribe` produces these documents. Transcription and the optional sale-preview builder require an installed, authenticated `claude` CLI and can incur model usage. Their input roots contain one subdirectory per pack. Supply `--stylegallery` only when a different SG corpus should be used; the installed StyleGallery package is the default.
|
|
47
47
|
|
|
48
|
-
Read `
|
|
48
|
+
Read `stylegallery workflow` before selecting stages. Its installed compiler document owns the task policy: faithful reconstruction, adaptation for a new design, or sale-package production. These are task purposes, not CLI flags. Reconstruction uses available source media and direct observation to fill document gaps; the current implementation agent remains the builder. A media record carries source URLs, responsive variants, reuse or replacement decisions, and playback checks separately from the observation document.
|
|
49
49
|
|
|
50
50
|
`sale` replaces observed copy with structural descriptors. `build` consumes only those sale editions and intentionally restricts original-page and external-media access. It is a package preview, not the faithful-reconstruction route. Use sale, build, and release operations when that package purpose is requested. A document-only evaluation is an explicit experiment, not the default website workflow. The SG skill only retrieves this workflow; it does not duplicate these policies.
|
|
51
51
|
|
|
@@ -70,7 +70,7 @@ By default, `sale` writes its private audit to `copy-audit.private.json` in the
|
|
|
70
70
|
|
|
71
71
|
All CLI results are one JSON envelope on stdout, including failures. Child logs are returned in `result.stdout` and `result.stderr`, with a 64 KiB tail per stream and truncation flags. Relative path options resolve from the caller's working directory; upstream scripts execute from the compiler checkout. `pack-release` is the upstream exception with fixed checkout-relative directories, and produces local archives without publishing them.
|
|
72
72
|
|
|
73
|
-
Use `
|
|
73
|
+
Use `stylegallery discover` or `stylegallery ops` for the full input schemas. CLI options use hyphens (`--max-height`); MCP inputs use underscores (`max_height`). Unknown options, repeated CLI options, missing required paths, and invalid numeric values fail before execution. `--format json` is optional. `--runner-timeout-ms` bounds the entire process tree, defaults to 30 minutes, and accepts up to six hours; `--timeout-ms` on model operations instead controls upstream per-job timeouts. Interrupting the CLI or cancelling an MCP request terminates its running process tree.
|
|
74
74
|
|
|
75
75
|
`ok` reports process success. Batch scripts can exit zero with failed individual jobs: use the returned ledger path to inspect each pack. A gate pass is an upstream consistency check, not SG conformance, accessibility proof, or rights clearance. Each result records the compiler revision, tracked-change status, executed script hash, and normalized arguments. Captures and generated documents remain consumer-owned outputs and are not admitted to the governed material registry.
|
|
76
76
|
|
|
@@ -90,7 +90,7 @@ Use `sg discover` or `sg ops` for the full input schemas. CLI options use hyphen
|
|
|
90
90
|
|
|
91
91
|
Call `compile` with `{"url":"https://example.com","out":"/absolute/path/to/captures/example"}` or `gate` with `{"in":"/absolute/path/to/captures/example"}`. Prefer absolute paths because the MCP client's launch directory may differ from your terminal. Long captures require a client request timeout longer than the capture; the runner's timeout does not change client settings. `workflow` and the knowledge tools are read-only; execution tools are marked as potentially destructive and non-idempotent because existing artifacts may be replaced.
|
|
92
92
|
|
|
93
|
-
From the StyleGallery checkout, use `bun run
|
|
93
|
+
From the StyleGallery checkout, use `bun run stylegallery -- compile ...` and `bun run sg:mcp` for local development. To connect an MCP client to that unreleased checkout, use `"command": "node"` and `"args": ["/absolute/path/to/StyleGallery/scripts/sg-server.mjs"]` with the same environment settings. The frozen six-tool v1 server is `bun run sg:mcp:v1`, and its CLI is `bun run sg:v1 -- ...`.
|
|
94
94
|
|
|
95
95
|
## Contracts And Verification
|
|
96
96
|
|
package/scripts/compiler/mcp.mjs
CHANGED
|
@@ -6,6 +6,40 @@ import { agentNativeRegistry } from "../agent-native/registry.mjs";
|
|
|
6
6
|
import { compilerOperations, compilerToolDefinitions } from "./operations.mjs";
|
|
7
7
|
import { withCompilerDiscovery } from "./cli.mjs";
|
|
8
8
|
import { executeCompiler, failure } from "./runtime.mjs";
|
|
9
|
+
import { materialRuntimeOptions } from "../agent-native/v2/material-cli-adapter.mjs";
|
|
10
|
+
import { invokeSearchOperation } from "../agent-native/v2/material-search-v3.mjs";
|
|
11
|
+
|
|
12
|
+
const readOnly = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
13
|
+
export const searchToolDefinitions = Object.freeze([
|
|
14
|
+
{
|
|
15
|
+
name: "search",
|
|
16
|
+
description: "Search the StyleGallery Markdown corpus (layout patterns, recipes, motion, design engineering, platform guides, terminology, expression). Offline BM25F ranking. Returns light results: id, path, title, description, a verbatim snippet, and the next read command. When the question is not in English, also pass query_en with an English rewrite; Korean-only queries match only documents with Korean aliases.",
|
|
17
|
+
inputSchema: {
|
|
18
|
+
type: "object", additionalProperties: false, required: ["query"],
|
|
19
|
+
properties: {
|
|
20
|
+
query: { type: "string", description: "The question or keywords, in any language." },
|
|
21
|
+
query_en: { type: "string", description: "English rewrite of query. Strongly recommended when query is not English." },
|
|
22
|
+
limit: { type: "integer", minimum: 1, maximum: 50, default: 5 },
|
|
23
|
+
paths_only: { type: "boolean", description: "Return repository-relative paths only." },
|
|
24
|
+
response_format: { type: "string", enum: ["concise", "detailed"], default: "concise", description: "detailed adds scores, hash identities, and ranking terms." },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
annotations: readOnly,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: "read",
|
|
31
|
+
description: "Read one StyleGallery document by id (for example sg:layout/sticky-header), repository path, or StableRef. Use outline to list section slugs, then section to read only that part.",
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: "object", additionalProperties: false, required: ["reference"],
|
|
34
|
+
properties: {
|
|
35
|
+
reference: { type: "string" },
|
|
36
|
+
outline: { type: "boolean" },
|
|
37
|
+
section: { type: "string", description: "Heading slug from outline." },
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
annotations: readOnly,
|
|
41
|
+
},
|
|
42
|
+
]);
|
|
9
43
|
|
|
10
44
|
// Compose at the transport boundary. The byte-frozen v1 server, registry,
|
|
11
45
|
// conformance receipts, and resources remain independently usable.
|
|
@@ -28,7 +62,7 @@ export function createSgMcpServer(config = {}) {
|
|
|
28
62
|
};
|
|
29
63
|
});
|
|
30
64
|
server.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
31
|
-
tools: [...legacyTools, ...compilerToolDefinitions()].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0),
|
|
65
|
+
tools: [...legacyTools, ...compilerToolDefinitions(), ...searchToolDefinitions].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0),
|
|
32
66
|
}));
|
|
33
67
|
const callSchema = CallToolRequestSchema.extend({
|
|
34
68
|
params: CallToolRequestSchema.shape.params.extend({ arguments: z.unknown().optional() }),
|
|
@@ -37,7 +71,9 @@ export function createSgMcpServer(config = {}) {
|
|
|
37
71
|
const operation = request.params.name;
|
|
38
72
|
let input = request.params.arguments ?? {};
|
|
39
73
|
let report;
|
|
40
|
-
if (
|
|
74
|
+
if (operation === "search" || operation === "read") {
|
|
75
|
+
report = invokeSearchOperation(operation, input, materialRuntimeOptions());
|
|
76
|
+
} else if (Object.hasOwn(compilerOperations, operation) || operation === "workflow") {
|
|
41
77
|
const signals = [lifetime.signal, extra.signal, config.signal].filter(Boolean);
|
|
42
78
|
report = await executeCompiler(operation, input, { ...config, signal: AbortSignal.any(signals) });
|
|
43
79
|
} else if (legacyByName.has(operation)) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Old bin name `stylegallery-material-mcp`: notice on stderr, then serve unchanged for 0.1.12 only.
|
|
3
|
+
import { main } from "./sg-material-mcp.mjs";
|
|
4
|
+
|
|
5
|
+
process.stderr.write("stylegallery-material-mcp is renamed: use stylegallery-mcp (tools: search, read). This old name will be removed in the next release.\n");
|
|
6
|
+
await main().catch((error) => {
|
|
7
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
8
|
+
process.exitCode = 1;
|
|
9
|
+
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Old bin names `sg-material` and `stylegallery-material`: notice, then delegate for 0.1.12 only.
|
|
3
|
+
process.stderr.write("sg-material is renamed to `stylegallery` (stylegallery search, stylegallery read, stylegallery context --query). This old name will be removed in the next release.\n");
|
|
4
|
+
await import("./sg-material.mjs");
|