@nanisoft/prism-mcp-server 0.2.0 → 0.4.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/README.md +66 -0
- package/THIRD-PARTY-NOTICES.md +48 -0
- package/dist/descriptions.d.ts +22 -0
- package/dist/descriptions.js +33 -0
- package/dist/generated/built.d.ts +2 -0
- package/dist/generated/built.js +6 -0
- package/dist/generated/server-version.d.ts +1 -0
- package/dist/generated/server-version.js +5 -0
- package/dist/index.d.ts +11 -10
- package/dist/index.js +10 -9
- package/dist/render.d.ts +69 -44
- package/dist/render.js +422 -145
- package/dist/rules.d.ts +12 -0
- package/dist/rules.js +14 -0
- package/dist/schemas.d.ts +104 -0
- package/dist/schemas.js +102 -0
- package/dist/server.d.ts +22 -0
- package/dist/server.js +75 -0
- package/dist/version.d.ts +1 -10
- package/dist/version.js +10 -7
- package/dist/vocab.d.ts +13 -0
- package/dist/vocab.js +19 -0
- package/package.json +32 -23
- package/dist/factory.d.ts +0 -31
- package/dist/factory.d.ts.map +0 -1
- package/dist/factory.js +0 -33
- package/dist/factory.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lookup.d.ts +0 -68
- package/dist/lookup.d.ts.map +0 -1
- package/dist/lookup.js +0 -113
- package/dist/lookup.js.map +0 -1
- package/dist/render.d.ts.map +0 -1
- package/dist/render.js.map +0 -1
- package/dist/search.d.ts +0 -34
- package/dist/search.d.ts.map +0 -1
- package/dist/search.js +0 -68
- package/dist/search.js.map +0 -1
- package/dist/store.d.ts +0 -62
- package/dist/store.d.ts.map +0 -1
- package/dist/store.js +0 -80
- package/dist/store.js.map +0 -1
- package/dist/tools.d.ts +0 -22
- package/dist/tools.d.ts.map +0 -1
- package/dist/tools.js +0 -120
- package/dist/tools.js.map +0 -1
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
package/dist/render.js
CHANGED
|
@@ -1,156 +1,433 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
1
|
+
import { STORE_SECTION_TITLES, STORE_SECTIONS, } from '@nanisoft/prism-llms';
|
|
2
|
+
import { IMPORT_RULE, NO_INVENTION_RULE, SURFACE_RULE } from './rules.js';
|
|
3
|
+
import { TOKEN_GROUPS } from './vocab.js';
|
|
4
|
+
/* -------------------------------------------------------------------------- */
|
|
5
|
+
/* Result helpers */
|
|
6
|
+
/* -------------------------------------------------------------------------- */
|
|
7
|
+
const text = (value) => ({ content: [{ type: 'text', text: value }] });
|
|
8
|
+
const error = (value) => ({
|
|
9
|
+
content: [{ type: 'text', text: value }],
|
|
10
|
+
isError: true,
|
|
11
|
+
});
|
|
12
|
+
function fence(code, language = 'tsx') {
|
|
13
|
+
return `\`\`\`${language}\n${code.replace(/\r\n/g, '\n').replace(/\n$/, '')}\n\`\`\``;
|
|
14
|
+
}
|
|
15
|
+
function kindLabel(kind) {
|
|
16
|
+
if (kind === 'component')
|
|
17
|
+
return 'Component';
|
|
18
|
+
if (kind === 'block')
|
|
19
|
+
return 'Block';
|
|
20
|
+
return 'Page';
|
|
21
|
+
}
|
|
22
|
+
function countKinds(items) {
|
|
23
|
+
const counts = { component: 0, block: 0, page: 0 };
|
|
24
|
+
for (const item of items)
|
|
25
|
+
counts[item.kind] += 1;
|
|
26
|
+
return counts;
|
|
27
|
+
}
|
|
28
|
+
/* -------------------------------------------------------------------------- */
|
|
29
|
+
/* Lookup */
|
|
30
|
+
/* -------------------------------------------------------------------------- */
|
|
31
|
+
/** Every exact name or slug match in the optional kind, case-insensitively. */
|
|
32
|
+
export function matchesFor(store, name, kind) {
|
|
33
|
+
const needle = name.trim().toLowerCase();
|
|
34
|
+
const pool = store.items.filter((item) => !kind || item.kind === kind);
|
|
35
|
+
const byName = pool.filter((item) => item.name.toLowerCase() === needle);
|
|
36
|
+
if (byName.length > 0)
|
|
37
|
+
return byName;
|
|
38
|
+
return pool.filter((item) => item.slug.toLowerCase() === needle);
|
|
39
|
+
}
|
|
40
|
+
function suggestItems(store, name, kind) {
|
|
41
|
+
const needle = name.trim().toLowerCase();
|
|
42
|
+
const pool = store.items.filter((item) => !kind || item.kind === kind);
|
|
43
|
+
return pool
|
|
44
|
+
.map((item) => {
|
|
45
|
+
const candidate = `${item.name} ${item.slug}`.toLowerCase();
|
|
46
|
+
let score = candidate.includes(needle) ? 3 : 0;
|
|
47
|
+
let common = 0;
|
|
48
|
+
while (common < item.name.length && common < needle.length && item.name[common]?.toLowerCase() === needle[common]) {
|
|
49
|
+
common += 1;
|
|
50
|
+
}
|
|
51
|
+
score += common;
|
|
52
|
+
return { item, score };
|
|
53
|
+
})
|
|
54
|
+
.filter((entry) => entry.score > 0)
|
|
55
|
+
.sort((a, b) => b.score - a.score || a.item.name.localeCompare(b.item.name))
|
|
56
|
+
.slice(0, 3)
|
|
57
|
+
.map((entry) => entry.item);
|
|
58
|
+
}
|
|
59
|
+
function itemMiss(store, name, kind) {
|
|
60
|
+
const suggestions = suggestItems(store, name, kind);
|
|
61
|
+
const did = suggestions.length > 0
|
|
62
|
+
? ` Did you mean ${suggestions.map((item) => `\`${item.name}\` (${item.kind})`).join(', ')}?`
|
|
63
|
+
: '';
|
|
64
|
+
return error(`No Prism item named \`${name}\`${kind ? ` of kind \`${kind}\`` : ''}.${did} Browse the catalogue with \`list_items\` or search with \`search_docs\`.`);
|
|
65
|
+
}
|
|
66
|
+
function resolveItem(store, name, kind) {
|
|
67
|
+
const matches = matchesFor(store, name, kind);
|
|
68
|
+
if (matches.length === 0)
|
|
69
|
+
return { ok: false, result: itemMiss(store, name, kind) };
|
|
70
|
+
if (matches.length > 1) {
|
|
71
|
+
const kinds = matches.map((item) => `\`${item.kind}\``).join(', ');
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
result: error(`\`${name}\` matches more than one kind: ${kinds}. Pass \`kind\` to disambiguate.`),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const item = matches[0];
|
|
78
|
+
if (!item)
|
|
79
|
+
return { ok: false, result: itemMiss(store, name, kind) };
|
|
80
|
+
return { ok: true, item };
|
|
81
|
+
}
|
|
82
|
+
function renderListItem(item) {
|
|
83
|
+
const category = item.kind === 'component' && item.category ? ` _(${item.category})_` : '';
|
|
84
|
+
return `- **${item.name}** - ${item.description}${category}`;
|
|
85
|
+
}
|
|
86
|
+
export function renderListItems(store, args, built) {
|
|
87
|
+
const { kind, category } = args;
|
|
88
|
+
if (category && kind && kind !== 'component') {
|
|
89
|
+
return error(`\`category\` is a Component-only field. Kind \`${kind}\` has no categories; omit \`category\` or pass \`kind: "component"\`.`);
|
|
90
|
+
}
|
|
91
|
+
const counts = countKinds(store.items);
|
|
92
|
+
const stamp = built ? ` (built ${built})` : '';
|
|
93
|
+
const header = `# Prism ${store.version} - ${counts.component} components, ${counts.block} blocks, ${counts.page} pages${stamp}`;
|
|
94
|
+
const filters = [];
|
|
23
95
|
if (kind)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
96
|
+
filters.push(`kind = \`${kind}\``);
|
|
97
|
+
if (category)
|
|
98
|
+
filters.push(`category = \`${category}\``);
|
|
99
|
+
const note = filters.length > 0 ? `\n\nFiltered to ${filters.join(', ')}.` : '';
|
|
100
|
+
let items = store.items;
|
|
101
|
+
if (kind)
|
|
102
|
+
items = items.filter((item) => item.kind === kind);
|
|
103
|
+
if (category) {
|
|
104
|
+
items = items.filter((item) => item.kind === 'component' && item.category === category);
|
|
105
|
+
}
|
|
106
|
+
const groups = [
|
|
107
|
+
{ kind: 'component', heading: 'Components' },
|
|
108
|
+
{ kind: 'block', heading: 'Blocks' },
|
|
109
|
+
{ kind: 'page', heading: 'Pages' },
|
|
110
|
+
];
|
|
111
|
+
const sections = [];
|
|
112
|
+
for (const group of groups) {
|
|
113
|
+
const groupItems = items.filter((item) => item.kind === group.kind);
|
|
114
|
+
if (groupItems.length === 0)
|
|
32
115
|
continue;
|
|
116
|
+
sections.push(`## ${group.heading}\n\n${groupItems.map(renderListItem).join('\n')}`);
|
|
117
|
+
}
|
|
118
|
+
if (sections.length === 0) {
|
|
119
|
+
const label = kind ? `${groupPlural(kind)}` : 'items';
|
|
120
|
+
sections.push(`No ${label} in this build.`);
|
|
121
|
+
}
|
|
122
|
+
const footer = `Use \`get_item_doc\`, \`get_item_props\` or \`search_docs\` to read an item.\n\nImport rule: ${IMPORT_RULE}\n\nSurface rule: ${SURFACE_RULE}`;
|
|
123
|
+
return text([`${header}${note}`, ...sections, footer].join('\n\n'));
|
|
124
|
+
}
|
|
125
|
+
function groupPlural(kind) {
|
|
126
|
+
if (kind === 'component')
|
|
127
|
+
return 'components';
|
|
128
|
+
if (kind === 'block')
|
|
129
|
+
return 'blocks';
|
|
130
|
+
return 'pages';
|
|
131
|
+
}
|
|
132
|
+
export function renderItemDoc(store, args) {
|
|
133
|
+
const resolved = resolveItem(store, args.name, args.kind);
|
|
134
|
+
if (!resolved.ok)
|
|
135
|
+
return resolved.result;
|
|
136
|
+
const item = resolved.item;
|
|
137
|
+
return text(`${item.doc.trimEnd()}\n\n---\n\nPublic import: \`${item.importLine}\`\n\nImport rule: ${IMPORT_RULE}`);
|
|
138
|
+
}
|
|
139
|
+
/* -------------------------------------------------------------------------- */
|
|
140
|
+
/* get_item_props */
|
|
141
|
+
/* -------------------------------------------------------------------------- */
|
|
142
|
+
export function renderItemProps(store, args) {
|
|
143
|
+
const resolved = resolveItem(store, args.name, args.kind);
|
|
144
|
+
if (!resolved.ok)
|
|
145
|
+
return resolved.result;
|
|
146
|
+
const item = resolved.item;
|
|
147
|
+
// Both sections, when both are recorded. A Block and a Page publish an
|
|
148
|
+
// interface section AND a composition section, and reading one while calling
|
|
149
|
+
// the other "no own props" is the type-level lie the published surface exists
|
|
150
|
+
// to prevent: the store carried both and the tool reported one and denied the
|
|
151
|
+
// other.
|
|
152
|
+
if (item.props && item.composition) {
|
|
153
|
+
return text(`# ${item.name} - props and composition\n\n## Props\n\n${item.props}\n\n## Composition\n\n${item.composition}\n\n` +
|
|
154
|
+
`This is the complete public Prism API for \`${item.name}\`; internal primitive props are not a consumer contract.\n\n` +
|
|
155
|
+
`Import rule: ${IMPORT_RULE}`);
|
|
156
|
+
}
|
|
157
|
+
if (item.props) {
|
|
158
|
+
return text(`# ${item.name} - props\n\n${item.props}\n\nThis is the complete public Prism API for \`${item.name}\`; internal primitive props are not a consumer contract.\n\nImport rule: ${IMPORT_RULE}`);
|
|
159
|
+
}
|
|
160
|
+
if (item.composition) {
|
|
161
|
+
return text(`# ${item.name} - composition\n\nThe public surface of this ${kindLabel(item.kind)} is the composition below.\n\n${item.composition}\n\nImport rule: ${IMPORT_RULE}`);
|
|
162
|
+
}
|
|
163
|
+
return error(`No props or composition are recorded for \`${item.name}\`. Use \`get_item_doc\` for its documentation.`);
|
|
164
|
+
}
|
|
165
|
+
/* -------------------------------------------------------------------------- */
|
|
166
|
+
/* get_item_source */
|
|
167
|
+
/* -------------------------------------------------------------------------- */
|
|
168
|
+
export function renderItemSource(store, args) {
|
|
169
|
+
const resolved = resolveItem(store, args.name, args.kind);
|
|
170
|
+
if (!resolved.ok)
|
|
171
|
+
return resolved.result;
|
|
172
|
+
const item = resolved.item;
|
|
173
|
+
if (!item.example) {
|
|
174
|
+
return error(`No demo source is recorded for \`${item.name}\`. Use \`get_item_doc\` for its documentation.`);
|
|
175
|
+
}
|
|
176
|
+
return text([
|
|
177
|
+
`# ${item.name} - example source`,
|
|
178
|
+
`Public import:\n\n${fence(item.importLine)}`,
|
|
179
|
+
`${item.example.title}\n\n${fence(item.example.code)}`,
|
|
180
|
+
'This demo is self-contained: it imports only `@nanisoft/prism-ui/*` or `react`, and exposes one default export.',
|
|
181
|
+
`Import rule: ${IMPORT_RULE}`,
|
|
182
|
+
].join('\n\n'));
|
|
183
|
+
}
|
|
184
|
+
export function renderThemeDoc(store, args) {
|
|
185
|
+
const pack = args.pack ?? 'default';
|
|
186
|
+
const mode = args.mode ?? 'light';
|
|
187
|
+
const group = args.group;
|
|
188
|
+
if (!store.tokens.packs.includes(pack)) {
|
|
189
|
+
return error(`Unknown pack \`${pack}\`. Valid packs: ${store.tokens.packs.join(', ')}.`);
|
|
190
|
+
}
|
|
191
|
+
if (!store.tokens.modes.includes(mode)) {
|
|
192
|
+
return error(`Unknown mode \`${mode}\`. Valid modes: ${store.tokens.modes.join(', ')}.`);
|
|
193
|
+
}
|
|
194
|
+
if (group !== undefined && !TOKEN_GROUPS.includes(group)) {
|
|
195
|
+
return error(`Unknown group \`${group}\`. Valid groups: ${TOKEN_GROUPS.join(', ')}.`);
|
|
196
|
+
}
|
|
197
|
+
const preamble = [
|
|
198
|
+
`Packs: ${store.tokens.packs.join(', ')}.`,
|
|
199
|
+
`Modes: ${store.tokens.modes.join(', ')}.`,
|
|
200
|
+
`Queryable groups: ${TOKEN_GROUPS.join(', ')}.`,
|
|
201
|
+
// The attribute is not root-only, and the correction is in this string
|
|
202
|
+
// because this string is the agent-facing surface: a tool that prints the
|
|
203
|
+
// pack's radius below and tells the reader the attribute belongs on `<html>`
|
|
204
|
+
// is worse than a tool that says nothing, because the reader then places a
|
|
205
|
+
// boundary where a boundary is not needed and believes a subtree cannot carry
|
|
206
|
+
// one. A boundary moves BOTH axes beneath it, and radius is the axis people
|
|
207
|
+
// forget.
|
|
208
|
+
'Selection: `data-pack="<id>"` on any element makes that element and its subtree a theme boundary. ' +
|
|
209
|
+
'It wears the mode of the nearest ancestor carrying `.dark`, and carries that class itself only to hold a fixed mode. ' +
|
|
210
|
+
'A boundary also moves the pack corner radius, which is why a boundary belongs on a fully-rounded element, on one carrying no radius utility, or on a shape with no radius concept.',
|
|
211
|
+
].join('\n\n');
|
|
212
|
+
const footer = `Import rule: ${IMPORT_RULE}\n\n${NO_INVENTION_RULE}`;
|
|
213
|
+
if (group === undefined || group === 'semantic') {
|
|
214
|
+
const theme = store.tokens.themes.find((entry) => entry.pack === pack && entry.mode === mode);
|
|
215
|
+
if (!theme) {
|
|
216
|
+
return error(`No resolved values are recorded for pack \`${pack}\` in mode \`${mode}\`.`);
|
|
217
|
+
}
|
|
218
|
+
const lines = theme.semantic.map((token) => `--${token.token}: ${token.value}`).join('\n');
|
|
219
|
+
return text([`# Prism theme: ${pack} / ${mode}`, preamble, lines, footer].join('\n\n'));
|
|
220
|
+
}
|
|
221
|
+
const scale = store.tokens.scales[group];
|
|
222
|
+
const lines = scale
|
|
223
|
+
.map((token) => token.binding
|
|
224
|
+
? `--${token.token}: ${token.value} (binding: --${token.binding})`
|
|
225
|
+
: `--${token.token}: ${token.value}`)
|
|
226
|
+
.join('\n');
|
|
227
|
+
const close = group === 'breakpoint'
|
|
228
|
+
? '\n\nThe scale closes with `--breakpoint-xl: initial` and `--breakpoint-2xl: initial`.'
|
|
229
|
+
: '';
|
|
230
|
+
return text([`# Prism token scale: ${group}`, preamble, `${lines}${close}`, footer].join('\n\n'));
|
|
231
|
+
}
|
|
232
|
+
/* -------------------------------------------------------------------------- */
|
|
233
|
+
/* list_pages / get_page */
|
|
234
|
+
/* -------------------------------------------------------------------------- */
|
|
235
|
+
export function renderListPages(store) {
|
|
236
|
+
// The Sections and the label each is read under are both the Store's own, so a
|
|
237
|
+
// Section added to the content tree appears here from the same edit that added
|
|
238
|
+
// it there. A hand-kept list of the three prose Sections is what would leave
|
|
239
|
+
// the Changelogs out of the one tool that exists to list pages.
|
|
240
|
+
const groups = STORE_SECTIONS.map((section) => ({
|
|
241
|
+
section,
|
|
242
|
+
heading: STORE_SECTION_TITLES[section],
|
|
243
|
+
pages: store.pages.filter((page) => page.section === section),
|
|
244
|
+
}));
|
|
245
|
+
const counted = groups.map((group) => `${group.pages.length} ${group.heading.toLowerCase()}`);
|
|
246
|
+
const header = `# Prism pages - ${store.pages.length} pages (${counted.join(', ')})`;
|
|
247
|
+
const sections = groups
|
|
248
|
+
.filter((group) => group.pages.length > 0)
|
|
249
|
+
.map((group) => `## ${group.heading}\n\n${group.pages
|
|
250
|
+
.map((page) => `- **${page.title}** - ${page.description} (\`${page.url}\`)`)
|
|
251
|
+
.join('\n')}`);
|
|
252
|
+
const footer = `Use \`get_page({ url })\` to read one, or \`get_changelog\` for a published package's changes.\n\nImport rule: ${IMPORT_RULE}`;
|
|
253
|
+
return text([header, ...sections, footer].join('\n\n'));
|
|
254
|
+
}
|
|
255
|
+
function suggestPages(store, normalized) {
|
|
256
|
+
const needle = normalized.replace(/^\//, '').toLowerCase();
|
|
257
|
+
return store.pages
|
|
258
|
+
.map((page) => {
|
|
259
|
+
const candidate = `${page.section}/${page.slug}`.toLowerCase();
|
|
260
|
+
let score = candidate.includes(needle) || needle.includes(candidate) ? 3 : 0;
|
|
261
|
+
let common = 0;
|
|
262
|
+
while (common < candidate.length && common < needle.length && candidate[common] === needle[common]) {
|
|
263
|
+
common += 1;
|
|
264
|
+
}
|
|
265
|
+
score += common;
|
|
266
|
+
return { page, score };
|
|
267
|
+
})
|
|
268
|
+
.filter((entry) => entry.score > 0)
|
|
269
|
+
.sort((a, b) => b.score - a.score || a.page.title.localeCompare(b.page.title))
|
|
270
|
+
.slice(0, 3)
|
|
271
|
+
.map((entry) => entry.page);
|
|
272
|
+
}
|
|
273
|
+
export function renderPage(store, args) {
|
|
274
|
+
const raw = args.url.trim();
|
|
275
|
+
const withoutMd = raw.endsWith('.md') ? raw.slice(0, -3) : raw;
|
|
276
|
+
const normalized = withoutMd.startsWith('/') ? withoutMd : `/${withoutMd}`;
|
|
277
|
+
if (/^\/(components|blocks|pages)\//.test(normalized)) {
|
|
278
|
+
return error(`\`${args.url}\` is a catalogue item URL, not a documentation page. Use \`get_item_doc\` or \`get_item_source\` for an item.`);
|
|
279
|
+
}
|
|
280
|
+
const page = store.pages.find((entry) => entry.url === normalized || entry.mirror === raw || entry.url === withoutMd);
|
|
281
|
+
if (!page) {
|
|
282
|
+
const suggestions = suggestPages(store, normalized);
|
|
283
|
+
const did = suggestions.length > 0
|
|
284
|
+
? ` Did you mean ${suggestions.map((entry) => `\`${entry.url}\``).join(', ')}?`
|
|
285
|
+
: '';
|
|
286
|
+
return error(`No Prism page at \`${args.url}\`.${did} List the pages with \`list_pages\`.`);
|
|
287
|
+
}
|
|
288
|
+
return text(`${page.markdown.trimEnd()}\n\n---\n\nMirror: \`${page.mirror}\`\n\nImport rule: ${IMPORT_RULE}`);
|
|
289
|
+
}
|
|
290
|
+
export function searchCandidates(store, kind) {
|
|
291
|
+
const candidates = [];
|
|
292
|
+
if (kind === undefined || kind !== 'doc') {
|
|
293
|
+
for (const item of store.items) {
|
|
294
|
+
if (kind !== undefined && item.kind !== kind)
|
|
295
|
+
continue;
|
|
296
|
+
candidates.push({
|
|
297
|
+
title: item.name,
|
|
298
|
+
kind: item.kind,
|
|
299
|
+
url: item.url,
|
|
300
|
+
snippet: item.description,
|
|
301
|
+
content: item.doc,
|
|
302
|
+
followUp: `get_item_doc({ name: "${item.name}", kind: "${item.kind}" })`,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (kind === undefined || kind === 'doc') {
|
|
307
|
+
for (const page of store.pages) {
|
|
308
|
+
candidates.push({
|
|
309
|
+
title: page.title,
|
|
310
|
+
kind: 'doc',
|
|
311
|
+
url: page.url,
|
|
312
|
+
snippet: page.description,
|
|
313
|
+
content: page.markdown,
|
|
314
|
+
followUp: `get_page({ url: "${page.url}" })`,
|
|
315
|
+
});
|
|
33
316
|
}
|
|
34
|
-
for (const item of inKind)
|
|
35
|
-
lines.push(catalogEntry(item));
|
|
36
|
-
lines.push('');
|
|
37
317
|
}
|
|
38
|
-
|
|
39
|
-
return lines.join('\n');
|
|
318
|
+
return candidates;
|
|
40
319
|
}
|
|
41
|
-
function
|
|
42
|
-
const
|
|
43
|
-
|
|
320
|
+
export function searchDocs(store, args) {
|
|
321
|
+
const limit = Math.min(Math.max(args.limit ?? 5, 1), 10);
|
|
322
|
+
const terms = args.query
|
|
323
|
+
.toLowerCase()
|
|
324
|
+
.split(/\s+/)
|
|
325
|
+
.filter(Boolean);
|
|
326
|
+
const hits = [];
|
|
327
|
+
for (const candidate of searchCandidates(store, args.kind)) {
|
|
328
|
+
const name = candidate.title.toLowerCase();
|
|
329
|
+
const description = candidate.snippet.toLowerCase();
|
|
330
|
+
const content = candidate.content.toLowerCase();
|
|
331
|
+
const matched = new Set();
|
|
332
|
+
let score = 0;
|
|
333
|
+
let matchesAll = true;
|
|
334
|
+
for (const term of terms) {
|
|
335
|
+
let termScore = 0;
|
|
336
|
+
if (name.includes(term)) {
|
|
337
|
+
matched.add('name');
|
|
338
|
+
termScore += 5;
|
|
339
|
+
}
|
|
340
|
+
if (description.includes(term)) {
|
|
341
|
+
matched.add('description');
|
|
342
|
+
termScore += 3;
|
|
343
|
+
}
|
|
344
|
+
if (content.includes(term)) {
|
|
345
|
+
matched.add('content');
|
|
346
|
+
termScore += 1;
|
|
347
|
+
}
|
|
348
|
+
if (termScore === 0) {
|
|
349
|
+
matchesAll = false;
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
score += termScore;
|
|
353
|
+
}
|
|
354
|
+
if (!matchesAll)
|
|
355
|
+
continue;
|
|
356
|
+
hits.push({ ...candidate, matched: [...matched], score });
|
|
357
|
+
}
|
|
358
|
+
hits.sort((a, b) => b.score - a.score || a.title.localeCompare(b.title));
|
|
359
|
+
const top = hits.slice(0, limit);
|
|
360
|
+
if (top.length === 0) {
|
|
361
|
+
return text(`No matches for \`${args.query}\`${args.kind ? ` in kind \`${args.kind}\`` : ''}. It searches item names, descriptions and page bodies. Try a shorter query, or browse with \`list_items\` and \`list_pages\`.`);
|
|
362
|
+
}
|
|
363
|
+
const header = `# Search: ${args.query}\n\n${top.length} hit${top.length === 1 ? '' : 's'}${args.kind ? ` in kind \`${args.kind}\`` : ''}.`;
|
|
364
|
+
const lines = top.map((hit) => `- **${hit.title}** (${hit.kind}) - ${hit.snippet} [matched: ${hit.matched.join(', ')}] -> \`${hit.followUp}\``);
|
|
365
|
+
const footer = `Read the body with the follow-up call, or repeat a narrower search.\n\nImport rule: ${IMPORT_RULE}`;
|
|
366
|
+
return text([header, lines.join('\n'), footer].join('\n\n'));
|
|
44
367
|
}
|
|
45
368
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
369
|
+
* Every published package that ships a changelog, named and routed, for a miss.
|
|
370
|
+
*
|
|
371
|
+
* The miss carries the whole set rather than a "did you mean", because the
|
|
372
|
+
* argument is a free string rather than a closed enum: an agent that guessed a
|
|
373
|
+
* package name gets the real names in one round trip instead of guessing again.
|
|
48
374
|
*/
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
375
|
+
function changelogMiss(store, wanted) {
|
|
376
|
+
const available = store.changelogs.length === 0
|
|
377
|
+
? 'This build carries no package changelogs.'
|
|
378
|
+
: `Prism publishes a changelog for: ${store.changelogs
|
|
379
|
+
.map((entry) => `\`${entry.package}\` (\`${entry.route}\`)`)
|
|
380
|
+
.join(', ')}.`;
|
|
381
|
+
return error(`No Prism changelog for \`${wanted}\`. ${available} Read one with \`get_changelog({ package })\`.`);
|
|
382
|
+
}
|
|
383
|
+
/** The entry a package name, unscoped name or route segment resolves to. */
|
|
384
|
+
function changelogFor(store, wanted) {
|
|
385
|
+
const needle = wanted.trim().toLowerCase();
|
|
386
|
+
return store.changelogs.find((entry) => entry.package.toLowerCase() === needle || entry.slug.toLowerCase() === needle);
|
|
58
387
|
}
|
|
59
388
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* is the
|
|
389
|
+
* One published package's changelog, or one version entry of it.
|
|
390
|
+
*
|
|
391
|
+
* The text is the package's own file, byte for byte, because the Store carries
|
|
392
|
+
* the bytes rather than a summary of them. A generated summary would drift the
|
|
393
|
+
* moment a release was published, which is the one thing a changelog cannot do:
|
|
394
|
+
* a breaking change in a published package has to be discoverable by an agent
|
|
395
|
+
* that never saw the release notes anywhere else. The reference design system
|
|
396
|
+
* shipped a breaking module-system change that no agent using it could find, and
|
|
397
|
+
* the reason is precisely that its corpus had no changelog and no tool to return
|
|
398
|
+
* one.
|
|
399
|
+
*
|
|
400
|
+
* The footers state where the text is published, so an agent holding an older
|
|
401
|
+
* corpus can still fetch the same bytes over HTTP, and which versions exist, so
|
|
402
|
+
* one call can pick the next `version` to ask for.
|
|
63
403
|
*/
|
|
64
|
-
export function
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
if (
|
|
71
|
-
return [
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
'',
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
'```',
|
|
95
|
-
'',
|
|
96
|
-
`Verbatim from the docs demo — self-contained (imports only \`@nanisoft/prism-ui\` and \`react\`). ${IMPORT_RULE}`,
|
|
97
|
-
];
|
|
98
|
-
if (others.length > 0)
|
|
99
|
-
lines.push('', `Other examples: ${others.join(', ')} — pass \`example\` to pick one.`);
|
|
100
|
-
return lines.join('\n');
|
|
101
|
-
}
|
|
102
|
-
/** `get_theme_doc` — the pack × mode atom verbatim, plus the sibling slugs. */
|
|
103
|
-
export function themeMarkdown(theme, all) {
|
|
104
|
-
const others = all.filter((entry) => entry.slug !== theme.slug).map((entry) => `\`${entry.slug}\``);
|
|
105
|
-
const footer = others.length > 0 ? `\n\n---\n\nOther theme atoms: ${others.join(', ')} — pass \`pack\`/\`mode\` to switch.` : '';
|
|
106
|
-
return `${theme.markdown}${footer}`;
|
|
107
|
-
}
|
|
108
|
-
/** `list_pages` — the site's llms.txt body as an index. */
|
|
109
|
-
export function pagesIndexMarkdown(pages, baseUrl) {
|
|
110
|
-
const lines = ['# Prism docs pages', ''];
|
|
111
|
-
if (pages.length === 0) {
|
|
112
|
-
lines.push('This build carries no docs pages — the catalog is the whole corpus for now.');
|
|
113
|
-
}
|
|
114
|
-
else {
|
|
115
|
-
lines.push(`Found ${pages.length}:`, '');
|
|
116
|
-
for (const page of pages) {
|
|
117
|
-
lines.push(`- **${page.title}** — \`${page.url}\`${page.description ? ` — ${page.description}` : ''}`);
|
|
118
|
-
lines.push(` → \`get_page { "url": "${page.url}" }\``);
|
|
119
|
-
}
|
|
120
|
-
lines.push('');
|
|
121
|
-
}
|
|
122
|
-
lines.push('---', '', `Full site index: ${baseUrl}/llms.txt · Catalog: \`list_items\`.`);
|
|
123
|
-
return lines.join('\n');
|
|
124
|
-
}
|
|
125
|
-
/** `get_page` — the prerendered page verbatim, cited back to the site. */
|
|
126
|
-
export function pageMarkdown(page, baseUrl) {
|
|
127
|
-
return `${page.markdown}\n\n---\n\nSource: ${baseUrl}${page.url} · More: \`list_pages\`.`;
|
|
128
|
-
}
|
|
129
|
-
/** `search_docs` — ranked hits, each printing its own next call. */
|
|
130
|
-
export function searchResultsMarkdown(query, hits) {
|
|
131
|
-
const lines = [`# Search results for "${query}"`, ''];
|
|
132
|
-
if (hits.length === 0) {
|
|
133
|
-
lines.push('No matches.', '', 'Try broader terms — or browse: `list_items` for the catalog, `list_pages` for docs pages.');
|
|
134
|
-
return lines.join('\n');
|
|
135
|
-
}
|
|
136
|
-
lines.push(`Found ${hits.length} result(s):`, '');
|
|
137
|
-
hits.forEach((hit, index) => {
|
|
138
|
-
if (index > 0)
|
|
139
|
-
lines.push('---', '');
|
|
140
|
-
lines.push(`## ${hit.title} (${hit.kind})`, hit.snippet, `→ \`${hit.followUp}\`${hit.followUp.startsWith('get_item_source') ? ' for the copyable source.' : ''}`);
|
|
141
|
-
});
|
|
142
|
-
return lines.join('\n');
|
|
143
|
-
}
|
|
144
|
-
/** A lookup miss: the closest names plus the browse pointer, as an error. */
|
|
145
|
-
export function missMarkdown(target, suggestions, browseHint) {
|
|
146
|
-
const lines = [`\`${target}\` not found.`];
|
|
147
|
-
if (suggestions.length > 0)
|
|
148
|
-
lines.push('', `Did you mean: ${suggestions.map((name) => `**${name}**`).join(', ')}?`);
|
|
149
|
-
lines.push('', browseHint);
|
|
150
|
-
return lines.join('\n');
|
|
151
|
-
}
|
|
152
|
-
/** A name hit whose `kind` argument pointed somewhere else. */
|
|
153
|
-
export function kindMismatchMarkdown(item, requested) {
|
|
154
|
-
return `\`${item.name}\` is a ${item.kind}, not a ${requested}. Retry with \`kind\` omitted — or with \`"${item.kind}"\`.`;
|
|
155
|
-
}
|
|
156
|
-
//# sourceMappingURL=render.js.map
|
|
404
|
+
export function renderChangelog(store, args) {
|
|
405
|
+
const changelog = changelogFor(store, args.package);
|
|
406
|
+
if (changelog === undefined)
|
|
407
|
+
return changelogMiss(store, args.package);
|
|
408
|
+
const versions = changelog.versions.join(', ');
|
|
409
|
+
const published = `Published at \`${changelog.route}\` and its mirror \`${changelog.route}.md\`, byte for byte from the package.`;
|
|
410
|
+
if (args.version === undefined) {
|
|
411
|
+
return text([
|
|
412
|
+
`# ${changelog.package} - changelog`,
|
|
413
|
+
`Versions, newest first as the file records them: ${versions}.`,
|
|
414
|
+
changelog.text.trimEnd(),
|
|
415
|
+
'---',
|
|
416
|
+
`${published} Ask for one entry with \`get_changelog({ package: "${changelog.package}", version: "<version>" })\`.`,
|
|
417
|
+
`Import rule: ${IMPORT_RULE}`,
|
|
418
|
+
].join('\n\n'));
|
|
419
|
+
}
|
|
420
|
+
const wanted = args.version.trim();
|
|
421
|
+
const release = changelog.releases.find((entry) => entry.version === wanted);
|
|
422
|
+
if (release === undefined) {
|
|
423
|
+
return error(`\`${wanted}\` is not a version \`${changelog.package}\` records. It records: ${versions}. ` +
|
|
424
|
+
`Read the whole changelog with \`get_changelog({ package: "${changelog.package}" })\`.`);
|
|
425
|
+
}
|
|
426
|
+
return text([
|
|
427
|
+
`# ${changelog.package} - ${release.version}`,
|
|
428
|
+
release.body,
|
|
429
|
+
'---',
|
|
430
|
+
`${published} Every version: ${versions}. Read the whole changelog with \`get_changelog({ package: "${changelog.package}" })\`.`,
|
|
431
|
+
`Import rule: ${IMPORT_RULE}`,
|
|
432
|
+
].join('\n\n'));
|
|
433
|
+
}
|
package/dist/rules.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The three rules every tool description carries.
|
|
3
|
+
*
|
|
4
|
+
* Ticket 13 requires each description to state the import rule and the surface
|
|
5
|
+
* rule, so a client that reads only `tools/list` learns the contract without a
|
|
6
|
+
* round-trip. The no-invention rule is repeated by `get_theme_doc`.
|
|
7
|
+
*/
|
|
8
|
+
export declare const IMPORT_RULE = "Import from `@nanisoft/prism-ui/components/<slug>`, `./blocks/<slug>` or `./pages/<slug>`. Base UI and internal paths are never a consumer import.";
|
|
9
|
+
export declare const SURFACE_RULE = "Prism answers its complete owned surface; Base UI and native elements are internal implementation details.";
|
|
10
|
+
export declare const NO_INVENTION_RULE = "Motion and typography are named by token, never by a millisecond or a `cubic-bezier` literal.";
|
|
11
|
+
/** Compose a tool description: the intro, then the two standing rules. */
|
|
12
|
+
export declare function describe(intro: string): string;
|
package/dist/rules.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The three rules every tool description carries.
|
|
3
|
+
*
|
|
4
|
+
* Ticket 13 requires each description to state the import rule and the surface
|
|
5
|
+
* rule, so a client that reads only `tools/list` learns the contract without a
|
|
6
|
+
* round-trip. The no-invention rule is repeated by `get_theme_doc`.
|
|
7
|
+
*/
|
|
8
|
+
export const IMPORT_RULE = 'Import from `@nanisoft/prism-ui/components/<slug>`, `./blocks/<slug>` or `./pages/<slug>`. Base UI and internal paths are never a consumer import.';
|
|
9
|
+
export const SURFACE_RULE = 'Prism answers its complete owned surface; Base UI and native elements are internal implementation details.';
|
|
10
|
+
export const NO_INVENTION_RULE = 'Motion and typography are named by token, never by a millisecond or a `cubic-bezier` literal.';
|
|
11
|
+
/** Compose a tool description: the intro, then the two standing rules. */
|
|
12
|
+
export function describe(intro) {
|
|
13
|
+
return `${intro}\n\nImport rule: ${IMPORT_RULE}\n\nSurface rule: ${SURFACE_RULE}`;
|
|
14
|
+
}
|