@sakaniui/react 0.8.6 → 0.8.8
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 +21 -0
- package/CHANGELOG.md +472 -0
- package/README.md +33 -0
- package/bin/sakaniui-mcp.mjs +362 -0
- package/dist/components/IconButton/IconButton.d.ts +1 -1
- package/dist/manifest.json +8687 -0
- package/package.json +9 -3
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SakaniUI MCP server -- lets an AI editor/agent look up the design system instead of guessing.
|
|
4
|
+
*
|
|
5
|
+
* npx -y -p @sakaniui/react sakaniui-mcp [--project <dir>]
|
|
6
|
+
*
|
|
7
|
+
* Zero dependencies: speaks newline-delimited JSON-RPC (MCP stdio transport).
|
|
8
|
+
*
|
|
9
|
+
* Read-only by construction. It reads four fixed files (package.json, dist/manifest.json,
|
|
10
|
+
* AGENTS.md, CHANGELOG.md) from a @sakaniui/react install and writes nothing, runs nothing and
|
|
11
|
+
* opens no network connection. No tool takes a path; every tool argument is validated.
|
|
12
|
+
*
|
|
13
|
+
* Version-aware. `npx` may fetch a newer release than the project uses, so the server looks for
|
|
14
|
+
* the project's own installed @sakaniui/react (walking up from --project or the working
|
|
15
|
+
* directory). If it has a manifest, answers come from that copy. If it is older than the
|
|
16
|
+
* manifest era, answers come from this copy but every component is checked against its `since`
|
|
17
|
+
* release and flagged when the project does not have it yet.
|
|
18
|
+
*/
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import readline from 'node:readline';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
|
|
24
|
+
const ownRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
25
|
+
|
|
26
|
+
/* ---------- reading an install (fixed file names only) ---------- */
|
|
27
|
+
const readText = (root, rel) => { try { return fs.readFileSync(path.join(root, rel), 'utf8'); } catch { return null; } };
|
|
28
|
+
const readJson = (root, rel) => { try { return JSON.parse(readText(root, rel)); } catch { return null; } };
|
|
29
|
+
|
|
30
|
+
function loadInstall(root) {
|
|
31
|
+
const pkg = readJson(root, 'package.json');
|
|
32
|
+
if (!pkg || pkg.name !== '@sakaniui/react') return null;
|
|
33
|
+
return { root, version: pkg.version, manifest: readJson(root, 'dist/manifest.json'), guide: readText(root, 'AGENTS.md'), changelog: readText(root, 'CHANGELOG.md') };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function findProjectInstall() {
|
|
37
|
+
const i = process.argv.indexOf('--project');
|
|
38
|
+
let dir = path.resolve(i > -1 && process.argv[i + 1] ? process.argv[i + 1] : process.cwd());
|
|
39
|
+
for (let depth = 0; depth < 12; depth++) {
|
|
40
|
+
const candidate = path.join(dir, 'node_modules', '@sakaniui', 'react');
|
|
41
|
+
if (fs.existsSync(path.join(candidate, 'package.json'))) return loadInstall(candidate);
|
|
42
|
+
const up = path.dirname(dir);
|
|
43
|
+
if (up === dir) break;
|
|
44
|
+
dir = up;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const bundled = loadInstall(ownRoot);
|
|
50
|
+
if (!bundled || !bundled.manifest) {
|
|
51
|
+
process.stderr.write('sakaniui-mcp: dist/manifest.json not found. Run `npm run build:lib` (or install a published version).\n');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
const project = findProjectInstall();
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Answer from the project's own copy when it can (it is what the code will run against) -- but only
|
|
58
|
+
* if its manifest is at least as complete as this one. Older manifest schemas are known to omit
|
|
59
|
+
* things that exist (components, tokens), and "not in the manifest" must mean "does not exist".
|
|
60
|
+
* In that case the newer data is used and checked against the project's version instead.
|
|
61
|
+
*/
|
|
62
|
+
const schemaOf = (i) => (i && i.manifest && Number.isInteger(i.manifest.schemaVersion) ? i.manifest.schemaVersion : 0);
|
|
63
|
+
const active = project && project.manifest && schemaOf(project) >= schemaOf(bundled) ? project : bundled;
|
|
64
|
+
const manifest = active.manifest;
|
|
65
|
+
const guide = active.guide || bundled.guide || '';
|
|
66
|
+
const changelog = active.changelog || bundled.changelog || '';
|
|
67
|
+
const projectVersion = project ? project.version : null;
|
|
68
|
+
|
|
69
|
+
/* ---------- versions ---------- */
|
|
70
|
+
const SEMVER = /^\d+\.\d+\.\d+/;
|
|
71
|
+
const parse = (v) => String(v).split('-')[0].split('.').map(Number);
|
|
72
|
+
const cmp = (a, b) => { const x = parse(a), y = parse(b); for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] - y[i]; return 0; };
|
|
73
|
+
|
|
74
|
+
const refVersion = manifest.version;
|
|
75
|
+
const answeringFromProject = active === project;
|
|
76
|
+
|
|
77
|
+
function versionNote(installed = projectVersion) {
|
|
78
|
+
if (answeringFromProject) return null;
|
|
79
|
+
if (!installed) {
|
|
80
|
+
return `The project's installed @sakaniui/react version could not be detected, so this describes ${refVersion}. Confirm it (npm ls @sakaniui/react) and call check_compatibility with installedVersion before suggesting a component.`;
|
|
81
|
+
}
|
|
82
|
+
if (installed !== refVersion) {
|
|
83
|
+
return `The project has @sakaniui/react ${installed} installed; this reference describes ${refVersion}. Entries marked availableInInstalled: false were added after ${installed}. Props can also differ between releases: check get_changelog.`;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Is this component in the project's install? null = cannot tell. */
|
|
89
|
+
function inInstalled(c, installed = projectVersion) {
|
|
90
|
+
if (!installed || !c.since) return null;
|
|
91
|
+
return cmp(c.since, installed) <= 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* ---------- helpers ---------- */
|
|
95
|
+
const byName = new Map(manifest.components.map((c) => [c.name.toLowerCase(), c]));
|
|
96
|
+
const brief = (c) => {
|
|
97
|
+
const out = { name: c.name, kind: c.kind, category: c.category, import: c.import, since: c.since, summary: c.summary, requiredProps: c.contract.requiredProps };
|
|
98
|
+
if (inInstalled(c) === false) out.availableInInstalled = false;
|
|
99
|
+
return out;
|
|
100
|
+
};
|
|
101
|
+
const text = (obj) => ({ content: [{ type: 'text', text: typeof obj === 'string' ? obj : JSON.stringify(obj, null, 2) }] });
|
|
102
|
+
const fail = (msg) => ({ isError: true, content: [{ type: 'text', text: msg }] });
|
|
103
|
+
|
|
104
|
+
const MAX_STR = 200;
|
|
105
|
+
function strArg(args, key, { required = false } = {}) {
|
|
106
|
+
const v = args[key];
|
|
107
|
+
if (v === undefined || v === null) return required ? { err: `"${key}" is required.` } : { value: undefined };
|
|
108
|
+
if (typeof v !== 'string') return { err: `"${key}" must be a string.` };
|
|
109
|
+
if (v.length > MAX_STR) return { err: `"${key}" is too long (max ${MAX_STR} characters).` };
|
|
110
|
+
return { value: v };
|
|
111
|
+
}
|
|
112
|
+
function enumArg(args, key, allowed) {
|
|
113
|
+
const v = args[key];
|
|
114
|
+
if (v === undefined || v === null) return { value: undefined };
|
|
115
|
+
if (!allowed.includes(v)) return { err: `"${key}" must be one of: ${allowed.join(', ')}.` };
|
|
116
|
+
return { value: v };
|
|
117
|
+
}
|
|
118
|
+
const verArg = (args, key) => {
|
|
119
|
+
const s = strArg(args, key);
|
|
120
|
+
if (s.err || s.value === undefined) return s;
|
|
121
|
+
return SEMVER.test(s.value) ? s : { err: `"${key}" must be a version like 0.8.7.` };
|
|
122
|
+
};
|
|
123
|
+
const firstError = (...rs) => rs.find((r) => r.err);
|
|
124
|
+
|
|
125
|
+
function score(c, terms) {
|
|
126
|
+
const hay = `${c.name} ${c.category ?? ''} ${c.summary} ${c.props.map((p) => p.name).join(' ')}`.toLowerCase();
|
|
127
|
+
let s = 0;
|
|
128
|
+
for (const t of terms) {
|
|
129
|
+
if (c.name.toLowerCase() === t) s += 20;
|
|
130
|
+
else if (c.name.toLowerCase().includes(t)) s += 8;
|
|
131
|
+
if ((c.category ?? '').toLowerCase().includes(t)) s += 3;
|
|
132
|
+
if (c.summary.toLowerCase().includes(t)) s += 3;
|
|
133
|
+
else if (hay.includes(t)) s += 1;
|
|
134
|
+
}
|
|
135
|
+
return s;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function distance(a, b) {
|
|
139
|
+
const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
140
|
+
for (let j = 1; j <= b.length; j++) d[0][j] = j;
|
|
141
|
+
for (let i = 1; i <= a.length; i++) for (let j = 1; j <= b.length; j++) d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
142
|
+
return d[a.length][b.length];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function closest(name) {
|
|
146
|
+
const n = name.toLowerCase();
|
|
147
|
+
return manifest.components
|
|
148
|
+
.map((c) => [c.name.toLowerCase().includes(n) || n.includes(c.name.toLowerCase()) ? 0 : distance(n, c.name.toLowerCase()), c.name])
|
|
149
|
+
.filter(([d]) => d <= 2)
|
|
150
|
+
.sort((a, b) => a[0] - b[0])
|
|
151
|
+
.slice(0, 5)
|
|
152
|
+
.map(([, c]) => c);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const NATIVE = /^(className|style|id|ref|key|children|role|tabIndex|title|hidden|lang|dir|slot|on[A-Z].*|aria-.*|data-.*)$/;
|
|
156
|
+
|
|
157
|
+
/** Release notes: [{version, body}] newest first, from the CHANGELOG that matches the answering install. */
|
|
158
|
+
function releases() {
|
|
159
|
+
const parts = changelog.split(/^## (\d+\.\d+\.\d+[^\n]*)\n/m);
|
|
160
|
+
const out = [];
|
|
161
|
+
for (let i = 1; i < parts.length; i += 2) out.push({ version: parts[i].trim(), body: (parts[i + 1] || '').trim() });
|
|
162
|
+
return out.sort((a, b) => cmp(b.version, a.version));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const READ_ONLY = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
166
|
+
|
|
167
|
+
/* ---------- tools ---------- */
|
|
168
|
+
const tools = {
|
|
169
|
+
search_components: {
|
|
170
|
+
description: 'Find SakaniUI components/blocks by what you need (e.g. "date picker", "sidebar navigation", "pricing"). Returns the best matches with import path, the release that added them and required props.',
|
|
171
|
+
inputSchema: { type: 'object', properties: { query: { type: 'string' }, kind: { type: 'string', enum: ['component', 'block'] }, limit: { type: 'number' } }, required: ['query'] },
|
|
172
|
+
run(args) {
|
|
173
|
+
const q = strArg(args, 'query', { required: true }); const k = enumArg(args, 'kind', ['component', 'block']);
|
|
174
|
+
const e = firstError(q, k); if (e) return fail(e.err);
|
|
175
|
+
const limit = Number.isInteger(args.limit) ? Math.min(Math.max(args.limit, 1), 25) : 8;
|
|
176
|
+
const terms = q.value.toLowerCase().split(/\s+/).filter(Boolean);
|
|
177
|
+
const hits = manifest.components
|
|
178
|
+
.filter((c) => !k.value || c.kind === k.value)
|
|
179
|
+
.map((c) => [score(c, terms), c])
|
|
180
|
+
.filter(([s]) => s > 0)
|
|
181
|
+
.sort((a, b) => b[0] - a[0])
|
|
182
|
+
.slice(0, limit)
|
|
183
|
+
.map(([, c]) => brief(c));
|
|
184
|
+
const note = versionNote();
|
|
185
|
+
const body = hits.length ? hits : `No match for "${q.value}". If nothing fits, follow AGENTS.md section 3 ("When nothing fits"): compose from tokens and say so. Try list_components to browse.`;
|
|
186
|
+
return text(note && hits.length ? { versionNote: note, results: hits } : body);
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
get_component: {
|
|
190
|
+
description: 'Full contract for one component or block: import path, the release that added it, every prop with type / allowed values / default / required, and the accessible-name rule. Use this before writing JSX.',
|
|
191
|
+
inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
|
|
192
|
+
run(args) {
|
|
193
|
+
const n = strArg(args, 'name', { required: true }); if (n.err) return fail(n.err);
|
|
194
|
+
const c = byName.get(n.value.toLowerCase());
|
|
195
|
+
if (!c) return fail(`No component named "${n.value}". Did you mean: ${closest(n.value).join(', ') || '(none close)'}? Use search_components.`);
|
|
196
|
+
const out = { ...c, note: 'Native HTML attributes (className, style, aria-*, event handlers) are also accepted on the root element.' };
|
|
197
|
+
if (inInstalled(c) === false) out.availableInInstalled = false;
|
|
198
|
+
const note = versionNote();
|
|
199
|
+
if (note) out.versionNote = note;
|
|
200
|
+
if (inInstalled(c) === false) out.warning = `${c.name} was added in ${c.since} but the project has ${projectVersion}. Do not use it without upgrading (npm install @sakaniui/react@latest).`;
|
|
201
|
+
return text(out);
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
list_components: {
|
|
205
|
+
description: 'List all components and blocks (name + one-line summary), optionally filtered by kind or category (e.g. Forms, Core, Marketing).',
|
|
206
|
+
inputSchema: { type: 'object', properties: { kind: { type: 'string', enum: ['component', 'block'] }, category: { type: 'string' } } },
|
|
207
|
+
run(args) {
|
|
208
|
+
const k = enumArg(args, 'kind', ['component', 'block']); const cat = strArg(args, 'category');
|
|
209
|
+
const e = firstError(k, cat); if (e) return fail(e.err);
|
|
210
|
+
const list = manifest.components
|
|
211
|
+
.filter((c) => (!k.value || c.kind === k.value) && (!cat.value || (c.category ?? '').toLowerCase().includes(cat.value.toLowerCase())))
|
|
212
|
+
.map((c) => { const o = { name: c.name, kind: c.kind, category: c.category, since: c.since, summary: c.summary }; if (inInstalled(c) === false) o.availableInInstalled = false; return o; });
|
|
213
|
+
const categories = [...new Set(manifest.components.map((c) => c.category).filter(Boolean))].sort();
|
|
214
|
+
const out = { version: refVersion, count: list.length, categories, components: list };
|
|
215
|
+
const note = versionNote(); if (note) out.versionNote = note;
|
|
216
|
+
return text(out);
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
get_tokens: {
|
|
220
|
+
description: 'Design tokens (CSS custom properties). Pass a group (color, space, radius, shadow, font, motion, glass, surface, liquid, ...) and/or a substring to filter. Only these tokens exist.',
|
|
221
|
+
inputSchema: { type: 'object', properties: { group: { type: 'string' }, query: { type: 'string' } } },
|
|
222
|
+
run(args) {
|
|
223
|
+
const g = strArg(args, 'group'); const q = strArg(args, 'query');
|
|
224
|
+
const e = firstError(g, q); if (e) return fail(e.err);
|
|
225
|
+
const groups = manifest.tokens.groups;
|
|
226
|
+
if (g.value && !Object.hasOwn(groups, g.value)) return fail(`Unknown token group "${g.value}". Groups: ${Object.keys(groups).join(', ')}`);
|
|
227
|
+
const needle = q.value?.toLowerCase();
|
|
228
|
+
const out = {};
|
|
229
|
+
for (const [name, toks] of Object.entries(groups)) {
|
|
230
|
+
if (g.value && name !== g.value) continue;
|
|
231
|
+
const sel = Object.entries(toks).filter(([k, v]) => !needle || k.includes(needle) || String(v).toLowerCase().includes(needle));
|
|
232
|
+
if (sel.length) out[name] = Object.fromEntries(sel);
|
|
233
|
+
}
|
|
234
|
+
return text(Object.keys(out).length ? out : `No tokens match. Groups: ${Object.keys(groups).join(', ')}`);
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
check_usage: {
|
|
238
|
+
description: 'Validate props you are about to use against the contract: reports missing required props, values outside an enum, props that do not exist, and components the project\'s installed version does not have. Run it on each component you write.',
|
|
239
|
+
inputSchema: { type: 'object', properties: { component: { type: 'string' }, props: { type: 'object', description: 'prop name -> value (use true/false/numbers/strings; for non-literal values pass any placeholder)' } }, required: ['component'] },
|
|
240
|
+
run(args) {
|
|
241
|
+
const n = strArg(args, 'component', { required: true }); if (n.err) return fail(n.err);
|
|
242
|
+
const props = args.props === undefined || args.props === null ? {} : args.props;
|
|
243
|
+
if (typeof props !== 'object' || Array.isArray(props)) return fail('"props" must be an object of prop name -> value.');
|
|
244
|
+
if (Object.keys(props).length > 200) return fail('"props" has too many entries (max 200).');
|
|
245
|
+
const c = byName.get(n.value.toLowerCase());
|
|
246
|
+
if (!c) return fail(`No component named "${n.value}". Did you mean: ${closest(n.value).join(', ') || '(none close)'}?`);
|
|
247
|
+
const known = new Map(c.props.map((p) => [p.name, p]));
|
|
248
|
+
const errors = [];
|
|
249
|
+
const warnings = [];
|
|
250
|
+
if (inInstalled(c) === false) errors.push(`${c.name} was added in ${c.since}; the project has ${projectVersion}. It does not exist there.`);
|
|
251
|
+
for (const r of c.contract.requiredProps) if (!Object.hasOwn(props, r)) errors.push(`Missing required prop "${r}".`);
|
|
252
|
+
for (const [k, v] of Object.entries(props)) {
|
|
253
|
+
const p = known.get(k);
|
|
254
|
+
if (!p) { if (!NATIVE.test(k)) warnings.push(`"${k}" is not a ${c.name} prop in the manifest (it would only work if it is a native attribute).`); continue; }
|
|
255
|
+
if (p.values && (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') && !p.values.includes(v)) {
|
|
256
|
+
errors.push(`"${k}" = ${JSON.stringify(v)} is not allowed. Use one of: ${p.values.map((x) => JSON.stringify(x)).join(', ')}.`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (c.contract.requiresAccessibleName && !errors.length) warnings.push('This control has no visible text of its own: keep its accessible-name prop set.');
|
|
260
|
+
const out = { component: c.name, ok: errors.length === 0, errors, warnings };
|
|
261
|
+
const note = versionNote(); if (note) out.versionNote = note;
|
|
262
|
+
return text(out);
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
check_compatibility: {
|
|
266
|
+
description: 'Compare the version this reference describes with the version installed in the project, and list what the project does not have yet. Call this first when you cannot tell which @sakaniui/react version the project uses (pass installedVersion, from `npm ls @sakaniui/react` or package.json).',
|
|
267
|
+
inputSchema: { type: 'object', properties: { installedVersion: { type: 'string', description: 'e.g. 0.7.0' } } },
|
|
268
|
+
run(args) {
|
|
269
|
+
const v = verArg(args, 'installedVersion'); if (v.err) return fail(v.err.err);
|
|
270
|
+
const installed = v.value ? v.value.match(SEMVER)[0] : projectVersion;
|
|
271
|
+
const source = v.value ? 'provided' : projectVersion ? 'detected' : 'unknown';
|
|
272
|
+
if (!installed) {
|
|
273
|
+
return text({ reference: refVersion, installed: null, source, advice: 'The project version is unknown. Run `npm ls @sakaniui/react`, then call this tool again with installedVersion.' });
|
|
274
|
+
}
|
|
275
|
+
const missing = manifest.components.filter((c) => c.since && cmp(c.since, installed) > 0).map((c) => ({ name: c.name, since: c.since }));
|
|
276
|
+
const newer = releases().filter((r) => cmp(r.version, installed) > 0).map((r) => r.version);
|
|
277
|
+
const relation = cmp(installed, refVersion) === 0 ? 'same' : cmp(installed, refVersion) < 0 ? 'project is older' : 'project is newer than this reference';
|
|
278
|
+
const advice = relation === 'same' ? 'The reference matches the installed version.'
|
|
279
|
+
: relation === 'project is older' ? `Do not suggest the components listed under missingInInstalled, and check get_changelog (since ${installed}) before relying on newer props. Or upgrade: npm install @sakaniui/react@latest.`
|
|
280
|
+
: 'The project has a newer release than this reference. Run the server from the project root (or pass --project) so it answers from the installed copy.';
|
|
281
|
+
return text({ reference: refVersion, installed, source, relation, missingInInstalled: missing, releasesAfterInstalled: newer, advice });
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
get_changelog: {
|
|
285
|
+
description: 'Release notes that match a version. With `version`: that release. With `since`: every release after it, up to the version this reference describes. With neither: the releases between the project\'s installed version and this reference (or the latest release when they match or the project version is unknown).',
|
|
286
|
+
inputSchema: { type: 'object', properties: { version: { type: 'string' }, since: { type: 'string' } } },
|
|
287
|
+
run(args) {
|
|
288
|
+
const ver = verArg(args, 'version'); const since = verArg(args, 'since');
|
|
289
|
+
const e = firstError(ver, since); if (e) return fail(e.err);
|
|
290
|
+
const all = releases();
|
|
291
|
+
if (!all.length) return fail('No release notes are bundled with this install.');
|
|
292
|
+
let pick;
|
|
293
|
+
if (ver.value) {
|
|
294
|
+
pick = all.filter((r) => cmp(r.version, ver.value) === 0 && r.version.startsWith(ver.value.match(SEMVER)[0]));
|
|
295
|
+
if (!pick.length) return fail(`No release notes for ${ver.value}. Releases: ${all.map((r) => r.version).join(', ')}`);
|
|
296
|
+
} else {
|
|
297
|
+
const from = since.value ?? (projectVersion && cmp(projectVersion, refVersion) < 0 ? projectVersion : null);
|
|
298
|
+
pick = from ? all.filter((r) => cmp(r.version, from) > 0 && cmp(r.version, refVersion) <= 0) : all.slice(0, 1);
|
|
299
|
+
if (!pick.length) return text(`No releases after ${from}. This reference describes ${refVersion}.`);
|
|
300
|
+
}
|
|
301
|
+
return text(pick.map((r) => `## ${r.version}\n\n${r.body}`).join('\n\n'));
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
get_guide: {
|
|
305
|
+
description: 'The AGENTS.md guide (principles, token rules, when nothing fits, glass, icons, per-component "why / when not to use", checklist). Pass a section number or heading text to get just that section; omit for the section list.',
|
|
306
|
+
inputSchema: { type: 'object', properties: { section: { type: 'string' } } },
|
|
307
|
+
run(args) {
|
|
308
|
+
const s0 = strArg(args, 'section'); if (s0.err) return fail(s0.err);
|
|
309
|
+
if (!guide) return fail('AGENTS.md is not available in this install.');
|
|
310
|
+
const parts = guide.split(/^(?=## )/m).filter((p) => p.startsWith('## '));
|
|
311
|
+
const heads = parts.map((p) => p.split('\n')[0].replace(/^## /, ''));
|
|
312
|
+
if (!s0.value) return text({ version: refVersion, sections: heads, hint: 'Call again with a number (e.g. "5") or heading text.' });
|
|
313
|
+
const s = s0.value.toLowerCase().trim();
|
|
314
|
+
const hit = parts.filter((p) => { const h = p.split('\n')[0].replace(/^## /, '').toLowerCase(); return h === s || h.startsWith(s + '.') || h.startsWith(s + ' ') || h.includes(s); });
|
|
315
|
+
return hit.length ? text(hit.join('\n')) : fail(`No section "${s0.value}". Sections: ${heads.join(' | ')}`);
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
/* ---------- JSON-RPC over stdio ---------- */
|
|
321
|
+
const send = (msg) => process.stdout.write(JSON.stringify({ jsonrpc: '2.0', ...msg }) + '\n');
|
|
322
|
+
|
|
323
|
+
function handle(msg) {
|
|
324
|
+
if (!msg || typeof msg !== 'object' || Array.isArray(msg)) return send({ id: null, error: { code: -32600, message: 'Invalid request' } });
|
|
325
|
+
const { id, method, params } = msg;
|
|
326
|
+
const isRequest = id !== undefined && id !== null;
|
|
327
|
+
try {
|
|
328
|
+
switch (method) {
|
|
329
|
+
case 'initialize': {
|
|
330
|
+
const note = versionNote();
|
|
331
|
+
return send({ id, result: {
|
|
332
|
+
protocolVersion: params?.protocolVersion || '2025-06-18',
|
|
333
|
+
capabilities: { tools: {} },
|
|
334
|
+
serverInfo: { name: 'sakaniui', version: refVersion },
|
|
335
|
+
instructions: `SakaniUI ${refVersion}${projectVersion ? ` (project has ${projectVersion})` : ''}. Read-only reference. Look components up with search_components / get_component before writing JSX, validate with check_usage, use only tokens from get_tokens, and read get_guide when nothing fits.${note ? ` ${note}` : ''}`,
|
|
336
|
+
} });
|
|
337
|
+
}
|
|
338
|
+
case 'ping':
|
|
339
|
+
return send({ id, result: {} });
|
|
340
|
+
case 'tools/list':
|
|
341
|
+
return send({ id, result: { tools: Object.entries(tools).map(([name, t]) => ({ name, description: t.description, inputSchema: t.inputSchema, annotations: READ_ONLY })) } });
|
|
342
|
+
case 'tools/call': {
|
|
343
|
+
const t = typeof params?.name === 'string' && Object.hasOwn(tools, params.name) ? tools[params.name] : null;
|
|
344
|
+
if (!t) return send({ id, error: { code: -32602, message: `Unknown tool: ${String(params?.name).slice(0, 60)}` } });
|
|
345
|
+
const args = params.arguments;
|
|
346
|
+
if (args !== undefined && args !== null && (typeof args !== 'object' || Array.isArray(args))) return send({ id, result: fail('Tool arguments must be an object.') });
|
|
347
|
+
return send({ id, result: t.run(args || {}) });
|
|
348
|
+
}
|
|
349
|
+
default:
|
|
350
|
+
if (isRequest) send({ id, error: { code: -32601, message: `Method not found: ${String(method).slice(0, 60)}` } });
|
|
351
|
+
}
|
|
352
|
+
} catch (e) {
|
|
353
|
+
if (isRequest) send({ id, error: { code: -32603, message: String(e?.message || e) } });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
readline.createInterface({ input: process.stdin }).on('line', (line) => {
|
|
358
|
+
if (!line.trim()) return;
|
|
359
|
+
let msg;
|
|
360
|
+
try { msg = JSON.parse(line); } catch { return send({ id: null, error: { code: -32700, message: 'Parse error' } }); }
|
|
361
|
+
handle(msg);
|
|
362
|
+
});
|
|
@@ -3,7 +3,7 @@ import { IconComponent } from '../../icons/types';
|
|
|
3
3
|
export type IconButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive';
|
|
4
4
|
export type IconButtonSize = 'sm' | 'md' | 'lg';
|
|
5
5
|
export interface IconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
6
|
-
/** Lucide
|
|
6
|
+
/** Icon component (Tabler or Lucide), passed as the component itself, e.g. icon={IconSettings}. */
|
|
7
7
|
icon: IconComponent;
|
|
8
8
|
variant?: IconButtonVariant;
|
|
9
9
|
size?: IconButtonSize;
|