aura-components-mcp 1.6.3 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -1,1374 +1,1958 @@
1
- #!/usr/bin/env node
2
- // aura-components-mcp v1.6.3 — bundled (zero deps). Built by swastiksingh-dev.
3
-
4
- // ---- config.mjs ----
5
- // Module: config — one small interface (loadConfig) over all env parsing.
6
- // Callers learn one function; env names, defaults, and clamping live here.
7
-
8
- const num = (raw, fallback, { min = 1, max = 100 } = {}) => {
9
- const n = Number.parseInt(String(raw ?? ""), 10);
10
- if (!Number.isFinite(n)) return fallback;
11
- return Math.min(max, Math.max(min, n));
12
- };
13
-
14
- const bool = (raw, fallback) => {
15
- if (raw === undefined) return fallback;
16
- return /^(1|true|yes|on)$/i.test(String(raw));
17
- };
18
-
19
- function loadConfig(env = process.env) {
20
- const defaultKey =
21
- "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImhvaXJxcmtkZ2JtdnB3dXR3dXdqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDM2Nzc2NTAsImV4cCI6MjA1OTI1MzY1MH0._UsCSHsTELn7m54tOhX3ySm67WEhcyHAPbuxEQZsl3c";
22
- return {
23
- supabaseUrl: env.AURA_SUPABASE_URL ?? "https://hoirqrkdgbmvpwutwuwj.supabase.co",
24
- anonKey: env.AURA_SUPABASE_ANON_KEY ?? defaultKey,
25
- timeoutMs: num(env.AURA_TIMEOUT_MS, 12_000, { min: 1_000, max: 60_000 }),
26
- retries: num(env.AURA_RETRIES, 2, { min: 0, max: 5 }),
27
- cacheTtlMs: num(env.AURA_CACHE_TTL_MS, 60_000, { min: 0, max: 3_600_000 }),
28
- defaultLimit: num(env.AURA_DEFAULT_LIMIT, 10, { min: 1, max: 50 }),
29
- maxLimit: num(env.AURA_MAX_LIMIT, 25, { min: 1, max: 50 }),
30
- codeChars: num(env.AURA_CODE_CHARS, 12_000, { min: 1_000, max: 60_000 }),
31
- contentChars: num(env.AURA_CONTENT_CHARS, 12_000, { min: 1_000, max: 60_000 }),
32
- freeOnlyDefault: bool(env.AURA_FREE_ONLY_DEFAULT, true),
33
- userAgent: env.AURA_USER_AGENT ?? "aura-components-mcp/1.6.3",
34
- };
35
- }
36
-
37
- // ---- http.mjs ----
38
- // Module: http — one small interface (createFetcher) hiding timeout,
39
- // retry-with-backoff, error mapping, and the injectable fetch seam.
40
- // Tests inject a stub fetch; production passes globalThis.fetch.
41
-
42
- function createFetcher({ fetchImpl, timeoutMs, retries, userAgent }) {
43
- if (typeof fetchImpl !== "function") throw new Error("fetchImpl must be a function");
44
-
45
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
46
-
47
- async function once(url, init) {
48
- const ctrl = new AbortController();
49
- const t = setTimeout(() => ctrl.abort(new Error("timeout after " + timeoutMs + "ms")), timeoutMs);
50
- try {
51
- return await fetchImpl(url, {
52
- ...init,
53
- signal: ctrl.signal,
54
- headers: { "user-agent": userAgent, ...(init?.headers ?? {}) },
55
- });
56
- } finally {
57
- clearTimeout(t);
58
- }
59
- }
60
-
61
- // GET JSON over PostgREST. Returns { rows, total } (total from content-range).
62
- async function getJson(url, headers) {
63
- let lastErr;
64
- for (let attempt = 0; attempt <= retries; attempt++) {
65
- try {
66
- const res = await once(url, { headers });
67
- if (res.status === 429 || (res.status >= 500 && res.status < 600)) {
68
- lastErr = new Error("transient upstream status " + res.status);
69
- } else if (!res.ok) {
70
- const body = await res.text().catch(() => "");
71
- throw new Error("upstream " + res.status + ": " + body.slice(0, 300));
72
- } else {
73
- const range = res.headers?.get?.("content-range") ?? null;
74
- const total = range && range.includes("/") ? Number(range.split("/").pop()) : null;
75
- const rows = await res.json();
76
- return { rows: Array.isArray(rows) ? rows : [rows], total: Number.isFinite(total) ? total : null };
77
- }
78
- } catch (err) {
79
- lastErr = err;
80
- if (err && err.name === "AbortError") break; // timeout: retrying rarely helps
81
- }
82
- if (attempt < retries) await sleep(120 * 2 ** attempt + Math.floor(Math.random() * 60));
83
- }
84
- throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
85
- }
86
-
87
- return { getJson };
88
- }
89
-
90
- // ---- catalog.mjs ----
91
- // Module: catalog — the deep module. Small interface (searchCatalog,
92
- // getItem, getStatus) over query building, caching, truncation, author
93
- // enrichment, and page-URL derivation. Adapters: PostgREST via fetcher.
94
-
95
- const SITE = "https://www.aura.build";
96
-
97
- const CATALOG_SORTS = {
98
- components: { popular: "views.desc", recent: "created_at.desc", trending: "views.desc", updated: "updated_at.desc" },
99
- skills: { popular: "views.desc", recent: "created_at.desc", trending: "views.desc", updated: "updated_at.desc" },
100
- assets: { popular: "views.desc", recent: "created_at.desc", trending: "views.desc", updated: "updated_at.desc" },
101
- design_systems: { popular: "views.desc", recent: "created_at.desc", trending: "views.desc", updated: "updated_at.desc" },
102
- };
103
-
104
- const LIST_COLS = {
105
- // PostgREST can't substring server-side: lists fetch code but shapeRow truncates to
106
- // a 600-char probe for facets BEFORE the truncation marker (see shapeRow). ~0.7KB/row.
107
- components: "id,title,description,tags,code,premium,views,forks,slug,background,created_by,created_at,updated_at",
108
- skills: "id,title,description,source_url,views,forks,featured,created_by,created_at,updated_at",
109
- assets: "id,title,description,keywords,media_type,premium,views,forks,image_800w,video_url,video_poster_url,created_by,created_at",
110
- design_systems: "id,slug,title,description,views,forks,featured,created_by,created_at,updated_at",
111
- };
112
-
113
- const DETAIL_COLS = {
114
- components: "id,title,description,tags,code,premium,views,forks,slug,image_url,background,credit_name,credit_url,created_by,created_at,updated_at",
115
- skills: "id,title,description,content,source_url,views,forks,featured,created_by,created_at,updated_at",
116
- assets: "id,title,description,keywords,resolution,colors,media_type,premium,views,forks,image_320w,image_800w,image_1600w,image_3840w,image_original,image_url,video_url,video_poster_url,video_duration,slug,created_by,created_at,updated_at",
117
- design_systems: "id,slug,title,description,content,preview_html,thumbnail_url,source_name,views,forks,featured,created_by,created_at,updated_at",
118
- };
119
-
120
- const esc = (s) => String(s).replaceAll('"', '""');
121
- const ilike = (v) => "*" + String(v).replaceAll("*", "").replaceAll(",", " ").trim() + "*";
122
-
123
- // Split a goal sentence into significant tokens for OR fallback (stop-word filtered).
124
- // Theme words (dark/light) are NOT search tokens — they become rank/filter signals
125
- // (see detectThemeHint + scoreByTheme), so "dark cinematic" can't match light rows.
126
- const STOP = new Set("a,an,the,for,with,and,or,of,to,in,on,my,new,free,also,that,this,from,into,plus,vs,top,best,up".split(","));
127
- const THEME_WORDS = new Set(["dark", "light", "midnight", "noir", "black", "white", "bright", "airy", "minimal"]);
128
- function queryTokens(q, max = 4) {
129
- return String(q ?? "").toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).map((t) => t.trim()).filter((t) => t.length > 2 && !STOP.has(t) && !THEME_WORDS.has(t)).slice(0, max);
130
- }
131
- // Theme hint from the raw query: explicit signal, separate from match tokens.
132
- function detectThemeHint(q) {
133
- const t = String(q ?? "").toLowerCase();
134
- const dark = /\bdark\b|\bmidnight\b|\bnoir\b|\bcinematic\b/.test(t);
135
- const light = /\blight\b|\bbright\b|\bairy\b/.test(t);
136
- if (dark && !light) return "dark";
137
- if (light && !dark) return "light";
138
- return null;
139
- }
140
- // Rank rows: +2 per matched token in title, +1 in description/tags, theme match first.
141
- // Returns { rows, scored } where scored explains the top pick for fallback_score.
142
- function scoreRows(kind, rows, toks, themeHint) {
143
- const scored = rows.map((r) => {
144
- const title = String(r.title || "").toLowerCase();
145
- const desc = String(r.description || "").toLowerCase();
146
- const tags = ((r.tags || []).map((x) => String(x).toLowerCase()).join(" "));
147
- let s = 0;
148
- for (const t of toks) { if (title.includes(t)) s += 2; else if (desc.includes(t) || tags.includes(t)) s += 1; }
149
- const th = r.facets && r.facets.theme;
150
- const themeOk = !themeHint || th === themeHint || th === "mixed" || th === "unknown";
151
- if (themeHint && th === themeHint) s += 3;
152
- return { r, s, themeOk };
153
- });
154
- scored.sort((a, b) => ((b.themeOk ? 1 : 0) - (a.themeOk ? 1 : 0)) || (b.s - a.s) || ((b.r.views || 0) - (a.r.views || 0)));
155
- return scored;
156
- }
157
-
158
- // Kind-aware token overlap for the zero-overlap refusal gate (pure, no fetch).
159
- // Components/assets: title-only (strict visual surfaces; tags/descriptions don't count).
160
- // Skills/design_systems: wider net — title + description + tags + any snippet present.
161
- function matchOverlap(kind, toks, item) {
162
- const list = Array.isArray(toks) ? toks : [];
163
- if (!list.length || !item) return 0;
164
- if (kind === "components" || kind === "assets") {
165
- const title = String(item.title || "").toLowerCase();
166
- let n = 0;
167
- for (const t of list) {
168
- const tok = String(t ?? "").toLowerCase();
169
- if (tok && title.includes(tok)) n++;
170
- }
171
- return n;
172
- }
173
- const parts = [String(item.title || ""), String(item.description || "")];
174
- if (Array.isArray(item.tags)) parts.push(item.tags.map((x) => String(x)).join(" "));
175
- for (const k of ["content_snippet", "content_excerpt", "preview_snippet", "tokens_snippet"]) {
176
- if (typeof item[k] === "string" && item[k]) parts.push(item[k]);
177
- }
178
- const hay = parts.join("\n").toLowerCase();
179
- let n = 0;
180
- for (const t of list) {
181
- const tok = String(t ?? "").toLowerCase();
182
- if (tok && hay.includes(tok)) n++;
183
- }
184
- return n;
185
- }
186
-
187
- function windowAgoIso(days) {
188
- const d = new Date();
189
- d.setDate(d.getDate() - days);
190
- return d.toISOString();
191
- }
192
- function weekAgoIso() { return windowAgoIso(7); }
193
- function trendingIso(days = 90) { return windowAgoIso(days); } // 7d seed is empty; callers pass window_days (default 30 for components now)
194
-
195
- function buildSearchParams(kind, q = {}) {
196
- const p = new URLSearchParams();
197
- p.set("select", LIST_COLS[kind]);
198
- const limit = q.limit;
199
- const offset = q.offset ?? 0;
200
- p.set("limit", String(limit));
201
- p.set("offset", String(offset));
202
- const sort = CATALOG_SORTS[kind][q.sort] ?? CATALOG_SORTS[kind].popular;
203
- p.set("order", sort);
204
- const filters = [];
205
- if (kind === "components" || kind === "assets") {
206
- filters.push("private=eq.false");
207
- if (q.freeOnly) filters.push("premium=eq.false");
208
- } else if (kind === "skills" || kind === "design_systems") {
209
- filters.push("private=eq.false");
210
- }
211
- if (q.tag && kind === "components") filters.push("tags=cs.{" + q.tag.toLowerCase() + "}");
212
- if (q.mediaType && kind === "assets") filters.push("media_type=eq." + q.mediaType);
213
- const ors = [];
214
- if (q.query && q.query.trim()) {
215
- const v = ilike(q.query);
216
- if (kind === "components") ors.push("title.ilike." + v + ",description.ilike." + v);
217
- else if (kind === "skills") ors.push("title.ilike." + v + ",description.ilike." + v);
218
- else if (kind === "assets") ors.push("title.ilike." + v + ",description.ilike." + v);
219
- else ors.push("title.ilike." + v + ",description.ilike." + v + ",content.ilike." + v + ",preview_html.ilike." + v);
220
- }
221
- if (q.sort === "trending") filters.push("created_at=gte." + trendingIso(q.window_days ?? 90));
222
- return { params: p, filters, ors };
223
- }
224
-
225
- function buildUrl(base, table, params, filters, ors) {
226
- let qs = params.toString();
227
- for (const f of filters) qs += "&" + f;
228
- if (ors.length) qs += "&or=(" + ors.join(",") + ")";
229
- return base + "/rest/v1/" + table + "?" + qs;
230
- }
231
-
232
- // Truncation with cursors: agents see full_length + chunk info instead of a silent
233
- // marker, and can fetch the rest via getItem with { chunk } (see getItem below).
234
- const trunc = (s, n) => (s && s.length > n ? s.slice(0, n) + "…[truncated]" : s);
235
- function chunkText(s, n, chunk = 0) {
236
- const full = String(s || "");
237
- const total = Math.max(1, Math.ceil(full.length / n));
238
- const i = Math.max(0, Math.min(chunk, total - 1));
239
- return { text: full.slice(i * n, (i + 1) * n) + (i < total - 1 ? "…[truncated]" : ""), chunk: i, chunks: total, full_length: full.length, truncated: total > 1 };
240
- }
241
-
242
- // Slim lists: drop heavy blobs (full code/content/preview_html) from list rows,
243
- // keep excerpts + chunk pointers + full_fetch so agents fetch detail on purpose.
244
- // getItem/bundleItems stay full — detail is their job. Default on (q.slim ?? true).
245
- function slimRows(kind, items) {
246
- return (items || []).map((it) => {
247
- const o = { ...it };
248
- if (kind === "components" && typeof o.code === "string") delete o.code; // keep code_excerpt + code_info + full_fetch
249
- if (kind === "skills" && typeof o.content === "string") { o.content_excerpt = String(o.content).slice(0, 800); delete o.content; }
250
- if (kind === "design_systems") {
251
- if (typeof o.content === "string") { o.content_excerpt = String(o.content).slice(0, 800); delete o.content; }
252
- if (typeof o.preview_html === "string") { o.preview_excerpt = String(o.preview_html).slice(0, 800); delete o.preview_html; }
253
- }
254
- return o;
255
- });
256
- }
257
-
258
- function pageUrl(kind, row) {
259
- if (kind === "components") return SITE + "/component/" + row.slug;
260
- if (kind === "skills") return SITE + "/skills/" + row.id;
261
- // ISS9: asset pages are query-routed (no /assets/ID route exists). Keep the q= form
262
- // but ALSO expose a stable key so agents can cite: id + slug + title together.
263
- if (kind === "assets") return SITE + "/assets?q=" + encodeURIComponent(row.title ?? "");
264
- return SITE + "/design-systems/" + (row.slug ?? row.id);
265
- }
266
-
267
- function assetKey(row) {
268
- return { id: row.id, slug: row.slug ?? null, title: row.title ?? null };
269
- }
270
-
271
- function shapeRow(kind, row, cfg, withFacets) {
272
- const base = { ...row };
273
- // Facets probe the FULL available code (pre-truncation) so search facets equal
274
- // detail facets and install-time needs: a short prefix missed late markers
275
- // like data-lucide (id 1572 drift). Weight uses the true length (ISS1/2/3).
276
- if (kind === "components" && typeof base.code === "string") {
277
- base.code_chars = base.code.length;
278
- if (withFacets) { try { base.facets = withFacets(kind, base, { fullLength: base.code.length }); } catch { /* never break rows */ } }
279
- // Gap 3: code_excerpt (first 1200 chars, readable) + full_fetch flag. Agents skim
280
- // the excerpt, then get chunk N — no more blind 18k-70k dumps or silent cuts.
281
- base.code_excerpt = String(base.code).slice(0, 1200);
282
- base.full_fetch = { tool: "aura_get_component", id: base.id ?? base.slug, chunks: Math.max(1, Math.ceil(base.code.length / cfg.codeChars)) };
283
- const ch = chunkText(base.code, cfg.codeChars, 0);
284
- base.code = ch.text;
285
- base.code_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: 0, truncated: ch.truncated };
286
- if (base.facets) base.facets.deps_summary = base.facets.needsTailwind || base.facets.needsIcons ? [base.facets.needsTailwind && "tailwindcss", base.facets.needsIcons && "iconify-icon"].filter(Boolean) : [];
287
- }
288
- if (kind === "skills" && typeof base.content === "string") {
289
- const ch = chunkText(base.content, cfg.contentChars, 0);
290
- base.content = ch.text;
291
- base.content_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: 0, truncated: ch.truncated };
292
- }
293
- if (kind === "design_systems") {
294
- if (typeof base.content === "string") {
295
- const ch = chunkText(base.content, cfg.contentChars, 0);
296
- base.content = ch.text;
297
- base.content_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: 0, truncated: ch.truncated };
298
- }
299
- if (typeof base.preview_html === "string") {
300
- const ch = chunkText(base.preview_html, cfg.codeChars, 0);
301
- base.preview_html = ch.text;
302
- base.preview_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: 0, truncated: ch.truncated };
303
- }
304
- }
305
- base.page_url = pageUrl(kind, row);
306
- if (kind === "assets") base.asset_key = assetKey(row);
307
- if (withFacets && !base.facets) { try { base.facets = withFacets(kind, base); } catch { /* facets never break rows */ } }
308
- // ISS8: force absolute Supabase URLs — relative variant paths break fetch.
309
- if (kind === "assets") {
310
- const abs = (u) => (!u ? u : (/^https?:\/\//i.test(u) ? u : ("https://hoirqrkdgbmvpwutwuwj-all.supabase.co/storage/v1/object/public/assets/" + String(u).replace(/^\/+/, ""))));
311
- for (const k of ["image_320w", "image_800w", "image_1600w", "image_3840w", "image_original", "image_url", "video_url", "video_poster_url"]) if (typeof base[k] === "string") base[k] = abs(base[k]);
312
- if (base.facets) {
313
- if (base.facets.download) base.facets.download = abs(base.facets.download);
314
- if (base.facets.preview) base.facets.preview = abs(base.facets.preview);
315
- }
316
- }
317
- return base;
318
- }
319
-
320
- function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = null }) {
321
- const base = config.supabaseUrl.replace(/\/$/, "");
322
- const headers = { apikey: config.anonKey, Authorization: "Bearer " + config.anonKey, Prefer: "count=exact" };
323
- const cache = new Map();
324
- const MAX_KEYS = 300;
325
- const cacheGet = (k) => {
326
- if (!config.cacheTtlMs) return null;
327
- const e = cache.get(k);
328
- if (!e) return null;
329
- if (now() - e.t > config.cacheTtlMs) { cache.delete(k); return null; }
330
- cache.delete(k); cache.set(k, e); // LRU refresh
331
- return e.v;
332
- };
333
- const cacheSet = (k, v) => {
334
- if (!config.cacheTtlMs) return;
335
- if (cache.has(k)) cache.delete(k);
336
- cache.set(k, { t: now(), v });
337
- while (cache.size > MAX_KEYS) cache.delete(cache.keys().next().value);
338
- };
339
- const authorCache = new Map(); // created_by -> profile (shared across searches)
340
- const inflight = new Map(); // cacheKey -> Promise (coalesce concurrent identical calls)
341
-
342
- const clampLimit = (l) => Math.min(config.maxLimit, Math.max(1, l ?? config.defaultLimit));
343
-
344
- async function enrichAuthors(rows) {
345
- const ids = [...new Set(rows.map((r) => r.created_by).filter(Boolean).filter((id) => !authorCache.has(id)))];
346
- if (ids.length) {
347
- try {
348
- const url = base + "/rest/v1/public_author_profiles?select=id,full_name,avatar_url,slug&id=in.(" + ids.map(esc).join(",") + ")";
349
- const { rows: authors } = await fetcher.getJson(url, headers);
350
- for (const a of authors) { authorCache.set(a.id, a); if (authorCache.size > 500) break; }
351
- } catch { /* authors stay null, rows still return */ }
352
- }
353
- return rows.map((r) => ({ ...r, author: (r.created_by && authorCache.get(r.created_by)) || null }));
354
- }
355
-
356
- async function searchCatalog(kind, q = {}) {
357
- const limit = clampLimit(q.limit);
358
- const offset = Math.max(0, q.offset ?? 0);
359
- const sort = CATALOG_SORTS[kind][q.sort] ? q.sort : "popular";
360
- const freeOnly = q.freeOnly ?? config.freeOnlyDefault;
361
- // Gap 1: negative_theme (exclude a theme) + min_views (quality floor). Both ride
362
- // the cache key so filtered/unfiltered variants never collide.
363
- const negTheme = q.negative_theme ? String(q.negative_theme).toLowerCase() : "";
364
- const minViews = Math.max(0, q.min_views ?? 0);
365
- const slim = q.slim ?? true;
366
- const exactFlag = q.exact_match === true ? "exact" : "fuzzy";
367
- const key = ["search", kind, q.query ?? "", q.tag ?? "", q.mediaType ?? "", sort, limit, offset, freeOnly, negTheme, minViews, slim ? "slim" : "full", exactFlag].join("|");
368
- const hit = cacheGet(key);
369
- if (hit) return { ...hit, cached: true };
370
- if (inflight.has(key)) return inflight.get(key);
371
- const p = (async () => {
372
- const runOnce = async (qq) => {
373
- const { params, filters, ors } = buildSearchParams(kind, { ...qq, limit, offset, sort, freeOnly });
374
- const url = buildUrl(base, kind, params, filters, ors);
375
- return fetcher.getJson(url, headers);
376
- };
377
- let { rows, total } = await runOnce(q);
378
- let fallback = null;
379
- // AND-phrase queries like "dark cinematic portfolio" match nothing literally:
380
- // retry as OR over significant tokens so every surface degrades instead of emptying.
381
- // Theme words (dark/light/cinematic) are rank signals, not match tokens.
382
- const toks = (q.query && q.query.trim() && !q.tag) ? queryTokens(q.query) : [];
383
- const themeHint = (q.query && q.query.trim()) ? detectThemeHint(q.query) : null;
384
- const effTheme = q.theme || themeHint;
385
- const suggest = (t) => {
386
- const s = [];
387
- if (t.length > 1) s.push(t.slice(0, Math.min(3, t.length)).join(" "));
388
- for (const x of t.slice(0, 3)) s.push(x);
389
- return [...new Set(s)].slice(0, 4);
390
- };
391
- if ((!rows || !rows.length) && toks.length && q.exact_match !== true) {
392
- // Pass 1: OR over tokens, same filters (drops the AND-phrase requirement).
393
- const orParts = [];
394
- for (const t of toks) {
395
- const v = ilike(t);
396
- if (kind === "design_systems") orParts.push("title.ilike." + v + ",description.ilike." + v + ",content.ilike." + v + ",preview_html.ilike." + v);
397
- else orParts.push("title.ilike." + v + ",description.ilike." + v);
398
- }
399
- const b1 = buildSearchParams(kind, { ...q, query: "", limit, offset, sort, freeOnly });
400
- try {
401
- const fb = await fetcher.getJson(buildUrl(base, kind, b1.params, b1.filters, orParts), headers);
402
- if (fb.rows && fb.rows.length) { rows = fb.rows; total = fb.total; fallback = "or-tokens:" + toks.join(","); }
403
- } catch { /* try pass 2 */ }
404
- // Pass 2: still empty (e.g. trending window + rare tokens): drop sort window,
405
- // keep OR tokens, rank by views. Guarantees aggregators return something useful.
406
- if ((!rows || !rows.length)) {
407
- const b2 = buildSearchParams(kind, { ...q, query: "", limit, offset, sort: "popular", freeOnly });
408
- try {
409
- const fb2 = await fetcher.getJson(buildUrl(base, kind, b2.params, b2.filters, orParts), headers);
410
- if (fb2.rows && fb2.rows.length) { rows = fb2.rows; total = fb2.total; fallback = "or-tokens-unwindowed:" + toks.join(","); }
411
- } catch { /* try safety net */ }
412
- }
413
- // Safety net: total miss on a text query still returns usable items.
414
- // Same freeOnly/tag/mediaType filters, popular order, labeled honestly —
415
- // true never-empty (unless upstream itself is down).
416
- if ((!rows || !rows.length)) {
417
- const b3 = buildSearchParams(kind, { ...q, query: "", limit, offset, sort: "popular", freeOnly });
418
- try {
419
- const fb3 = await fetcher.getJson(buildUrl(base, kind, b3.params, b3.filters, []), headers);
420
- if (fb3.rows && fb3.rows.length) { rows = fb3.rows; total = fb3.total; fallback = "popular-safety-net"; }
421
- } catch { /* keep empty: upstream truth */ }
422
- }
423
- }
424
- // ISS7: dedupe skills by source_url (same SKILL.md listed twice). First (most-viewed)
425
- // wins, gets canonical:true; dupes are dropped and counted in deduped.
426
- let deduped = 0;
427
- if (kind === "skills") {
428
- const seen = new Set();
429
- const kept = [];
430
- for (const r of rows) {
431
- const u = (r.source_url || "").trim().toLowerCase();
432
- if (u && seen.has(u)) { deduped++; continue; }
433
- if (u) seen.add(u);
434
- kept.push(r);
435
- }
436
- const byUrl = new Map();
437
- for (const r of kept) {
438
- const u = (r.source_url || "").trim().toLowerCase();
439
- if (u && (!byUrl.has(u) || (r.views || 0) > (byUrl.get(u).views || 0))) byUrl.set(u, r);
440
- }
441
- rows = kept.map((r) => {
442
- const u = (r.source_url || "").trim().toLowerCase();
443
- return (u && byUrl.get(u) === r) ? { ...r, canonical: true } : r;
444
- });
445
- }
446
- // Gap 4: inline snippets — one extra batched fetch fills preview/tokens glimpses
447
- // into search rows (top N only) so agents decide without a second call per row.
448
- // Bounded: top 5 rows, client-side sliced to 800 chars. No extra call when empty.
449
- // Same fetch also resolves facets.has_preview to a real boolean for those rows.
450
- let presence = null;
451
- if ((kind === "design_systems" || kind === "skills") && rows.length) {
452
- const topIds = rows.slice(0, 5).map((r) => r.id);
453
- try {
454
- const idList = topIds.map((id) => encodeURIComponent(String(id))).join(",");
455
- const sel = kind === "design_systems" ? "id,content,preview_html" : "id,content,source_url";
456
- const { rows: full } = await fetcher.getJson(base + "/rest/v1/" + kind + "?select=" + encodeURIComponent(sel) + "&id=in.(" + idList + ")", headers);
457
- const byId = new Map(full.map((r) => [String(r.id), r]));
458
- if (kind === "design_systems") presence = new Map(full.map((r) => [String(r.id), Boolean(r.preview_html)]));
459
- rows = rows.map((r) => {
460
- const f = byId.get(String(r.id));
461
- if (!f) return r;
462
- if (kind === "design_systems") return { ...r, preview_snippet: String(f.preview_html || "").slice(0, 800), tokens_snippet: String(f.content || "").slice(0, 800) };
463
- return { ...r, content_snippet: String(f.content || "").slice(0, 800) };
464
- });
465
- } catch { /* snippets are best-effort; rows still return */ }
466
- }
467
- const enriched = await enrichAuthors(rows);
468
- const out = { kind, total, limit, offset, items: enriched.map((r) => shapeRow(kind, r, config, facetsFn)), cached: false };
469
- if (deduped) out.deduped = deduped;
470
- // Gap 1: min_views quality floor (views-gated surfaces only) + negative_theme
471
- // hard exclusion. Both apply before theme re-rank so counts stay honest.
472
- if (minViews > 0) out.items = out.items.filter((it) => (it.views || 0) >= minViews);
473
- if (negTheme && kind === "components") {
474
- const before = out.items.length;
475
- out.items = out.items.filter((it) => !it.facets || it.facets.theme !== negTheme);
476
- if (out.items.length < before) out.negative_theme_filtered_out = before - out.items.length;
477
- }
478
- // Theme-aware re-rank (components): theme hint from query (dark/light/cinematic)
479
- // or explicit q.theme reorders so matches come first — never silently drops rows,
480
- // and always reports checked/kept counts so the re-rank is auditable, not opaque.
481
- if (kind === "components" && effTheme) {
482
- const scored = scoreRows(kind, out.items, toks, effTheme);
483
- const themeOkCount = scored.filter((s) => s.themeOk).length;
484
- out.theme_hint = effTheme;
485
- out.theme_checked = scored.length;
486
- out.theme_kept = out.items.length;
487
- if (themeOkCount === 0) {
488
- out.theme_warning = "no " + effTheme + "-theme rows in top results; showing unfiltered, check facets.theme per row";
489
- } else {
490
- out.items = scored.map((s) => s.r);
491
- const dep = scored.length - themeOkCount;
492
- if (dep > 0) out.theme_deprioritized = dep;
493
- }
494
- }
495
- // Real has_preview for snippet-enriched rows (top 5); the rest stay unknown-list.
496
- if (presence) {
497
- for (const it of out.items) {
498
- if (presence.has(String(it.id)) && it.facets) it.facets = { ...it.facets, has_preview: presence.get(String(it.id)) };
499
- }
500
- }
501
- // ISS15: transparent fallback — agents see what happened, not a magic string.
502
- // Safety net additionally carries suggested_queries so agents can retry sharp.
503
- const suggestList = () => { const sq = suggest(toks).filter((s) => !/^[a-z0-9]{8,}$/i.test(s.replace(/\s/g, ""))); return sq.length ? sq : ["hero", "pricing", "landing page"]; };
504
- if (fallback) {
505
- out.fallback = fallback; out.isFallback = true;
506
- const scoredTop = out.items[0];
507
- const hits = matchOverlap(kind, toks, scoredTop);
508
- out.fallback_score = "token-overlap:" + hits + "/" + toks.length + "-terms" + (effTheme ? ";theme:" + effTheme : "");
509
- if (fallback === "popular-safety-net") out.suggested_queries = suggestList();
510
- }
511
- else if ((!rows || !rows.length) && toks.length) { out.isFallback = false; out.suggested_queries = suggestList(); }
512
- // Zero-overlap refusal: fallback engaged but top row matches 0/N tokens —
513
- // honest empty instead of irrelevant popular rows. Direct hits never refused.
514
- // Kind-aware: components/assets stay title-only strict; skills/systems use the
515
- // wider net (title+description+tags+snippets) via matchOverlap.
516
- if (fallback != null && toks.length > 0) {
517
- const top = out.items[0];
518
- const hits2 = matchOverlap(kind, toks, top);
519
- if (hits2 === 0) {
520
- const refused = fallback;
521
- out.items = [];
522
- out.total = 0;
523
- out.isFallback = false;
524
- delete out.fallback;
525
- delete out.fallback_score;
526
- out.no_match = true;
527
- out.refused_fallback = refused;
528
- out.suggested_queries = suggestList();
529
- out.note = "Top fallback result matched 0/" + toks.length + " title tokens — empty instead of irrelevant popular rows. Try a suggested query.";
530
- }
531
- }
532
- if (slim) out.items = slimRows(kind, out.items);
533
- cacheSet(key, out);
534
- return out;
535
- })();
536
- inflight.set(key, p);
537
- try { return await p; } finally { inflight.delete(key); }
538
- }
539
-
540
- async function getItem(kind, idOrSlug, opts = {}) {
541
- const chunk = Math.max(0, opts.chunk ?? 0);
542
- const wantFull = !!opts.full;
543
- const key = ["get", kind, String(idOrSlug), "c" + chunk, wantFull ? "full" : "trunc"].join("|");
544
- const hit = cacheGet(key);
545
- if (hit) return { ...hit, cached: true };
546
- const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(String(idOrSlug));
547
- const isNum = /^\d+$/.test(String(idOrSlug));
548
- const sel = DETAIL_COLS[kind];
549
- // design_systems + skills: uuid id vs slug; components: numeric id vs slug
550
- const filt = (kind === "design_systems" || kind === "skills")
551
- ? (isUuid ? "id=eq." + encodeURIComponent(String(idOrSlug)) : "slug=eq." + encodeURIComponent(String(idOrSlug)))
552
- : (kind === "components" && !isNum
553
- ? "slug=eq." + encodeURIComponent(String(idOrSlug))
554
- : "id=eq." + encodeURIComponent(String(idOrSlug)));
555
- const url = base + "/rest/v1/" + kind + "?select=" + encodeURIComponent(sel) + "&" + filt + "&limit=1";
556
- const { rows } = await fetcher.getJson(url, headers);
557
- if (!rows.length) { const e = new Error("not found: " + kind + " " + idOrSlug); e.code = "NOT_FOUND"; throw e; }
558
- const enriched = await enrichAuthors(rows);
559
- const full = enriched[0];
560
- // Chunked fetch: re-slice full blobs per requested chunk so agents can page
561
- // through 18k-70k payloads instead of hitting one silent truncation wall.
562
- const item = shapeRow(kind, full, config, facetsFn);
563
- if (chunk > 0 && !wantFull) {
564
- if (kind === "components" && typeof full.code === "string") {
565
- const ch = chunkText(full.code, config.codeChars, chunk);
566
- item.code = ch.text;
567
- item.code_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: ch.chunk, truncated: ch.truncated };
568
- }
569
- if ((kind === "skills") && typeof full.content === "string") {
570
- const ch = chunkText(full.content, config.contentChars, chunk);
571
- item.content = ch.text;
572
- item.content_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: ch.chunk, truncated: ch.truncated };
573
- }
574
- if (kind === "design_systems") {
575
- if (typeof full.content === "string") {
576
- const ch = chunkText(full.content, config.contentChars, chunk);
577
- item.content = ch.text;
578
- item.content_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: ch.chunk, truncated: ch.truncated };
579
- }
580
- if (typeof full.preview_html === "string") {
581
- const ch = chunkText(full.preview_html, config.codeChars, chunk);
582
- item.preview_html = ch.text;
583
- item.preview_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: ch.chunk, truncated: ch.truncated };
584
- }
585
- }
586
- }
587
- if (wantFull) {
588
- if (kind === "components" && typeof full.code === "string") {
589
- item.code = full.code;
590
- item.code_info = { full_length: full.code.length, chunks: 1, chunk: 0, truncated: false };
591
- }
592
- if (kind === "skills" && typeof full.content === "string") {
593
- item.content = full.content;
594
- item.content_info = { full_length: full.content.length, chunks: 1, chunk: 0, truncated: false };
595
- }
596
- if (kind === "design_systems") {
597
- if (typeof full.content === "string") {
598
- item.content = full.content;
599
- item.content_info = { full_length: full.content.length, chunks: 1, chunk: 0, truncated: false };
600
- }
601
- if (typeof full.preview_html === "string") {
602
- item.preview_html = full.preview_html;
603
- item.preview_info = { full_length: full.preview_html.length, chunks: 1, chunk: 0, truncated: false };
604
- }
605
- }
606
- }
607
- const out = { kind, item, cached: false };
608
- cacheSet(key, out);
609
- return out;
610
- }
611
-
612
- async function getStatus() {
613
- const key = "status";
614
- const hit = cacheGet(key);
615
- if (hit) return { ...hit, cached: true };
616
- const counts = {};
617
- const jobs = [
618
- ["components_free", "components?select=id&private=eq.false&premium=eq.false&limit=1"],
619
- ["components_pro", "components?select=id&private=eq.false&premium=eq.true&limit=1"],
620
- ["skills", "skills?select=id&private=eq.false&limit=1"],
621
- ["assets", "assets?select=id&private=eq.false&limit=1"],
622
- ["design_systems", "design_systems?select=id&private=eq.false&limit=1"],
623
- ];
624
- await Promise.all(jobs.map(async ([k, path]) => {
625
- try { const { total } = await fetcher.getJson(base + "/rest/v1/" + path, headers); counts[k] = total; }
626
- catch { counts[k] = null; }
627
- }));
628
- // Freshness: best-effort MAX(updated_at) per table so agents see staleness.
629
- // Never throws; null on failure; cached with the status entry.
630
- const freshness = { components: null, skills: null, assets: null, design_systems: null };
631
- await Promise.all(Object.keys(freshness).map(async (table) => {
632
- try {
633
- const { rows } = await fetcher.getJson(base + "/rest/v1/" + table + "?select=updated_at&order=updated_at.desc&limit=1", headers);
634
- const v = rows && rows[0] ? rows[0].updated_at : null;
635
- freshness[table] = v ? String(v) : null;
636
- } catch { freshness[table] = null; }
637
- }));
638
- const out = { site: SITE, catalogue: "aura.build public catalogue (PostgREST, anon)", counts, freshness, cached: false };
639
- cacheSet(key, out);
640
- return out;
641
- }
642
-
643
- async function categoryCounts() {
644
- const key = 'categories';
645
- const hit = cacheGet(key);
646
- if (hit) return hit.categories;
647
- const cats = ['hero','section','button','card','background','header','logo','feature','pricing','testimonial','footer','form','heading'];
648
- const out = await Promise.all(cats.map(async (c) => {
649
- try {
650
- const url = base + '/rest/v1/components?select=id&private=eq.false&premium=eq.false&tags=cs.{' + c + '}&limit=1';
651
- const { total } = await fetcher.getJson(url, headers);
652
- return { category: c, free: total };
653
- } catch { return { category: c, free: null }; }
654
- }));
655
- cacheSet(key, { categories: out });
656
- return out;
657
- }
658
-
659
- // Gap 2: editorial picks — human-grade component leads for when views-signal is
660
- // noise (free trending total=2). Top free by forks (remix = real reuse), then views.
661
- async function editorialPicks(limit = 5) {
662
- const lim = Math.max(1, Math.min(limit || 5, 8));
663
- const sel = encodeURIComponent(LIST_COLS.components);
664
- const url = base + "/rest/v1/components?select=" + sel + "&private=eq.false&premium=eq.false&order=forks.desc,views.desc&limit=" + lim;
665
- try {
666
- const { rows } = await fetcher.getJson(url, headers);
667
- const enriched = await enrichAuthors(rows);
668
- return enriched.map((r) => slimRows("components", [shapeRow("components", r, config, facetsFn)])[0]);
669
- } catch { return []; }
670
- }
671
-
672
- // Bulk fetch: N details in one parallel round (backs aura_bundle + aura_scaffold_page).
673
- // Per-id errors are captured, never thrown: { ok:true, item } or { ok:false, id, error }.
674
- async function bundleItems(kind, ids, max = 5, opts = {}) {
675
- const list = [...new Set((ids || []).map(String))].slice(0, Math.max(1, Math.min(max, 8)));
676
- const out = await Promise.all(list.map(async (id) => {
677
- try { const got = await getItem(kind, id, { full: !!opts.full }); return { ok: true, id, item: got.item }; }
678
- catch (e) { return { ok: false, id, error: (e && e.code === "NOT_FOUND") ? "not found" : String((e && e.message) || e).slice(0, 160) }; }
679
- }));
680
- return out;
681
- }
682
-
683
- // Related: same-tag / same-text overlap, views-ranked, excluding self.
684
- // ISS10: freeOnly default true — free flows never get Pro recommendations.
685
- async function relatedItems(kind, idOrSlug, limit = 3, freeOnly = true) {
686
- const got = await getItem(kind, idOrSlug);
687
- const item = got.item;
688
- const toks = queryTokens(item.title + " " + (item.description || "")).slice(0, 3);
689
- const orParts = toks.map((t) => { const v = ilike(t); return "title.ilike." + v + ",description.ilike." + v; });
690
- const tagF = (kind === "components" && item.tags && item.tags.length) ? "&tags=cs.{" + String(item.tags[0]).toLowerCase() + "}" : "";
691
- const lim = Math.max(1, Math.min(limit || 3, 8));
692
- const sel = LIST_COLS[kind];
693
- const idCol = (kind === "components") ? "id" : "id";
694
- const selfId = encodeURIComponent(String(item.id));
695
- const premF = (freeOnly && (kind === "components" || kind === "assets")) ? "&premium=eq.false" : "";
696
- const url = base + "/rest/v1/" + kind + "?select=" + encodeURIComponent(sel) + "&private=eq.false" + premF + tagF + "&" + idCol + "=neq." + selfId + (orParts.length ? "&or=(" + orParts.join(",") + ")" : "") + "&order=views.desc&limit=" + lim;
697
- try {
698
- const { rows } = await fetcher.getJson(url, headers);
699
- const enriched = await enrichAuthors(rows);
700
- return { item: shapeRow(kind, item, config), related: slimRows(kind, enriched.map((r) => shapeRow(kind, r, config))) };
701
- } catch { return { item: shapeRow(kind, item, config), related: [] }; }
702
- }
703
-
704
- return { searchCatalog, getItem, getStatus, categoryCounts, bundleItems, relatedItems, editorialPicks };
705
- }
706
-
707
- // ---- protocol.mjs ----
708
- const SERVER_INFO = { name: "aura-components-mcp", version: "1.6.3" };
709
- // Module: protocol — stdio JSON-RPC framing + MCP handshake + error codes.
710
- // One interface: createSession(send) -> { dispatch(msg) }. No business logic.
711
-
712
- const PROTOCOL_VERSION = "2024-11-05";
713
-
714
- const protocolFn_err = (code, message, data) => ({ code, message, ...(data === undefined ? {} : { data }) });
715
- const Errors = {
716
- parse: (m) => protocolFn_err(-32700, "Parse error" + (m ? ": " + m : "")),
717
- invalidRequest: (m) => protocolFn_err(-32600, "Invalid Request" + (m ? ": " + m : "")),
718
- methodNotFound: (m) => protocolFn_err(-32601, "Method not found: " + m),
719
- invalidParams: (m) => protocolFn_err(-32602, "Invalid params" + (m ? ": " + m : "")),
720
- upstream: (m) => protocolFn_err(-32000, "Upstream error" + (m ? ": " + m : "")),
721
- notFound: (m) => protocolFn_err(-32004, m || "Not found"),
722
- };
723
-
724
- function textResult(obj) {
725
- return { content: [{ type: "text", text: JSON.stringify(obj, null, 2) }] };
726
- }
727
-
728
- function createSession(send, { tools, handlers }) {
729
- let initialized = false;
730
- const ok = (id, result) => send({ jsonrpc: "2.0", id, result });
731
- const fail = (id, e) => send({ jsonrpc: "2.0", id, error: e });
732
-
733
- async function dispatch(msg) {
734
- if (!msg || typeof msg !== "object" || msg.jsonrpc !== "2.0") {
735
- if (msg && "id" in msg && msg.id !== undefined && msg.id !== null) fail(msg.id, Errors.invalidRequest("bad envelope"));
736
- return;
737
- }
738
- const { id, method, params } = msg;
739
- const isCall = id !== undefined && id !== null;
740
- try {
741
- if (method === "initialize") {
742
- initialized = true;
743
- if (!isCall) return;
744
- return ok(id, { protocolVersion: PROTOCOL_VERSION, capabilities: { tools: {} }, serverInfo: SERVER_INFO });
745
- }
746
- if (method === "notifications/initialized") return;
747
- if (method === "ping") { if (isCall) return ok(id, {}); return; }
748
- if (!initialized) { if (isCall) fail(id, Errors.invalidRequest("not initialized")); return; }
749
- if (method === "tools/list") { if (isCall) return ok(id, { tools }); return; }
750
- if (method === "tools/call") {
751
- if (!isCall) return;
752
- const name = params?.name;
753
- const args = params?.arguments ?? {};
754
- const h = handlers[name];
755
- if (!h) return fail(id, Errors.methodNotFound("tool " + name));
756
- try {
757
- const result = await h(args);
758
- return ok(id, result);
759
- } catch (e) {
760
- if (e && e.code === "NOT_FOUND") return fail(id, Errors.notFound(e.message));
761
- if (e && e.code === "BAD_ARGS") return fail(id, Errors.invalidParams(e.message));
762
- return fail(id, Errors.upstream(e?.message ?? String(e)));
763
- }
764
- }
765
- if (isCall) return fail(id, Errors.methodNotFound(method));
766
- } catch (e) {
767
- if (isCall) fail(id, Errors.upstream(e?.message ?? String(e)));
768
- }
769
- }
770
-
771
- return { dispatch };
772
- }
773
-
774
- // ---- guide.mjs ----
775
- // Module: guide — small interface (installGuide, tokenHints, detectNeeds)
776
- // turning raw catalogue rows into paste-ready help: setup steps, file map,
777
- // dependency list, token starter. Derived from row fields, no extra fetches.
778
-
779
- const GUIDE_CDN = {
780
- tailwind: 'https://cdn.tailwindcss.com',
781
- iconify: 'https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js',
782
- };
783
-
784
- // Font allowlist: real families only. Tailwind weight/size utilities (medium, semibold,
785
- // normal, light-as-weight, size-*) are NOT families — old regex leaked them into fonts[].
786
- const KNOWN_FONTS = ['inter', 'geist', 'manrope', 'sora', 'space-grotesk', 'space-mono', 'poppins', 'montserrat', 'playfair-display', 'instrument-serif', 'dm-sans', 'dm-serif-display', 'jetbrains-mono', 'ibm-plex-mono', 'ibm-plex-serif', 'plus-jakarta-sans', 'bricolage-grotesque', 'newsreader', 'fraunces', 'lora', 'merriweather', 'libre-baskerville', 'cormorant-garamond', 'gfs-didot', 'bodoni-moda', 'cardo', 'marcellus', 'gloock', 'aboreto', 'syne', 'urbanist', 'sora', 'anton', 'bebas-neue', 'archivo-black', 'league-gothic', 'fredoka', 'chewy', 'foldit', 'oi', 'honk', 'nabla', 'quicksand', 'nunito', 'work-sans', 'oswald', 'lora', 'roboto', 'open-sans', 'figtree', 'outfit', 'lexend', 'gloria-hallelujah', 'caveat'];
787
- const FONT_RE = /font-(?:family-)?\[?['"]?([a-z][a-z0-9-]*)/g;
788
-
789
- function detectNeeds(code, tags) {
790
- const c = String(code || '').toLowerCase();
791
- const t = new Set((tags || []).map((x) => String(x).toLowerCase()));
792
- const needsTailwind = c.includes('class=') || t.has('tailwind');
793
- // Probes must work on a 2k prefix too: icon svgs usually appear in the first screen of markup.
794
- // svg alone under-matches in lists (no code fetched) — but must not over-claim either:
795
- // 'svg' counts only with an icon-ish neighbor (iconify, <svg, lucide, heroicons, feather).
796
- const needsIcons = c.includes('iconify') || c.includes('<svg') || c.includes('lucide') || c.includes('heroicon') || c.includes('data-lucide');
797
- const needsKeyframes = c.includes('keyframes') || c.includes('animation:');
798
- const fonts = new Set();
799
- for (const m of c.matchAll(FONT_RE)) {
800
- const fam = m[1].replace(/['"\]]/g, '');
801
- if (KNOWN_FONTS.includes(fam)) fonts.add(fam);
802
- }
803
- // font-family: 'X', Y declarations (not utility classes)
804
- for (const m of c.matchAll(/font-family\s*:\s*([^;}]{1,80})/g)) {
805
- for (const part of m[1].split(',')) {
806
- const fam = part.trim().replace(/['"]/g, '').toLowerCase().replace(/\s+/g, '-');
807
- if (KNOWN_FONTS.includes(fam)) fonts.add(fam);
808
- }
809
- }
810
- return { needsTailwind, needsIcons, needsKeyframes, fonts: [...fonts].slice(0, 6) };
811
- };
812
-
813
- // Facets: cheap derived signals so search lists answer "dark? heavy? pro?" without a get.
814
- // theme: dark|light|mixed|unknown from background field + code palette probes.
815
- // weight: code_chars bucket (s/m/l) so agents can prefer light embeds.
816
- function facets(kind, row, opts) {
817
- const f = {};
818
- if (kind === 'components') {
819
- const bg = String(row.background || '').toLowerCase();
820
- const tags = (row.tags || []).map((x) => String(x).toLowerCase());
821
- const code = String(row.code || '').toLowerCase();
822
- const tagDark = tags.includes('dark');
823
- const tagLight = tags.includes('light');
824
- const darkHits = (code.match(/#0{3,6}\b|#1[0-9a-f]{5}\b|bg-black|bg-neutral-9|bg-zinc-9|bg-slate-9|text-white|slate-300/g) || []).length;
825
- const lightHits = (code.match(/bg-white|bg-neutral-50|bg-slate-50|bg-gray-50|text-black|text-neutral-9/g) || []).length;
826
- const bgDark = bg.includes('000') && !bg.includes('fff');
827
- const bgLight = bg.includes('fff') && !bg.includes('000');
828
- f.theme = bgDark || tagDark ? 'dark' : (bgLight || tagLight ? 'light' : (code ? (darkHits > lightHits * 2 ? 'dark' : (lightHits > darkHits * 2 ? 'light' : (darkHits || lightHits ? 'mixed' : 'unknown'))) : (tags.includes('saas') || tags.includes('minimal') ? 'light' : 'unknown')));
829
- const n = (opts && opts.fullLength) || String(row.code || '').length;
830
- f.weight = n > 20000 ? 'l' : (n > 8000 ? 'm' : 's');
831
- f.code_chars = n;
832
- const needs = detectNeeds(row.code || '', row.tags || []);
833
- f.needsTailwind = needs.needsTailwind;
834
- f.needsIcons = needs.needsIcons;
835
- f.fonts = needs.fonts;
836
- f.license = 'all-rights-reserved (Aura Terms §4) — check page_url before commercial reuse';
837
- }
838
- // ISS14 lives here too: license is top-level on every asset row, not buried.
839
- // ISS13: rewrite hotlinked demo images to stable asset references where possible.
840
- // i.pravatar.cc + bare demo URLs 404 in production; flag them in the install plan.
841
- if (kind === 'components' && typeof row.code === 'string') {
842
- const hot = [...row.code.matchAll(/<img[^>]+src=["']([^"']+)["']/gi)].map((m) => m[1]).filter((u) => /pravatar|placehold|dummyimage|unsplash\/photos\/..\/download|example\.com/i.test(u));
843
- if (hot.length) row = { ...row, _hotlinked: [...new Set(hot)].slice(0, 5) };
844
- }
845
- if (kind === 'assets' || row.image_800w || row.image_original || row.video_url) {
846
- // Resolved 2026-09-13 from primary source https://www.aura.build/terms §4-5:
847
- // catalogue content is the exclusive property of DESIGNCODE IO PTE. LTD.;
848
- // no per-asset license column exists, so commercial reuse needs Aura's permission.
849
- f.license = 'all-rights-reserved (Aura Terms §4: DESIGNCODE IO PTE. LTD.) — personal/preview use via page_url; commercial reuse needs Aura permission (support@designcode.io)';
850
- // Gap 5: machine-readable license gate. commercial_ok:false always (no per-asset
851
- // column exists to prove otherwise) — agents filter without parsing prose.
852
- f.commercial_ok = false;
853
- f.terms_url = 'https://www.aura.build/terms';
854
- f.download = row.image_original || row.image_1600w || row.image_800w || row.video_url || null;
855
- f.preview = row.image_800w || row.video_poster_url || null;
856
- }
857
- if (kind === 'design_systems') {
858
- // ISS4: lists never fetch preview_html (24KB/row). has_preview:true is authoritative
859
- // only on detail; on lists report 'unknown-list' so agents fetch instead of trusting false.
860
- f.has_preview = row.preview_html !== undefined ? Boolean(row.preview_html) : 'unknown-list';
861
- }
862
- return f;
863
- }
864
-
865
- function installGuide(kind, row) {
866
- const steps = [];
867
- const files = [];
868
- const deps = [];
869
- const url = row.page_url || '';
870
- if (kind === 'components') {
871
- const needs = detectNeeds(row.code || '', row.tags || []);
872
- if (needs.needsTailwind) {
873
- deps.push({ name: 'tailwindcss', via: 'CDN or project setup', cdn: GUIDE_CDN.tailwind });
874
- steps.push('Add the Tailwind CDN script to head for a quick preview, or paste the markup into a Tailwind project.');
875
- }
876
- if (needs.needsIcons) {
877
- deps.push({ name: 'iconify-icon', via: 'CDN', cdn: GUIDE_CDN.iconify });
878
- steps.push('Add the iconify-icon CDN script so the icon tags render.');
879
- }
880
- if (needs.needsKeyframes) steps.push('Keep the embedded style block with the markup: that is where the keyframes live.');
881
- // ISS11+12: pagination + CSS scoping. Code ships truncated at codeChars; the scope
882
- // prefix below prevents .button/.inner/.point collisions in multi-component scaffolds.
883
- const scope = 'aura-' + String(row.slug || row.id).toLowerCase().replace(/[^a-z0-9]+/g, '');
884
- const fullLen = String(row.code || '').length;
885
- const isTrunc = /…\[truncated\]$/.test(String(row.code || ''));
886
- if (isTrunc) {
887
- files.push({ path: 'components/' + (row.slug || row.id) + '.part1.html', contains: 'markup part 1 of N — fetch aura_get_component again and concatenate code chunks' });
888
- steps.push('Code is paginated (' + fullLen + ' chars shown of more): install part files in order, or re-fetch for the full body.');
889
- } else {
890
- files.push({ path: 'components/' + (row.slug || row.id) + '.html', contains: 'section markup plus its style block' });
891
- }
892
- steps.push('Scope global CSS before pasting: prefix bare selectors (.button, .inner, .point) with .' + scope + ' to avoid collisions in multi-component pages.');
893
- steps.push('Paste the markup where the section belongs and update the CTA link (it ships as #).');
894
- if (url) steps.push('Preview first: ' + url);
895
- if (row._hotlinked && row._hotlinked.length) steps.push('Replace demo images before shipping (' + row._hotlinked.slice(0, 3).join(', ') + '): hotlinked placeholders 404 in production — swap for aura_search_assets results.');
896
- if (needs.fonts.length) files.push({ path: 'styles/fonts.css', contains: 'font families referenced: ' + needs.fonts.join(', ') });
897
- // Chunk concat helper: truncated installs carry the exact re-fetch plan so no
898
- // agent is left guessing how part files map to chunks.
899
- const ci = row.code_info || null;
900
- const concat = isTrunc ? {
901
- tool: 'aura_get_component',
902
- id: row.slug || row.id,
903
- chunks: (ci && ci.chunks) || 'unknown — call once with chunk:0 and read code_info.chunks',
904
- how: 'call aura_get_component with chunk:0..chunks-1 and concatenate code in order; part files above map 1:1 to chunks',
905
- } : null;
906
- const css_scope_how = 'in the pasted <style>, rewrite every bare class selector as .' + scope + ' <selector> (e.g. .button -> .' + scope + ' .button); leave Tailwind utilities, :root vars, and @keyframes names untouched';
907
- return { kind, title: row.title, page_url: url, free: row.premium === false, steps, deps, files, needs, css_scope: scope, css_scope_how, code_truncated: isTrunc, ...(concat ? { concat } : {}) };
908
- }
909
- if (kind === 'skills') {
910
- steps.push('Read the SKILL.md content from aura_get_skill first: it names its own triggers and pitfalls.');
911
- steps.push('Save it as SKILL.md inside your agent skills folder so the agent can load it.');
912
- if (row.source_url) steps.push('Upstream source: ' + row.source_url);
913
- return { kind, title: row.title, page_url: url, steps, deps, files: [{ path: 'skills/' + row.id + '/SKILL.md', contains: 'full skill content' }] };
914
- }
915
- if (kind === 'design_systems') {
916
- // ISS6: derive preview deps from preview_html so agents don't ship static pages.
917
- const ph = String(row.preview_html || '');
918
- const pl = ph.toLowerCase();
919
- const has = (...ss) => ss.some((s) => pl.includes(s));
920
- if (has('cdn.tailwindcss.com', 'tailwind')) deps.push({ name: 'tailwindcss', via: 'CDN (in preview_html)', cdn: GUIDE_CDN.tailwind });
921
- if (has('iconify', '<svg', 'lucide', 'data-lucide')) deps.push({ name: 'iconify-icon', via: 'CDN (in preview_html)', cdn: GUIDE_CDN.iconify });
922
- if (has('gsap')) deps.push({ name: 'gsap', via: 'CDN (in preview_html)', cdn: 'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js' });
923
- if (has('scrolltrigger')) deps.push({ name: 'gsap-ScrollTrigger', via: 'CDN (in preview_html)', cdn: 'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js' });
924
- const fonts = new Set();
925
- for (const m of pl.matchAll(/family=([a-z+]+)/g)) fonts.add(m[1].replace(/\+/g, '-'));
926
- if (fonts.size) deps.push({ name: 'google-fonts:' + [...fonts].slice(0, 4).join(','), via: 'link (in preview_html)' });
927
- steps.push('Copy the DESIGN.md content into your repo as DESIGN.md and treat it as the source of truth.');
928
- steps.push('Apply the color, type, and spacing tokens before copying any component markup.');
929
- if (deps.length) steps.push('Install preview deps first (' + deps.map((d) => d.name).join(', ') + ') — the preview_html needs them.');
930
- return { kind, title: row.title, page_url: url, steps, deps, files: [{ path: 'DESIGN.md', contains: 'tokens and rules' }] };
931
- }
932
- steps.push('Use the image_800w URL for previews and image_original for production.');
933
- return { kind, title: row.title, page_url: url, steps, deps, files };
934
- };
935
-
936
- // ISS5: full token surface — colors + typography + spacing + radius — so scaffolded
937
- // pages don't lose the system. Also detects preview dark/light mode so agents don't
938
- // apply light-cream tokens to a dark preview body.
939
- function previewMode(previewHtml) {
940
- const h = String(previewHtml || '').toLowerCase();
941
- if (/bg-black|bg-neutral-950|bg-zinc-950|bg-slate-950|bg-\[#0/.test(h)) return 'dark';
942
- if (/bg-white|bg-neutral-50|bg-slate-50/.test(h)) return 'light';
943
- return 'unknown';
944
- }
945
-
946
- function tokenHints(content, previewHtml) {
947
- const Q = String.fromCharCode(39);
948
- const lines = String(content || '').split(Q + 'n' === Q + 'n' ? '\n' : '\n');
949
- const get = (key) => {
950
- const line = lines.find((l) => { const t = l.trim().toLowerCase(); return t === key + ':' || t.startsWith(key + ': ') || t.startsWith(key + ' :') || t.startsWith(key + ':\"') || t.startsWith(key + ":'"); });
951
- if (!line) return null;
952
- const t = line.trim();
953
- const ci = t.toLowerCase().indexOf(key);
954
- let v = t.slice(ci + key.length).trim();
955
- if (v.startsWith(':')) v = v.slice(1).trim();
956
- while (v.startsWith(Q) || v.startsWith('"')) v = v.slice(1);
957
- while (v.endsWith(Q) || v.endsWith('"')) v = v.slice(0, -1);
958
- return v.trim() || null;
959
- };
960
- const tokens = {
961
- primary: get('primary'),
962
- secondary: get('secondary'),
963
- accent: get('accent'),
964
- background: get('background'),
965
- surface: get('surface'),
966
- text_primary: get('text-primary') || get('text_primary'),
967
- text_secondary: get('text-secondary') || get('text_secondary'),
968
- border: get('border'),
969
- display_font: get('display_lg_fontfamily') || get('display-font'),
970
- body_font: get('body_md_fontfamily') || get('body-font'),
971
- mono_font: get('label_md_fontfamily') || get('mono-font'),
972
- section_padding: get('section-padding') || get('section_padding'),
973
- card_padding: get('card-padding') || get('card_padding'),
974
- card_radius: get('card') === null ? get('card_radius') : null,
975
- control_radius: get('control'),
976
- pill_radius: get('pill'),
977
- };
978
- const pick = (obj, keys) => { const o = {}; for (const k of keys) if (obj[k]) o[k] = obj[k]; return o; };
979
- const colors = pick(tokens, ['primary', 'secondary', 'accent', 'background', 'surface', 'text_primary', 'text_secondary', 'border']);
980
- const rows = [];
981
- for (const [k, v] of Object.entries(colors)) rows.push(' --aura-' + k.replace(/_/g, '-') + ': ' + v + ';');
982
- if (tokens.body_font) rows.push(' --aura-font-body: ' + tokens.body_font + ';');
983
- if (tokens.display_font) rows.push(' --aura-font-display: ' + tokens.display_font + ';');
984
- if (tokens.mono_font) rows.push(' --aura-font-mono: ' + tokens.mono_font + ';');
985
- if (tokens.section_padding) rows.push(' --aura-section-padding: ' + tokens.section_padding + ';');
986
- if (tokens.card_padding) rows.push(' --aura-card-padding: ' + tokens.card_padding + ';');
987
- if (tokens.card_radius) rows.push(' --aura-radius-card: ' + tokens.card_radius + ';');
988
- if (tokens.control_radius) rows.push(' --aura-radius-control: ' + tokens.control_radius + ';');
989
- if (tokens.pill_radius) rows.push(' --aura-radius-pill: ' + tokens.pill_radius + ';');
990
- const css = rows.length ? ':root{' + '\n' + rows.join('\n') + '\n}' : '';
991
- const mode = previewMode(previewHtml);
992
- const warn = (mode === 'dark' && tokens.background && !/000|0f1115|111827|1a1a1a|09090b/i.test(tokens.background))
993
- ? 'tokens say light background ' + tokens.background + ' but preview_html renders dark — follow the preview body, not the tokens, or ask which mode is wanted'
994
- : ((mode === 'light' && tokens.background && /000000|0f1115/i.test(tokens.background))
995
- ? 'tokens say dark background but preview renders light — follow the preview body'
996
- : null);
997
- return { tokens, css, ...(mode === 'unknown' ? {} : { preview_mode: mode }), mode_warning: warn };
998
- }
999
-
1000
- // Module: prompt — deep module, small interface (buildPrompt) over all
1001
- // need-based prompt rules. Pure, no fetch: same seam for tool + tests + docs.
1002
- // Pass 1 (no answers): draft + questions[<=4] + next_call. Agent MUST ask the
1003
- // user via native question UI before Pass 2. Pass 2 (with answers): final.
1004
- const PROMPT_SOURCES = [
1005
- 'https://www.aura.build/learn/tips-for-prompting',
1006
- 'https://www.aura.build/learn/introduction',
1007
- 'https://www.aura.build/design-systems',
1008
- ];
1009
- const PROMPT_CREDIT = 'Built by swastiksingh-dev — catalogue by Aura (aura.build)';
1010
- const PROMPT_ORDER = ['1 tokens.css', '2 system preview_html', '3 component markup in listed order'];
1011
-
1012
- function promptThemeHint(goal, explicit) {
1013
- if (explicit === 'dark' || explicit === 'light') return explicit;
1014
- const t = String(goal || '').toLowerCase();
1015
- if (/\bdark\b|\bmidnight\b|\bnoir\b|\bcinematic\b/.test(t) && !/\blight\b|\bbright\b|\bairy\b/.test(t)) return 'dark';
1016
- if (/\blight\b|\bbright\b|\bairy\b/.test(t)) return 'light';
1017
- return null;
1018
- }
1019
-
1020
- function draftPromptText(goal, theme) {
1021
- const th = theme ? theme + ' theme' : 'theme to confirm with user';
1022
- return 'Build: ' + goal + ' (' + th + '). Use Tailwind CSS with container mx-auto px-4, hex palette, numeric color scale 50-900, 8px radius, 768px hamburger breakpoint, hover scale 1.05 + shadow. Typography: Inter 40-60 / 28-36 / 14-16, weights 640/560/460. Motion <500ms + prefers-reduced-motion. Tokens first (DESIGN.md), then markup. Confirm theme/sections/system with user before scaffolding.';
1023
- }
1024
-
1025
- function finalPromptText(goal, a) {
1026
- const theme = a.theme || 'confirm with user';
1027
- const sections = (a.sections && a.sections.length ? a.sections.join(', ') : 'hero + 2 sections');
1028
- const system = a.system || 'auto-pick via aura_search_design_systems';
1029
- const assets = a.assets || 'only if needed via aura_search_assets';
1030
- return 'GOAL: ' + goal + '\n'
1031
- + 'THEME: ' + theme + ' (tokens first, follow preview body on mismatch)\n'
1032
- + 'SECTIONS: ' + sections + '\n'
1033
- + 'DESIGN SYSTEM: ' + system + ' — copy DESIGN.md as source of truth, apply color/type/spacing before markup\n'
1034
- + 'ASSETS: ' + assets + '\n'
1035
- + 'RULES: Tailwind container mx-auto px-4; hex palette 50-900; radius 8px; breakpoints 1024+/768-1023/<768, 44px touch targets; hover scale 1.05 + shadow; Inter 40-48 H1 / 16 body; motion <500ms + prefers-reduced-motion; vanilla HTML/Tailwind/JS, no proprietary runtime\n'
1036
- + 'ORDER: 1 tokens.css, 2 system preview_html, 3 component markup\n'
1037
- + 'CALLS: aura_recommend -> aura_get_* finalists -> aura_use_design_system -> aura_scaffold_page';
1038
- }
1039
-
1040
- function buildPrompt(input = {}) {
1041
- const goal = typeof input.goal === 'string' ? input.goal.trim() : '';
1042
- if (!goal) { const e = new Error('goal (string) is required'); e.code = 'BAD_ARGS'; throw e; }
1043
- const themeIn = input.theme === 'dark' || input.theme === 'light' ? input.theme : undefined;
1044
- if (input.theme !== undefined && themeIn === undefined) { const e = new Error('theme must be dark|light'); e.code = 'BAD_ARGS'; throw e; }
1045
- const theme = promptThemeHint(goal, themeIn);
1046
- const system = typeof input.system === 'string' && input.system.trim() ? input.system.trim() : null;
1047
- const surfaces = Array.isArray(input.surfaces) ? input.surfaces.filter((s) => typeof s === 'string').slice(0, 4) : [];
1048
- const answers = input.answers && typeof input.answers === 'object' && !Array.isArray(input.answers) ? input.answers : null;
1049
-
1050
- if (!answers) {
1051
- const questions = [
1052
- { key: 'theme', question: 'Which theme — dark or light?', options: ['dark', 'light'] },
1053
- { key: 'sections', question: 'Which sections (e.g. hero, pricing, testimonial, footer)?', options: [] },
1054
- { key: 'system', question: 'Which DESIGN system, or auto-pick?', options: system ? [system, 'auto-pick'] : ['auto-pick'] },
1055
- { key: 'assets', question: 'Need images/video?', options: ['no', 'images', 'video'] },
1056
- ];
1057
- return {
1058
- phase: 'draft',
1059
- goal,
1060
- theme_hint: theme,
1061
- draft_prompt: draftPromptText(goal, theme),
1062
- questions,
1063
- missing: ['theme', 'sections', 'system', 'assets'],
1064
- next_call: { tool: 'aura_prompt', arguments: { goal, answers: { theme: theme || 'dark', sections: [], system: system || 'auto-pick', assets: 'no' } } },
1065
- plan_order: PROMPT_ORDER,
1066
- sources: PROMPT_SOURCES,
1067
- credit: PROMPT_CREDIT,
1068
- note: 'Ask the user these questions via native question UI, then call aura_prompt again with goal + answers. Never skip confirmation.',
1069
- };
1070
- }
1071
-
1072
- const a = {
1073
- theme: answers.theme === 'dark' || answers.theme === 'light' ? answers.theme : (theme || 'dark'),
1074
- sections: Array.isArray(answers.sections) ? answers.sections.filter((s) => typeof s === 'string').slice(0, 6) : [],
1075
- system: typeof answers.system === 'string' && answers.system.trim() ? answers.system.trim() : (system || 'auto-pick'),
1076
- assets: typeof answers.assets === 'string' ? answers.assets : 'no',
1077
- };
1078
- return {
1079
- phase: 'final',
1080
- goal,
1081
- answers: a,
1082
- final_prompt: finalPromptText(goal, a),
1083
- plan_order: PROMPT_ORDER,
1084
- suggested_calls: [
1085
- { tool: 'aura_recommend', arguments: { goal } },
1086
- { tool: 'aura_search_all', arguments: { query: goal, limit: 3 } },
1087
- { tool: 'aura_use_design_system', arguments: a.system !== 'auto-pick' ? { slug: a.system } : { slug: 'auto-pick: search first' } },
1088
- { tool: 'aura_scaffold_page', arguments: { goal, system: a.system } },
1089
- ],
1090
- page_urls: ['https://www.aura.build/design-systems', 'https://www.aura.build/components'],
1091
- sources: PROMPT_SOURCES,
1092
- surfaces: surfaces.length ? surfaces : ['components', 'design_systems', 'assets', 'skills'],
1093
- credit: PROMPT_CREDIT,
1094
- };
1095
- };
1096
-
1097
- // ---- tools.mjs ----
1098
- // Module: tools — thin adapters. Each tool validates args at the seam,
1099
- // calls one catalog/guide method, wraps textResult. No retries/caching here
1100
- // (except aura_recommend's single bounded auto-retry + editorial fill).
1101
- const TOOL_KINDS = ['components', 'skills', 'assets', 'design_systems'];
1102
- const TOOL_SORTS = ['popular', 'recent', 'trending', 'updated'];
1103
- const TOOL_CATS = ['hero','section','button','card','background','header','logo','feature','pricing','testimonial','footer','form','heading'];
1104
- function toolFn_bad(msg) { const e = new Error(msg); e.code = 'BAD_ARGS'; throw e; }
1105
- function toolFn_str(v, name) { if (v !== undefined && typeof v !== 'string') toolFn_bad(name + ' must be a string'); return v; }
1106
- function toolFn_num(v, name) { if (v !== undefined && typeof v !== 'number') toolFn_bad(name + ' must be a number'); return v; }
1107
- function toolFn_bool(v, name) { if (v !== undefined && typeof v !== 'boolean') toolFn_bad(name + ' must be a boolean'); return v; }
1108
- function toolFn_sort(v) { if (v !== undefined && !TOOL_SORTS.includes(v)) toolFn_bad('sort must be one of ' + TOOL_SORTS.join('|')); return v; }
1109
- function theme(v) { if (v !== undefined && v !== 'dark' && v !== 'light') toolFn_bad('theme must be dark|light'); return v; }
1110
- function idList(v, name) { if (v === undefined) return v; if (!Array.isArray(v) || !v.length || v.length > 8 || v.some((x) => typeof x !== 'string' && typeof x !== 'number')) toolFn_bad(name + ' must be an array of 1-8 ids'); return v; }
1111
- function normRefs(v, name) {
1112
- if (v === undefined) return v;
1113
- let preview = '';
1114
- try { preview = JSON.stringify(v).slice(0, 120); } catch { preview = String(v).slice(0, 120); }
1115
- const ex = name + ' must be an array like ["732","1364"] or [{id:"732"}] — got ' + preview;
1116
- if (!Array.isArray(v) || !v.length || v.length > 8) toolFn_bad(ex);
1117
- const out = [];
1118
- for (const e of v) {
1119
- if (typeof e === 'string' && e) out.push(e);
1120
- else if (typeof e === 'number' && Number.isFinite(e)) out.push(String(e));
1121
- else if (e && typeof e === 'object' && !Array.isArray(e)) {
1122
- if (typeof e.id === 'string' && e.id) out.push(e.id);
1123
- else if (typeof e.id === 'number' && Number.isFinite(e.id)) out.push(String(e.id));
1124
- else if (typeof e.slug === 'string' && e.slug) out.push(e.slug);
1125
- else toolFn_bad(ex);
1126
- } else toolFn_bad(ex);
1127
- }
1128
- if (!out.length) toolFn_bad(ex);
1129
- return out;
1130
- }
1131
- function toolFn_cat(v) { if (v !== undefined && !TOOL_CATS.includes(v)) toolFn_bad('category must be one of ' + TOOL_CATS.join('|')); return v; }
1132
- const TOOL_DEFS = [
1133
- { name: 'aura_status', description: 'Catalogue health plus free counts (components, skills, assets, design systems). Free only, no login. Start here.', inputSchema: { type: 'object', properties: {}, additionalProperties: false } },
1134
- { name: 'aura_search_components', description: 'Search free Aura UI components (2,495). Text over title and description, optional category tag, theme dark|light filter, negative_theme exclusion, min_views floor, sorts. Slim rows by default (excerpt + fetch pointer, no full code; slim:false for full). exact_match:true skips fallbacks (honest empty).', inputSchema: { type: 'object', properties: { query: { type: 'string' }, category: { type: 'string', enum: TOOL_CATS }, theme: { type: 'string', enum: ['dark', 'light'] }, negative_theme: { type: 'string', enum: ['dark', 'light'] }, min_views: { type: 'number' }, sort: { type: 'string', enum: TOOL_SORTS }, limit: { type: 'number' }, offset: { type: 'number' }, slim: { type: 'boolean' }, exact_match: { type: 'boolean' } }, additionalProperties: false } },
1135
- { name: 'aura_get_component', description: 'Full free component detail with HTML/Tailwind source, preview image, page URL. Numeric id or slug. Chunk param pages large code (see code_info). full:true returns full code (ignores chunk).', inputSchema: { type: 'object', properties: { id: {}, slug: { type: 'string' }, chunk: { type: 'number' }, full: { type: 'boolean' } }, additionalProperties: false } },
1136
- { name: 'aura_search_skills', description: 'Search free Aura agent skills (187). Metadata only, slim by default; use aura_get_skill for the full SKILL.md content. exact_match:true skips fallbacks.', inputSchema: { type: 'object', properties: { query: { type: 'string' }, sort: { type: 'string', enum: TOOL_SORTS }, limit: { type: 'number' }, offset: { type: 'number' }, slim: { type: 'boolean' }, exact_match: { type: 'boolean' } }, additionalProperties: false } },
1137
- { name: 'aura_get_skill', description: 'Full free agent-skill content (SKILL.md body) plus source_url and page URL. Skill id. Chunk param pages large bodies (see content_info).', inputSchema: { type: 'object', properties: { id: { type: 'string' }, chunk: { type: 'number' } }, required: ['id'], additionalProperties: false } },
1138
- { name: 'aura_search_assets', description: 'Search free Aura assets (images and video). Keywords, media_type image or video. Slim rows by default. exact_match:true skips fallbacks.', inputSchema: { type: 'object', properties: { query: { type: 'string' }, mediaType: { type: 'string', enum: ['image', 'video'] }, commercial_ok: { type: 'boolean' }, sort: { type: 'string', enum: TOOL_SORTS }, limit: { type: 'number' }, offset: { type: 'number' }, slim: { type: 'boolean' }, exact_match: { type: 'boolean' } }, additionalProperties: false } },
1139
- { name: 'aura_search_design_systems', description: 'Search free Aura DESIGN.md systems (725). Metadata + snippets, slim by default; use aura_get_design_system for content. exact_match:true skips fallbacks.', inputSchema: { type: 'object', properties: { query: { type: 'string' }, sort: { type: 'string', enum: TOOL_SORTS }, limit: { type: 'number' }, offset: { type: 'number' }, slim: { type: 'boolean' }, exact_match: { type: 'boolean' } }, additionalProperties: false } },
1140
- { name: 'aura_get_design_system', description: 'Full free DESIGN.md content plus preview_html, tokens, and page URL. System id or slug. Chunk param pages large preview_html (see preview_info).', inputSchema: { type: 'object', properties: { id: { type: 'string' }, slug: { type: 'string' }, chunk: { type: 'number' } }, additionalProperties: false } },
1141
- { name: 'aura_search_all', description: 'One call across components, skills, assets, and design systems in parallel. Free only. Slim rows by default (excerpts, no blobs). Each surface runs its own fallback pipeline, so mixed outcomes are normal (skills may fall back while components go honestly empty) — read isFallback/no_match per surface, not once globally.', inputSchema: { type: 'object', properties: { query: { type: 'string' }, limit: { type: 'number' }, slim: { type: 'boolean' }, theme: { type: 'string', enum: ['dark', 'light'] }, negative_theme: { type: 'string', enum: ['dark', 'light'] }, min_views: { type: 'number' }, exact_match: { type: 'boolean' } }, additionalProperties: false } },
1142
- { name: 'aura_recommend', description: 'Starter kit for a goal: top 3 free hits per surface with page URLs and rationale. Slim rows + commercial-use note. Empty components auto-retry once via suggested_queries, then editorial picks.', inputSchema: { type: 'object', properties: { goal: { type: 'string' }, theme: { type: 'string', enum: ['dark', 'light'] }, negative_theme: { type: 'string', enum: ['dark', 'light'] }, min_views: { type: 'number' }, slim: { type: 'boolean' } }, required: ['goal'], additionalProperties: false } },
1143
- { name: 'aura_install_component', description: 'Paste-ready setup for a free component: dependency list, setup steps, file map, fonts. Goes beyond the official Aura MCP, which only reads project source.', inputSchema: { type: 'object', properties: { id: {}, slug: { type: 'string' } }, additionalProperties: false } },
1144
- { name: 'aura_install_skill', description: 'Save-and-load plan for a free skill: where to put SKILL.md per client plus upstream source.', inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'], additionalProperties: false } },
1145
- { name: 'aura_use_design_system', description: 'Apply a free DESIGN.md system: token starter CSS plus copy order (tokens first, then markup).', inputSchema: { type: 'object', properties: { id: { type: 'string' }, slug: { type: 'string' } }, additionalProperties: false } },
1146
- { name: 'aura_bundle', description: 'Bulk-fetch 2-8 component details in one call (ids or slugs). Per-item errors never fail the batch.', inputSchema: { type: 'object', properties: { ids: { type: 'array', items: {} }, slugs: { type: 'array', items: { type: 'string' } } }, additionalProperties: false } },
1147
- { name: 'aura_scaffold_page', description: 'One ordered page build: DESIGN.md tokens.css + system preview + component markup in dependency order, combined deps + files[]. Merges install_* + use_* in a single turn. components accepts string|number|{id}|{slug} refs, e.g. ["732","1364"] or [{id:"732"}] or [{slug:"..."}]; full:true returns full bodies.', inputSchema: { type: 'object', properties: { goal: { type: 'string' }, system: { type: 'string' }, components: { type: 'array', items: {} }, full: { type: 'boolean' } }, required: ['goal'], additionalProperties: false } },
1148
- { name: 'aura_related', description: 'More-like-this: 3 related items for a component/skill/design-system by tag + text overlap. Discovery never dead-ends.', inputSchema: { type: 'object', properties: { kind: { type: 'string', enum: ['components', 'skills', 'design_systems'] }, id: {} }, required: ['kind', 'id'], additionalProperties: false } },
1149
- { name: 'aura_bulk_fetch', description: 'Bulk-fetch 2-8 details in one call for components, design_systems, or assets. Alias-friendly name for aura_bundle. Per-item errors never fail the batch.', inputSchema: { type: 'object', properties: { kind: { type: 'string', enum: ['components', 'design_systems', 'assets'] }, ids: { type: 'array', items: {} } }, required: ['kind', 'ids'], additionalProperties: false } },
1150
- { name: 'aura_install_asset', description: 'Legal drop-in plan for an asset: direct download URL, preview URL, license (all-rights-reserved per Aura Terms §4 — check page before commercial use), suggested file path.', inputSchema: { type: 'object', properties: { id: { type: 'number' } }, required: ['id'], additionalProperties: false } },
1151
- { name: 'aura_categories', description: 'The 13 component categories with live free counts. Pick one, then search within it.', inputSchema: { type: 'object', properties: {}, additionalProperties: false } },
1152
- { name: 'aura_prompt', description: 'Need-based design prompt in 2 passes: Pass 1 (goal only) returns draft + 4 questions + next_call; ask the user via native question UI, then Pass 2 (goal + answers) returns copy-paste final_prompt + plan order + suggested calls. Always confirm with user before scaffolding.', inputSchema: { type: 'object', properties: { goal: { type: 'string' }, theme: { type: 'string', enum: ['dark', 'light'] }, surfaces: { type: 'array', items: { type: 'string' } }, system: { type: 'string' }, answers: { type: 'object' } }, required: ['goal'], additionalProperties: false } },
1153
- ];
1154
- function createHandlers(catalog) {
1155
- const free = { freeOnly: true };
1156
- return {
1157
- aura_status: async () => textResult(await catalog.getStatus()),
1158
- aura_search_components: async (a) => { a = a || {}; return textResult(await catalog.searchCatalog('components', {
1159
- query: toolFn_str(a.query, 'query'), tag: toolFn_cat(a.category), freeOnly: true, theme: theme(a.theme),
1160
- negative_theme: a.negative_theme === undefined ? undefined : theme(a.negative_theme),
1161
- min_views: toolFn_num(a.min_views, 'min_views'),
1162
- sort: toolFn_sort(a.sort), limit: toolFn_num(a.limit, 'limit'), offset: toolFn_num(a.offset, 'offset'),
1163
- slim: a.slim === undefined ? undefined : toolFn_bool(a.slim, 'slim'),
1164
- exact_match: a.exact_match === undefined ? undefined : toolFn_bool(a.exact_match, 'exact_match'),
1165
- })); },
1166
- aura_get_component: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
1167
- if (id === undefined || (typeof id !== 'string' && typeof id !== 'number')) toolFn_bad('provide id (number) or slug (string)');
1168
- const full = a.full === undefined ? undefined : toolFn_bool(a.full, 'full');
1169
- if (full === true) return textResult(await catalog.getItem('components', id, { full: true }));
1170
- const chunk = a.chunk === undefined ? 0 : toolFn_num(a.chunk, 'chunk');
1171
- return textResult(await catalog.getItem('components', id, { chunk })); },
1172
- aura_search_skills: async (a) => { a = a || {}; return textResult(await catalog.searchCatalog('skills', {
1173
- query: toolFn_str(a.query, 'query'), sort: toolFn_sort(a.sort), limit: toolFn_num(a.limit, 'limit'), offset: toolFn_num(a.offset, 'offset'),
1174
- slim: a.slim === undefined ? undefined : toolFn_bool(a.slim, 'slim'),
1175
- exact_match: a.exact_match === undefined ? undefined : toolFn_bool(a.exact_match, 'exact_match'),
1176
- })); },
1177
- aura_get_skill: async (a) => { a = a || {}; if (typeof a.id !== 'string' || !a.id) toolFn_bad('id (string) is required');
1178
- const chunk = a.chunk === undefined ? 0 : toolFn_num(a.chunk, 'chunk');
1179
- return textResult(await catalog.getItem('skills', a.id, { chunk })); },
1180
- aura_search_assets: async (a) => { a = a || {}; const co = a.commercial_ok === undefined ? undefined : toolFn_bool(a.commercial_ok, 'commercial_ok');
1181
- const r = await catalog.searchCatalog('assets', { query: toolFn_str(a.query, 'query'), mediaType: a.mediaType === undefined ? undefined : (a.mediaType === 'image' || a.mediaType === 'video' ? a.mediaType : toolFn_bad('mediaType must be image|video')), freeOnly: true, sort: toolFn_sort(a.sort), limit: toolFn_num(a.limit, 'limit'), offset: toolFn_num(a.offset, 'offset'), slim: a.slim === undefined ? undefined : toolFn_bool(a.slim, 'slim'), exact_match: a.exact_match === undefined ? undefined : toolFn_bool(a.exact_match, 'exact_match') });
1182
- // Gap 5: commercial_ok filter (all-rights-reserved per Aura Terms S4).
1183
- // commercial_ok:true returns [] with explanation, never silence.
1184
- if (co === true) return textResult({ ...r, items: [], commercial_ok: false, commercial_note: 'All Aura catalogue assets are all-rights-reserved per Aura Terms S4. Preview/personal use via page_url; commercial reuse needs Aura permission.' });
1185
- return textResult(r); },
1186
- aura_search_design_systems: async (a) => { a = a || {}; return textResult(await catalog.searchCatalog('design_systems', {
1187
- query: toolFn_str(a.query, 'query'), sort: toolFn_sort(a.sort), limit: toolFn_num(a.limit, 'limit'), offset: toolFn_num(a.offset, 'offset'),
1188
- slim: a.slim === undefined ? undefined : toolFn_bool(a.slim, 'slim'),
1189
- exact_match: a.exact_match === undefined ? undefined : toolFn_bool(a.exact_match, 'exact_match'),
1190
- })); },
1191
- aura_get_design_system: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
1192
- if (typeof id !== 'string' || !id) toolFn_bad('provide id or slug (string)');
1193
- const chunk = a.chunk === undefined ? 0 : toolFn_num(a.chunk, 'chunk');
1194
- return textResult(await catalog.getItem('design_systems', id, { chunk })); },
1195
- aura_search_all: async (a) => { a = a || {}; const query = toolFn_str(a.query, 'query'), limit = toolFn_num(a.limit, 'limit') || 5;
1196
- const slim = a.slim === undefined ? undefined : toolFn_bool(a.slim, 'slim');
1197
- const themeV = a.theme === undefined ? undefined : theme(a.theme);
1198
- const negV = a.negative_theme === undefined ? undefined : theme(a.negative_theme);
1199
- const minV = toolFn_num(a.min_views, 'min_views');
1200
- const exactV = a.exact_match === undefined ? undefined : toolFn_bool(a.exact_match, 'exact_match');
1201
- const r = await Promise.all([
1202
- catalog.searchCatalog('components', { query, freeOnly: true, limit, slim, theme: themeV, negative_theme: negV, min_views: minV, exact_match: exactV }),
1203
- catalog.searchCatalog('skills', { query, limit, slim, min_views: minV, exact_match: exactV }),
1204
- catalog.searchCatalog('assets', { query, freeOnly: true, limit, slim, min_views: minV, exact_match: exactV }),
1205
- catalog.searchCatalog('design_systems', { query, limit, slim, min_views: minV, exact_match: exactV }),
1206
- ]);
1207
- return textResult({ query: query || null, components: r[0], skills: r[1], assets: r[2], design_systems: r[3] }); },
1208
- aura_recommend: async (a) => { a = a || {}; if (typeof a.goal !== 'string' || !a.goal.trim()) toolFn_bad('goal (string) is required');
1209
- // Gap 1: recommend hard-filters theme (not just reranks) — starter kits must not
1210
- // mix light rows into a dark ask. min_views gates noise; both are explicit params.
1211
- const themeV = a.theme === undefined ? undefined : theme(a.theme);
1212
- const negative_themeV = a.negative_theme === undefined ? undefined : theme(a.negative_theme);
1213
- const min_views = toolFn_num(a.min_views, 'min_views');
1214
- const slim = a.slim === undefined ? undefined : toolFn_bool(a.slim, 'slim');
1215
- const r = await Promise.all([
1216
- catalog.searchCatalog('components', { query: a.goal, freeOnly: true, limit: 3, theme: themeV, negative_theme: negative_themeV, min_views, slim }),
1217
- catalog.searchCatalog('skills', { query: a.goal, limit: 3, slim }),
1218
- catalog.searchCatalog('assets', { query: a.goal, freeOnly: true, limit: 3, slim }),
1219
- catalog.searchCatalog('design_systems', { query: a.goal, limit: 3, slim }),
1220
- ]);
1221
- // P0 auto-retry (bounded: max 1 retry + 1 picks call, no loops): an honest-empty
1222
- // components surface (no_match) retries once via suggested_queries[0] with the
1223
- // same filters, then falls back to human-grade editorial picks so the kit fills.
1224
- if (r[0] && r[0].no_match === true && (!Array.isArray(r[0].items) || !r[0].items.length)) {
1225
- const sq = Array.isArray(r[0].suggested_queries) ? r[0].suggested_queries.filter((q) => typeof q === 'string' && q.trim()) : [];
1226
- let filled = false;
1227
- if (sq.length) {
1228
- try {
1229
- const retry = await catalog.searchCatalog('components', { query: sq[0], freeOnly: true, limit: 3, theme: themeV, negative_theme: negative_themeV, min_views, slim });
1230
- if (retry && Array.isArray(retry.items) && retry.items.length) {
1231
- retry.retried = true;
1232
- retry.retried_from = sq[0];
1233
- r[0] = retry;
1234
- filled = true;
1235
- }
1236
- } catch { /* keep the original honest-empty */ }
1237
- }
1238
- if (!filled && (!Array.isArray(r[0].items) || !r[0].items.length)) {
1239
- try {
1240
- const picks = typeof catalog.editorialPicks === 'function' ? await catalog.editorialPicks(3) : [];
1241
- if (Array.isArray(picks) && picks.length) {
1242
- r[0] = { kind: 'components', total: picks.length, items: picks, editorial_fallback: true, editorial_note: 'Goal matched nothing — showing human-grade free picks instead.' };
1243
- }
1244
- } catch { /* keep the original honest-empty */ }
1245
- }
1246
- }
1247
- r[0].items = (r[0].items || []).slice(0, 3);
1248
- const pick = (x) => ((x && x.items) || []).slice(0, 3).map((i) => ({ title: i.title, page_url: i.page_url }));
1249
- return textResult({ goal: a.goal, freeOnly: true,
1250
- rationale: 'Top 3 free catalogue hits per surface, ranked by Aura views. Open the page_url previews, then fetch full source or content for finalists.',
1251
- commercial_note: 'Assets (and component art) are all-rights-reserved per Aura Terms §4: fine to prototype, get Aura permission before commercial ship.',
1252
- components: r[0], skills: r[1], assets: r[2], design_systems: r[3],
1253
- starter_kit: { components: pick(r[0]), skills: pick(r[1]), assets: pick(r[2]), design_systems: pick(r[3]) } }); },
1254
- aura_install_component: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
1255
- if (id === undefined || (typeof id !== 'string' && typeof id !== 'number')) toolFn_bad('provide id (number) or slug (string)');
1256
- const got = await catalog.getItem('components', id);
1257
- if (got.item && got.item.premium !== false) toolFn_bad('that component is Pro (paid). Search with freeOnly or pick a free one.');
1258
- return textResult({ item: got.item, install: installGuide('components', got.item) }); },
1259
- aura_install_skill: async (a) => { a = a || {}; if (typeof a.id !== 'string' || !a.id) toolFn_bad('id (string) is required');
1260
- const got = await catalog.getItem('skills', a.id);
1261
- return textResult({ item: got.item, install: installGuide('skills', got.item) }); },
1262
- aura_use_design_system: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
1263
- if (typeof id !== 'string' || !id) toolFn_bad('provide id or slug (string)');
1264
- const got = await catalog.getItem('design_systems', id);
1265
- return textResult({ item: got.item, install: installGuide('design_systems', got.item), tokens: tokenHints(got.item.content || '', got.item.preview_html || '') }); },
1266
- aura_categories: async () => { const cats = await catalog.categoryCounts();
1267
- const s = (cats || []).reduce((n, c) => n + (typeof c.free === 'number' ? c.free : 0), 0);
1268
- return textResult({ categories: cats, overlap_note: 'Counts overlap: components carry multiple tags, so per-category free counts sum to ' + s + ' (> 2495 free total). They triage depth, not a partition.' }); },
1269
- aura_prompt: async (a) => { a = a || {}; return textResult(buildPrompt({ goal: typeof a.goal === 'string' ? a.goal : undefined, theme: a.theme, surfaces: a.surfaces, system: a.system, answers: a.answers })); },
1270
- aura_bundle: async (a) => { a = a || {}; const ids = normRefs(a.ids, 'ids') || normRefs(a.slugs, 'slugs');
1271
- if (!ids || !ids.length) toolFn_bad('provide ids (array of 1-8 numbers/strings) or slugs (array of strings)');
1272
- return textResult({ kind: 'components', results: await catalog.bundleItems('components', ids) }); },
1273
- aura_related: async (a) => { a = a || {}; const k = a.kind;
1274
- if (k !== 'components' && k !== 'skills' && k !== 'design_systems') toolFn_bad('kind must be components|skills|design_systems');
1275
- const id = a.id; if (typeof id !== 'string' && typeof id !== 'number') toolFn_bad('id is required');
1276
- return textResult(await catalog.relatedItems(k, id, 3)); },
1277
- aura_install_asset: async (a) => { a = a || {}; if (typeof a.id !== 'number') toolFn_bad('id (number) is required');
1278
- const got = await catalog.getItem('assets', a.id);
1279
- const it = got.item; const fx = (it.facets || {});
1280
- return textResult({ item: it, install: { kind: 'assets', title: it.title, page_url: it.page_url, license: fx.license || 'all-rights-reserved (Aura Terms §4)', terms_url: 'https://www.aura.build/terms', download: fx.download || null, preview: fx.preview || null, files: [{ path: 'assets/' + it.id + '-' + String(it.title || 'asset').toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 40) + '.jpg', contains: 'downloaded original' }], steps: ['Check the license on the Aura asset page before commercial use — this server reports unknown, never assumes free-to-sell.', 'Download the download URL into the suggested path.', 'Use the preview URL for <img> srcset while drafting.'] } }); },
1281
- aura_bulk_fetch: async (a) => { a = a || {}; const kind = a.kind;
1282
- if (kind !== 'components' && kind !== 'design_systems' && kind !== 'assets') toolFn_bad('kind must be components|design_systems|assets');
1283
- const ids = normRefs(a.ids, 'ids');
1284
- if (!ids || !ids.length) toolFn_bad('provide ids (array of 1-8)');
1285
- return textResult({ kind, results: await catalog.bundleItems(kind, ids) }); },
1286
- aura_scaffold_page: async (a) => { a = a || {}; if (typeof a.goal !== 'string' || !a.goal.trim()) toolFn_bad('goal (string) is required');
1287
- const sysRef = (typeof a.system === 'string' && a.system) ? a.system : null;
1288
- const compRefs = normRefs(a.components, 'components') || [];
1289
- const full = a.full === undefined ? undefined : toolFn_bool(a.full, 'full');
1290
- const fullOpt = full === undefined ? {} : { full };
1291
- const [sys, comps] = await Promise.all([
1292
- sysRef ? catalog.getItem('design_systems', sysRef, fullOpt).catch(() => null) : catalog.searchCatalog('design_systems', { query: a.goal, limit: 1 }).then((r) => (r.items[0] ? { item: r.items[0] } : null)),
1293
- compRefs.length ? catalog.bundleItems('components', compRefs, 5, fullOpt).then((rs) => rs.filter((x) => x.ok).map((x) => x.item)) : catalog.searchCatalog('components', { query: a.goal, freeOnly: true, limit: 3 }).then((r) => r.items),
1294
- ]);
1295
- const sysItem = sys && sys.item ? sys.item : null;
1296
- const toks = sysItem ? tokenHints(sysItem.content || '') : { tokens: {}, css: '' };
1297
- const guides = (comps || []).map((c) => installGuide('components', c));
1298
- const deps = []; const seen = new Set();
1299
- for (const g of guides) for (const d of (g.deps || [])) if (!seen.has(d.name)) { seen.add(d.name); deps.push(d); }
1300
- const files = [{ path: 'styles/tokens.css', contains: 'tokens.css from use_design_system' }];
1301
- if (sysItem) files.push({ path: 'DESIGN.md', contains: 'system rules' });
1302
- for (const g of guides) for (const f of (g.files || [])) files.push(f);
1303
- return textResult({ goal: a.goal, order: ['1 tokens.css', '2 system preview_html', '3 component markup in listed order'], tokens_css: toks.css || '', system: sysItem, components: comps || [], guides, combined_deps: deps, files, license_note: 'Markup and any pulled assets are all-rights-reserved per Aura Terms §4 — fine to prototype, check each page_url before commercial ship.' }); },
1304
- };
1305
- };
1306
-
1307
- // ---- server.mjs (main) ----
1308
- // Module: server — composition root. Wires config -> fetcher -> catalog ->
1309
- // tools -> protocol session over stdio. Only framing + wiring lives here.
1310
-
1311
-
1312
-
1313
-
1314
-
1315
-
1316
-
1317
- const config = loadConfig();
1318
- const fetcher = createFetcher({ fetchImpl: globalThis.fetch, timeoutMs: config.timeoutMs, retries: config.retries, userAgent: config.userAgent });
1319
- const catalog = createCatalog({ fetcher, config, facetsFn: facets });
1320
- const handlers = createHandlers(catalog);
1321
-
1322
- let buffer = "";
1323
- const send = (msg) => process.stdout.write(JSON.stringify(msg) + "\n");
1324
- const { dispatch } = createSession(send, { tools: TOOL_DEFS, handlers });
1325
-
1326
- process.stdin.setEncoding("utf8");
1327
- process.stdin.on("data", (chunk) => {
1328
- buffer += chunk;
1329
- let i;
1330
- while ((i = buffer.indexOf("\n")) >= 0) {
1331
- const line = buffer.slice(0, i).trim();
1332
- buffer = buffer.slice(i + 1);
1333
- if (!line) continue;
1334
- let msg;
1335
- try { msg = JSON.parse(line); }
1336
- catch { send({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } }); continue; }
1337
- dispatch(msg);
1338
- }
1339
- });
1340
- process.stdin.on("end", () => process.exit(0));
1341
- process.stdin.resume();
1342
-
1343
- // Startup cinematics live on stderr only: stdout is reserved for JSON-RPC.
1344
- // Sequence: aura moment -> catalogue credit -> builder credit -> running.
1345
- const BANNER = [
1346
- " <<< aura-components-mcp >>>",
1347
- " free Aura catalogue, every AI agent",
1348
- ];
1349
- const sleepMs = (ms) => new Promise((r) => setTimeout(r, ms));
1350
- const paint = (s) => process.stderr.write(s + "\n");
1351
- async function typeLine(text, cps = 90) {
1352
- for (const ch of text) { process.stderr.write(ch); await sleepMs(1000 / cps); }
1353
- process.stderr.write("\n");
1354
- }
1355
- async function dots(label, n = 3) {
1356
- process.stderr.write(label);
1357
- for (let i = 0; i < n; i++) { await sleepMs(160); process.stderr.write("."); }
1358
- process.stderr.write("\n");
1359
- }
1360
- async function boot() {
1361
- if (process.env.AURA_QUIET === "1" || process.argv.includes("--quiet")) {
1362
- paint(SERVER_INFO.name + " v" + SERVER_INFO.version + " listening on stdio");
1363
- return;
1364
- }
1365
- for (const l of BANNER) paint(l);
1366
- await typeLine("starting your aura for designing...");
1367
- await dots("warming catalogue cache");
1368
- paint("catalogue credit: Aura (aura.build) — components, skills, assets, systems");
1369
- await sleepMs(180);
1370
- paint("built by swastiksingh-dev — free to use, MIT, no paywall");
1371
- await sleepMs(180);
1372
- paint(SERVER_INFO.name + " v" + SERVER_INFO.version + " running — 20 tools on stdio");
1373
- }
1374
- boot();
1
+ #!/usr/bin/env node
2
+ // aura-components-mcp v1.7.1 — bundled (zero deps). Built by swastiksingh-dev.
3
+
4
+ // ---- config.mjs ----
5
+ // Module: config — one small interface (loadConfig) over all env parsing.
6
+ // Callers learn one function; env names, defaults, and clamping live here.
7
+
8
+ const num = (raw, fallback, { min = 1, max = 100 } = {}) => {
9
+ const n = Number.parseInt(String(raw ?? ""), 10);
10
+ if (!Number.isFinite(n)) return fallback;
11
+ return Math.min(max, Math.max(min, n));
12
+ };
13
+
14
+ const bool = (raw, fallback) => {
15
+ if (raw === undefined) return fallback;
16
+ return /^(1|true|yes|on)$/i.test(String(raw));
17
+ };
18
+
19
+ function loadConfig(env = process.env) {
20
+ const defaultKey =
21
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImhvaXJxcmtkZ2JtdnB3dXR3dXdqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDM2Nzc2NTAsImV4cCI6MjA1OTI1MzY1MH0._UsCSHsTELn7m54tOhX3ySm67WEhcyHAPbuxEQZsl3c";
22
+ return {
23
+ supabaseUrl: env.AURA_SUPABASE_URL ?? "https://hoirqrkdgbmvpwutwuwj.supabase.co",
24
+ anonKey: env.AURA_SUPABASE_ANON_KEY ?? defaultKey,
25
+ timeoutMs: num(env.AURA_TIMEOUT_MS, 12_000, { min: 1_000, max: 60_000 }),
26
+ retries: num(env.AURA_RETRIES, 2, { min: 0, max: 5 }),
27
+ cacheTtlMs: num(env.AURA_CACHE_TTL_MS, 60_000, { min: 0, max: 3_600_000 }),
28
+ defaultLimit: num(env.AURA_DEFAULT_LIMIT, 10, { min: 1, max: 50 }),
29
+ maxLimit: num(env.AURA_MAX_LIMIT, 25, { min: 1, max: 50 }),
30
+ codeChars: num(env.AURA_CODE_CHARS, 12_000, { min: 1_000, max: 60_000 }),
31
+ contentChars: num(env.AURA_CONTENT_CHARS, 12_000, { min: 1_000, max: 60_000 }),
32
+ freeOnlyDefault: bool(env.AURA_FREE_ONLY_DEFAULT, true),
33
+ userAgent: env.AURA_USER_AGENT ?? "aura-components-mcp/1.7.1",
34
+ };
35
+ }
36
+
37
+ // ---- http.mjs ----
38
+ // Module: http — one small interface (createFetcher) hiding timeout,
39
+ // retry-with-backoff, error mapping, and the injectable fetch seam.
40
+ // Tests inject a stub fetch; production passes globalThis.fetch.
41
+
42
+ function createFetcher({ fetchImpl, timeoutMs, retries, userAgent }) {
43
+ if (typeof fetchImpl !== "function") throw new Error("fetchImpl must be a function");
44
+
45
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
46
+
47
+ async function once(url, init) {
48
+ const ctrl = new AbortController();
49
+ const t = setTimeout(() => ctrl.abort(new Error("timeout after " + timeoutMs + "ms")), timeoutMs);
50
+ try {
51
+ return await fetchImpl(url, {
52
+ ...init,
53
+ signal: ctrl.signal,
54
+ headers: { "user-agent": userAgent, ...(init?.headers ?? {}) },
55
+ });
56
+ } finally {
57
+ clearTimeout(t);
58
+ }
59
+ }
60
+
61
+ // GET JSON over PostgREST. Returns { rows, total } (total from content-range).
62
+ // Retryable: 429/5xx, network errors, timeouts (except AbortError: hung
63
+ // requests are not retried). Non-retryable: other 4xx (invalid requests
64
+ // never succeed on retry) — thrown immediately with noRetry set.
65
+ async function getJson(url, headers) {
66
+ let lastErr;
67
+ for (let attempt = 0; attempt <= retries; attempt++) {
68
+ try {
69
+ const res = await once(url, { headers });
70
+ if (res.status === 429 || (res.status >= 500 && res.status < 600)) {
71
+ lastErr = new Error("transient upstream status " + res.status);
72
+ } else if (!res.ok) {
73
+ const body = await res.text().catch(() => "");
74
+ const e = new Error("upstream " + res.status + ": " + body.slice(0, 300));
75
+ if (res.status >= 400 && res.status < 500) e.noRetry = true;
76
+ throw e;
77
+ } else {
78
+ const range = res.headers?.get?.("content-range") ?? null;
79
+ const total = range && range.includes("/") ? Number(range.split("/").pop()) : null;
80
+ const rows = await res.json();
81
+ return { rows: Array.isArray(rows) ? rows : [rows], total: Number.isFinite(total) ? total : null };
82
+ }
83
+ } catch (err) {
84
+ if (err && (err.noRetry || err.name === "AbortError")) throw err;
85
+ lastErr = err;
86
+ }
87
+ if (attempt < retries) await sleep(120 * 2 ** attempt + Math.floor(Math.random() * 60));
88
+ }
89
+ throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
90
+ }
91
+
92
+ // GET raw text (icons). 404 returns null (fallback chain keeps walking);
93
+ // 429/5xx retry like getJson; other non-2xx also return null (a missing
94
+ // icon is normal, not an upstream failure).
95
+ async function getText(url, headers) {
96
+ let lastErr;
97
+ for (let attempt = 0; attempt <= retries; attempt++) {
98
+ try {
99
+ const res = await once(url, { headers });
100
+ if (res.status === 429 || (res.status >= 500 && res.status < 600)) {
101
+ lastErr = new Error("transient upstream status " + res.status);
102
+ } else if (res.status === 404) {
103
+ return null;
104
+ } else if (!res.ok) {
105
+ return null;
106
+ } else {
107
+ return await res.text();
108
+ }
109
+ } catch (err) {
110
+ lastErr = err;
111
+ if (err && err.name === "AbortError") break;
112
+ }
113
+ if (attempt < retries) await sleep(120 * 2 ** attempt + Math.floor(Math.random() * 60));
114
+ }
115
+ if (lastErr) throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
116
+ return null;
117
+ }
118
+
119
+ return { getJson, getText };
120
+ }
121
+
122
+ // ---- catalog.mjs ----
123
+ // Module: catalog — the deep module. Small interface (searchCatalog,
124
+ // getItem, getStatus) over query building, caching, truncation, author
125
+ // enrichment, and page-URL derivation. Adapters: PostgREST via fetcher.
126
+
127
+ const SITE = "https://www.aura.build";
128
+
129
+ const CATALOG_SORTS = {
130
+ components: { popular: "views.desc", recent: "created_at.desc", trending: "views.desc", updated: "updated_at.desc" },
131
+ skills: { popular: "views.desc", recent: "created_at.desc", trending: "views.desc", updated: "updated_at.desc" },
132
+ assets: { popular: "views.desc", recent: "created_at.desc", trending: "views.desc", updated: "updated_at.desc" },
133
+ design_systems: { popular: "views.desc", recent: "created_at.desc", trending: "views.desc", updated: "updated_at.desc" },
134
+ };
135
+
136
+ const LIST_COLS = {
137
+ // PostgREST can't substring server-side: lists fetch code but shapeRow truncates to
138
+ // a 600-char probe for facets BEFORE the truncation marker (see shapeRow). ~0.7KB/row.
139
+ components: "id,title,description,tags,code,premium,views,forks,slug,background,created_by,created_at,updated_at",
140
+ skills: "id,title,description,source_url,views,forks,featured,created_by,created_at,updated_at",
141
+ assets: "id,title,description,keywords,media_type,premium,views,forks,image_800w,video_url,video_poster_url,created_by,created_at",
142
+ design_systems: "id,slug,title,description,views,forks,featured,created_by,created_at,updated_at",
143
+ };
144
+
145
+ const DETAIL_COLS = {
146
+ components: "id,title,description,tags,code,premium,views,forks,slug,image_url,background,credit_name,credit_url,created_by,created_at,updated_at",
147
+ skills: "id,title,description,content,source_url,views,forks,featured,created_by,created_at,updated_at",
148
+ assets: "id,title,description,keywords,resolution,colors,media_type,premium,views,forks,image_320w,image_800w,image_1600w,image_3840w,image_original,image_url,video_url,video_poster_url,video_duration,slug,created_by,created_at,updated_at",
149
+ design_systems: "id,slug,title,description,content,preview_html,thumbnail_url,source_name,views,forks,featured,created_by,created_at,updated_at",
150
+ };
151
+
152
+ const esc = (s) => String(s).replaceAll('"', '""');
153
+ const ilike = (v) => "*" + String(v).replaceAll("*", "").replaceAll(",", " ").trim() + "*";
154
+
155
+ // Split a goal sentence into significant tokens for OR fallback (stop-word filtered).
156
+ // Theme words (dark/light) are NOT search tokens — they become rank/filter signals
157
+ // (see detectThemeHint + scoreByTheme), so "dark cinematic" can't match light rows.
158
+ const STOP = new Set("a,an,the,for,with,and,or,of,to,in,on,my,new,free,also,that,this,from,into,plus,vs,top,best,up".split(","));
159
+ const THEME_WORDS = new Set(["dark", "light", "midnight", "noir", "black", "white", "bright", "airy", "minimal"]);
160
+ function queryTokens(q, max = 4) {
161
+ return String(q ?? "").toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).map((t) => t.trim()).filter((t) => t.length > 2 && !STOP.has(t) && !THEME_WORDS.has(t)).slice(0, max);
162
+ }
163
+ // Theme hint from the raw query: explicit signal, separate from match tokens.
164
+ function detectThemeHint(q) {
165
+ const t = String(q ?? "").toLowerCase();
166
+ const dark = /\bdark\b|\bmidnight\b|\bnoir\b|\bcinematic\b/.test(t);
167
+ const light = /\blight\b|\bbright\b|\bairy\b/.test(t);
168
+ if (dark && !light) return "dark";
169
+ if (light && !dark) return "light";
170
+ return null;
171
+ }
172
+ // Rank rows: +2 per matched token in title, +1 in description/tags, theme match first.
173
+ // Returns { rows, scored } where scored explains the top pick for fallback_score.
174
+ function scoreRows(kind, rows, toks, themeHint) {
175
+ const scored = rows.map((r) => {
176
+ const title = String(r.title || "").toLowerCase();
177
+ const desc = String(r.description || "").toLowerCase();
178
+ const tags = ((r.tags || []).map((x) => String(x).toLowerCase()).join(" "));
179
+ let s = 0;
180
+ for (const t of toks) { if (title.includes(t)) s += 2; else if (desc.includes(t) || tags.includes(t)) s += 1; }
181
+ const th = r.facets && r.facets.theme;
182
+ const themeOk = !themeHint || th === themeHint || th === "mixed" || th === "unknown";
183
+ if (themeHint && th === themeHint) s += 3;
184
+ return { r, s, themeOk };
185
+ });
186
+ scored.sort((a, b) => ((b.themeOk ? 1 : 0) - (a.themeOk ? 1 : 0)) || (b.s - a.s) || ((b.r.views || 0) - (a.r.views || 0)));
187
+ return scored;
188
+ }
189
+
190
+ // Kind-aware token overlap for the zero-overlap refusal gate (pure, no fetch).
191
+ // Components/assets: title-only (strict visual surfaces; tags/descriptions don't count).
192
+ // Skills/design_systems: wider net — title + description + tags + any snippet present.
193
+ function matchOverlap(kind, toks, item) {
194
+ const list = Array.isArray(toks) ? toks : [];
195
+ if (!list.length || !item) return 0;
196
+ if (kind === "components" || kind === "assets") {
197
+ const title = String(item.title || "").toLowerCase();
198
+ let n = 0;
199
+ for (const t of list) {
200
+ const tok = String(t ?? "").toLowerCase();
201
+ if (tok && title.includes(tok)) n++;
202
+ }
203
+ return n;
204
+ }
205
+ const parts = [String(item.title || ""), String(item.description || "")];
206
+ if (Array.isArray(item.tags)) parts.push(item.tags.map((x) => String(x)).join(" "));
207
+ for (const k of ["content_snippet", "content_excerpt", "preview_snippet", "tokens_snippet"]) {
208
+ if (typeof item[k] === "string" && item[k]) parts.push(item[k]);
209
+ }
210
+ const hay = parts.join("\n").toLowerCase();
211
+ let n = 0;
212
+ for (const t of list) {
213
+ const tok = String(t ?? "").toLowerCase();
214
+ if (tok && hay.includes(tok)) n++;
215
+ }
216
+ return n;
217
+ }
218
+
219
+ function windowAgoIso(days) {
220
+ const d = new Date();
221
+ d.setDate(d.getDate() - days);
222
+ return d.toISOString();
223
+ }
224
+ function trendingIso(days = 90) { return windowAgoIso(days); } // 7d seed is empty; callers pass window_days (default 30 for components now)
225
+
226
+ function buildSearchParams(kind, q = {}) {
227
+ const p = new URLSearchParams();
228
+ p.set("select", LIST_COLS[kind]);
229
+ const limit = q.limit;
230
+ const offset = q.offset ?? 0;
231
+ p.set("limit", String(limit));
232
+ p.set("offset", String(offset));
233
+ const sort = CATALOG_SORTS[kind][q.sort] ?? CATALOG_SORTS[kind].popular;
234
+ p.set("order", sort);
235
+ const filters = [];
236
+ if (kind === "components" || kind === "assets") {
237
+ filters.push("private=eq.false");
238
+ if (q.freeOnly) filters.push("premium=eq.false");
239
+ } else if (kind === "skills" || kind === "design_systems") {
240
+ filters.push("private=eq.false");
241
+ }
242
+ if (q.tag && kind === "components") filters.push("tags=cs.{" + q.tag.toLowerCase() + "}");
243
+ if (q.mediaType && kind === "assets") filters.push("media_type=eq." + q.mediaType);
244
+ const ors = [];
245
+ if (q.query && q.query.trim()) {
246
+ const v = ilike(q.query);
247
+ if (kind === "components") ors.push("title.ilike." + v + ",description.ilike." + v);
248
+ else if (kind === "skills") ors.push("title.ilike." + v + ",description.ilike." + v);
249
+ else if (kind === "assets") ors.push("title.ilike." + v + ",description.ilike." + v);
250
+ else ors.push("title.ilike." + v + ",description.ilike." + v + ",content.ilike." + v + ",preview_html.ilike." + v);
251
+ }
252
+ if (q.sort === "trending") filters.push("created_at=gte." + trendingIso(q.window_days ?? 90));
253
+ return { params: p, filters, ors };
254
+ }
255
+
256
+ function buildUrl(base, table, params, filters, ors) {
257
+ let qs = params.toString();
258
+ for (const f of filters) qs += "&" + f;
259
+ if (ors.length) qs += "&or=(" + ors.join(",") + ")";
260
+ return base + "/rest/v1/" + table + "?" + qs;
261
+ }
262
+
263
+ // Truncation with cursors: agents see full_length + chunk info instead of a silent
264
+ // marker, and can fetch the rest via getItem with { chunk } (see getItem below).
265
+ function chunkText(s, n, chunk = 0) {
266
+ const full = String(s || "");
267
+ const total = Math.max(1, Math.ceil(full.length / n));
268
+ const i = Math.max(0, Math.min(chunk, total - 1));
269
+ return { text: full.slice(i * n, (i + 1) * n) + (i < total - 1 ? "…[truncated]" : ""), chunk: i, chunks: total, full_length: full.length, truncated: total > 1 };
270
+ }
271
+
272
+ // Slim lists: drop heavy blobs (full code/content/preview_html) from list rows,
273
+ // keep excerpts + chunk pointers + full_fetch so agents fetch detail on purpose.
274
+ // getItem/bundleItems stay full — detail is their job. Default on (q.slim ?? true).
275
+ function slimRows(kind, items) {
276
+ return (items || []).map((it) => {
277
+ const o = { ...it };
278
+ if (kind === "components" && typeof o.code === "string") delete o.code; // keep code_excerpt + code_info + full_fetch
279
+ if (kind === "skills" && typeof o.content === "string") { o.content_excerpt = String(o.content).slice(0, 800); delete o.content; }
280
+ if (kind === "design_systems") {
281
+ if (typeof o.content === "string") { o.content_excerpt = String(o.content).slice(0, 800); delete o.content; }
282
+ if (typeof o.preview_html === "string") { o.preview_excerpt = String(o.preview_html).slice(0, 800); delete o.preview_html; }
283
+ }
284
+ return o;
285
+ });
286
+ }
287
+
288
+ // Central access policy: only components/assets carry a premium flag, and a
289
+ // row is free ⟺ premium === false (fail-closed: missing or ambiguous
290
+ // entitlement is refused, never assumed public). Skills/design_systems
291
+ // have no premium concept and are always allowed. Every retrieval path
292
+ // (getItem, and therefore bundle/bulk/scaffold/related/install) funnels
293
+ // through assertFreeRow, so callers cannot reach restricted content by ID.
294
+ function isFreeRow(kind, row) {
295
+ if (kind !== "components" && kind !== "assets") return true;
296
+ return !!row && row.premium === false;
297
+ }
298
+
299
+ function assertFreeRow(kind, row) {
300
+ if (isFreeRow(kind, row)) return;
301
+ const what = kind === "assets" ? "asset" : "component";
302
+ const e = new Error("that " + what + " is Pro (paid). Search with freeOnly or pick a free one.");
303
+ e.code = "BAD_ARGS";
304
+ throw e;
305
+ }
306
+
307
+ // Row-shape gate: the minimum fields every surface needs to build a usable
308
+ // item (identity for page_url + title for display/matching). Rows that fail
309
+ // are upstream schema drift, not empty results: listings disclose them via
310
+ // malformed_dropped, single-record fetch fails loud. Never silently shaped
311
+ // into "undefined" URLs or untitled items.
312
+ function isWellFormedRow(kind, row) {
313
+ if (!row || typeof row !== "object") return false;
314
+ if (typeof row.title !== "string" || !row.title) return false;
315
+ const hasId = row.id !== undefined && row.id !== null;
316
+ const hasSlug = typeof row.slug === "string" && !!row.slug;
317
+ if (kind === "components" || kind === "design_systems") return hasId || hasSlug;
318
+ return hasId;
319
+ }
320
+
321
+ function pageUrl(kind, row) {
322
+ if (kind === "components") return SITE + "/component/" + row.slug;
323
+ if (kind === "skills") return SITE + "/skills/" + row.id;
324
+ // ISS9: asset pages are query-routed (no /assets/ID route exists). Keep the q= form
325
+ // but ALSO expose a stable key so agents can cite: id + slug + title together.
326
+ if (kind === "assets") return SITE + "/assets?q=" + encodeURIComponent(row.title ?? "");
327
+ return SITE + "/design-systems/" + (row.slug ?? row.id);
328
+ }
329
+
330
+ function assetKey(row) {
331
+ return { id: row.id, slug: row.slug ?? null, title: row.title ?? null };
332
+ }
333
+
334
+ function shapeRow(kind, row, cfg, withFacets) {
335
+ const base = { ...row };
336
+ // Facets probe the FULL available code (pre-truncation) so search facets equal
337
+ // detail facets and install-time needs: a short prefix missed late markers
338
+ // like data-lucide (id 1572 drift). Weight uses the true length (ISS1/2/3).
339
+ if (kind === "components" && typeof base.code === "string") {
340
+ base.code_chars = base.code.length;
341
+ if (withFacets) { try { base.facets = withFacets(kind, base, { fullLength: base.code.length }); } catch { /* never break rows */ } }
342
+ // Gap 3: code_excerpt (first 1200 chars, readable) + full_fetch flag. Agents skim
343
+ // the excerpt, then get chunk N — no more blind 18k-70k dumps or silent cuts.
344
+ base.code_excerpt = String(base.code).slice(0, 1200);
345
+ base.full_fetch = { tool: "aura_get_component", id: base.id ?? base.slug, chunks: Math.max(1, Math.ceil(base.code.length / cfg.codeChars)) };
346
+ const ch = chunkText(base.code, cfg.codeChars, 0);
347
+ base.code = ch.text;
348
+ base.code_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: 0, truncated: ch.truncated };
349
+ if (base.facets) base.facets.deps_summary = base.facets.needsTailwind || base.facets.needsIcons ? [base.facets.needsTailwind && "tailwindcss", base.facets.needsIcons && "iconify-icon"].filter(Boolean) : [];
350
+ }
351
+ if (kind === "skills" && typeof base.content === "string") {
352
+ const ch = chunkText(base.content, cfg.contentChars, 0);
353
+ base.content = ch.text;
354
+ base.content_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: 0, truncated: ch.truncated };
355
+ }
356
+ if (kind === "design_systems") {
357
+ if (typeof base.content === "string") {
358
+ const ch = chunkText(base.content, cfg.contentChars, 0);
359
+ base.content = ch.text;
360
+ base.content_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: 0, truncated: ch.truncated };
361
+ }
362
+ if (typeof base.preview_html === "string") {
363
+ const ch = chunkText(base.preview_html, cfg.codeChars, 0);
364
+ base.preview_html = ch.text;
365
+ base.preview_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: 0, truncated: ch.truncated };
366
+ }
367
+ }
368
+ base.page_url = pageUrl(kind, row);
369
+ if (kind === "assets") base.asset_key = assetKey(row);
370
+ if (withFacets && !base.facets) { try { base.facets = withFacets(kind, base); } catch { /* facets never break rows */ } }
371
+ // ISS8: force absolute Supabase URLs — relative variant paths break fetch.
372
+ if (kind === "assets") {
373
+ const abs = (u) => (!u ? u : (/^https?:\/\//i.test(u) ? u : ("https://hoirqrkdgbmvpwutwuwj-all.supabase.co/storage/v1/object/public/assets/" + String(u).replace(/^\/+/, ""))));
374
+ for (const k of ["image_320w", "image_800w", "image_1600w", "image_3840w", "image_original", "image_url", "video_url", "video_poster_url"]) if (typeof base[k] === "string") base[k] = abs(base[k]);
375
+ if (base.facets) {
376
+ if (base.facets.download) base.facets.download = abs(base.facets.download);
377
+ if (base.facets.preview) base.facets.preview = abs(base.facets.preview);
378
+ }
379
+ }
380
+ return base;
381
+ }
382
+
383
+ function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = null }) {
384
+ const base = config.supabaseUrl.replace(/\/$/, "");
385
+ const headers = { apikey: config.anonKey, Authorization: "Bearer " + config.anonKey, Prefer: "count=exact" };
386
+ const cache = new Map();
387
+ const MAX_KEYS = 300;
388
+ const cacheGet = (k) => {
389
+ if (!config.cacheTtlMs) return null;
390
+ const e = cache.get(k);
391
+ if (!e) return null;
392
+ if (now() - e.t > config.cacheTtlMs) { cache.delete(k); return null; }
393
+ cache.delete(k); cache.set(k, e); // LRU refresh
394
+ return e.v;
395
+ };
396
+ const cacheSet = (k, v) => {
397
+ if (!config.cacheTtlMs) return;
398
+ if (cache.has(k)) cache.delete(k);
399
+ cache.set(k, { t: now(), v });
400
+ while (cache.size > MAX_KEYS) cache.delete(cache.keys().next().value);
401
+ };
402
+ const authorCache = new Map(); // created_by -> profile (shared across searches)
403
+ const inflight = new Map(); // cacheKey -> Promise (coalesce concurrent identical calls)
404
+
405
+ const clampLimit = (l) => Math.min(config.maxLimit, Math.max(1, Math.floor(l ?? config.defaultLimit)));
406
+
407
+ async function enrichAuthors(rows) {
408
+ const ids = [...new Set(rows.map((r) => r.created_by).filter(Boolean).filter((id) => !authorCache.has(id)))];
409
+ if (ids.length) {
410
+ try {
411
+ const url = base + "/rest/v1/public_author_profiles?select=id,full_name,avatar_url,slug&id=in.(" + ids.map(esc).join(",") + ")";
412
+ const { rows: authors } = await fetcher.getJson(url, headers);
413
+ for (const a of authors) { authorCache.set(a.id, a); if (authorCache.size > 500) break; }
414
+ } catch { /* authors stay null, rows still return */ }
415
+ }
416
+ return rows.map((r) => ({ ...r, author: (r.created_by && authorCache.get(r.created_by)) || null }));
417
+ }
418
+
419
+ async function searchCatalog(kind, q = {}) {
420
+ const limit = clampLimit(q.limit);
421
+ const offset = Math.max(0, q.offset ?? 0);
422
+ const sort = CATALOG_SORTS[kind][q.sort] ? q.sort : "popular";
423
+ const freeOnly = q.freeOnly ?? config.freeOnlyDefault;
424
+ // Gap 1: negative_theme (exclude a theme) + min_views (quality floor). Both ride
425
+ // the cache key so filtered/unfiltered variants never collide. Explicit
426
+ // theme also rides the key: it re-ranks results, so dark/light variants
427
+ // must never share a cache entry or an in-flight request.
428
+ const themeKey = q.theme ? String(q.theme).toLowerCase() : "";
429
+ const negTheme = q.negative_theme ? String(q.negative_theme).toLowerCase() : "";
430
+ const minViews = Math.max(0, q.min_views ?? 0);
431
+ const slim = q.slim ?? true;
432
+ const exactFlag = q.exact_match === true ? "exact" : "fuzzy";
433
+ const key = ["search", kind, q.query ?? "", q.tag ?? "", q.mediaType ?? "", sort, limit, offset, freeOnly, themeKey, negTheme, minViews, slim ? "slim" : "full", exactFlag].join("|");
434
+ const hit = cacheGet(key);
435
+ if (hit) return { ...hit, cached: true };
436
+ if (inflight.has(key)) return inflight.get(key);
437
+ const p = (async () => {
438
+ const runOnce = async (qq) => {
439
+ const { params, filters, ors } = buildSearchParams(kind, { ...qq, limit, offset, sort, freeOnly });
440
+ const url = buildUrl(base, kind, params, filters, ors);
441
+ return fetcher.getJson(url, headers);
442
+ };
443
+ let { rows, total } = await runOnce(q);
444
+ let fallback = null;
445
+ // AND-phrase queries like "dark cinematic portfolio" match nothing literally:
446
+ // retry as OR over significant tokens so every surface degrades instead of emptying.
447
+ // Theme words (dark/light/cinematic) are rank signals, not match tokens.
448
+ const toks = (q.query && q.query.trim() && !q.tag) ? queryTokens(q.query) : [];
449
+ const themeHint = (q.query && q.query.trim()) ? detectThemeHint(q.query) : null;
450
+ const effTheme = q.theme || themeHint;
451
+ const suggest = (t) => {
452
+ const s = [];
453
+ if (t.length > 1) s.push(t.slice(0, Math.min(3, t.length)).join(" "));
454
+ for (const x of t.slice(0, 3)) s.push(x);
455
+ return [...new Set(s)].slice(0, 4);
456
+ };
457
+ if ((!rows || !rows.length) && toks.length && q.exact_match !== true) {
458
+ // Pass 1: OR over tokens, same filters (drops the AND-phrase requirement).
459
+ const orParts = [];
460
+ for (const t of toks) {
461
+ const v = ilike(t);
462
+ if (kind === "design_systems") orParts.push("title.ilike." + v + ",description.ilike." + v + ",content.ilike." + v + ",preview_html.ilike." + v);
463
+ else orParts.push("title.ilike." + v + ",description.ilike." + v);
464
+ }
465
+ const b1 = buildSearchParams(kind, { ...q, query: "", limit, offset, sort, freeOnly });
466
+ try {
467
+ const fb = await fetcher.getJson(buildUrl(base, kind, b1.params, b1.filters, orParts), headers);
468
+ if (fb.rows && fb.rows.length) { rows = fb.rows; total = fb.total; fallback = "or-tokens:" + toks.join(","); }
469
+ } catch { /* try pass 2 */ }
470
+ // Pass 2: still empty (e.g. trending window + rare tokens): drop sort window,
471
+ // keep OR tokens, rank by views. Guarantees aggregators return something useful.
472
+ if ((!rows || !rows.length)) {
473
+ const b2 = buildSearchParams(kind, { ...q, query: "", limit, offset, sort: "popular", freeOnly });
474
+ try {
475
+ const fb2 = await fetcher.getJson(buildUrl(base, kind, b2.params, b2.filters, orParts), headers);
476
+ if (fb2.rows && fb2.rows.length) { rows = fb2.rows; total = fb2.total; fallback = "or-tokens-unwindowed:" + toks.join(","); }
477
+ } catch { /* try safety net */ }
478
+ }
479
+ // Safety net: total miss on a text query still returns usable items.
480
+ // Same freeOnly/tag/mediaType filters, popular order, labeled honestly —
481
+ // true never-empty (unless upstream itself is down).
482
+ if ((!rows || !rows.length)) {
483
+ const b3 = buildSearchParams(kind, { ...q, query: "", limit, offset, sort: "popular", freeOnly });
484
+ try {
485
+ const fb3 = await fetcher.getJson(buildUrl(base, kind, b3.params, b3.filters, []), headers);
486
+ if (fb3.rows && fb3.rows.length) { rows = fb3.rows; total = fb3.total; fallback = "popular-safety-net"; }
487
+ } catch { /* keep empty: upstream truth */ }
488
+ }
489
+ }
490
+ // ISS7: dedupe skills by source_url (same SKILL.md listed twice). First (most-viewed)
491
+ // wins, gets canonical:true; dupes are dropped and counted in deduped.
492
+ let deduped = 0;
493
+ if (kind === "skills") {
494
+ const seen = new Set();
495
+ const kept = [];
496
+ for (const r of rows) {
497
+ const u = (r.source_url || "").trim().toLowerCase();
498
+ if (u && seen.has(u)) { deduped++; continue; }
499
+ if (u) seen.add(u);
500
+ kept.push(r);
501
+ }
502
+ const byUrl = new Map();
503
+ for (const r of kept) {
504
+ const u = (r.source_url || "").trim().toLowerCase();
505
+ if (u && (!byUrl.has(u) || (r.views || 0) > (byUrl.get(u).views || 0))) byUrl.set(u, r);
506
+ }
507
+ rows = kept.map((r) => {
508
+ const u = (r.source_url || "").trim().toLowerCase();
509
+ return (u && byUrl.get(u) === r) ? { ...r, canonical: true } : r;
510
+ });
511
+ }
512
+ // Gap 4: inline snippets — one extra batched fetch fills preview/tokens glimpses
513
+ // into search rows (top N only) so agents decide without a second call per row.
514
+ // Bounded: top 5 rows, client-side sliced to 800 chars. No extra call when empty.
515
+ // Same fetch also resolves facets.has_preview to a real boolean for those rows.
516
+ let presence = null;
517
+ if ((kind === "design_systems" || kind === "skills") && rows.length) {
518
+ const topIds = rows.slice(0, 5).map((r) => r.id);
519
+ try {
520
+ const idList = topIds.map((id) => encodeURIComponent(String(id))).join(",");
521
+ const sel = kind === "design_systems" ? "id,content,preview_html" : "id,content,source_url";
522
+ const { rows: full } = await fetcher.getJson(base + "/rest/v1/" + kind + "?select=" + encodeURIComponent(sel) + "&id=in.(" + idList + ")", headers);
523
+ const byId = new Map(full.map((r) => [String(r.id), r]));
524
+ if (kind === "design_systems") presence = new Map(full.map((r) => [String(r.id), Boolean(r.preview_html)]));
525
+ rows = rows.map((r) => {
526
+ const f = byId.get(String(r.id));
527
+ if (!f) return r;
528
+ if (kind === "design_systems") return { ...r, preview_snippet: String(f.preview_html || "").slice(0, 800), tokens_snippet: String(f.content || "").slice(0, 800) };
529
+ return { ...r, content_snippet: String(f.content || "").slice(0, 800) };
530
+ });
531
+ } catch { /* snippets are best-effort; rows still return */ }
532
+ }
533
+ const enriched = await enrichAuthors(rows);
534
+ // Schema-drift gate: malformed rows are disclosed, never shaped into
535
+ // untitled items or ".../undefined" page URLs, and never counted as
536
+ // successful empty results (total keeps its upstream meaning).
537
+ const formed = [];
538
+ let malformed = 0;
539
+ for (const r of enriched) {
540
+ if (isWellFormedRow(kind, r)) formed.push(r);
541
+ else malformed++;
542
+ }
543
+ const out = { kind, total, limit, offset, items: formed.map((r) => shapeRow(kind, r, config, facetsFn)), cached: false };
544
+ if (malformed) out.malformed_dropped = malformed;
545
+ if (deduped) out.deduped = deduped;
546
+ // Gap 1: min_views quality floor (views-gated surfaces only) + negative_theme
547
+ // hard exclusion. Both apply before theme re-rank so counts stay honest.
548
+ // total keeps its upstream meaning (all matches for limit/offset paging);
549
+ // local drops are disclosed via min_views_filtered_out / deduped /
550
+ // negative_theme_filtered_out instead of rewriting total.
551
+ if (minViews > 0) {
552
+ const beforeViews = out.items.length;
553
+ out.items = out.items.filter((it) => (it.views || 0) >= minViews);
554
+ if (out.items.length < beforeViews) out.min_views_filtered_out = beforeViews - out.items.length;
555
+ }
556
+ if (negTheme && kind === "components") {
557
+ const before = out.items.length;
558
+ out.items = out.items.filter((it) => !it.facets || it.facets.theme !== negTheme);
559
+ if (out.items.length < before) out.negative_theme_filtered_out = before - out.items.length;
560
+ }
561
+ // Theme-aware re-rank (components): theme hint from query (dark/light/cinematic)
562
+ // or explicit q.theme reorders so matches come first — never silently drops rows,
563
+ // and always reports checked/kept counts so the re-rank is auditable, not opaque.
564
+ if (kind === "components" && effTheme) {
565
+ const scored = scoreRows(kind, out.items, toks, effTheme);
566
+ const themeOkCount = scored.filter((s) => s.themeOk).length;
567
+ out.theme_hint = effTheme;
568
+ out.theme_checked = scored.length;
569
+ out.theme_kept = out.items.length;
570
+ if (themeOkCount === 0) {
571
+ out.theme_warning = "no " + effTheme + "-theme rows in top results; showing unfiltered, check facets.theme per row";
572
+ } else {
573
+ out.items = scored.map((s) => s.r);
574
+ const dep = scored.length - themeOkCount;
575
+ if (dep > 0) out.theme_deprioritized = dep;
576
+ }
577
+ }
578
+ // Real has_preview for snippet-enriched rows (top 5); the rest stay unknown-list.
579
+ if (presence) {
580
+ for (const it of out.items) {
581
+ if (presence.has(String(it.id)) && it.facets) it.facets = { ...it.facets, has_preview: presence.get(String(it.id)) };
582
+ }
583
+ }
584
+ // ISS15: transparent fallback — agents see what happened, not a magic string.
585
+ // Safety net additionally carries suggested_queries so agents can retry sharp.
586
+ const suggestList = () => { const sq = suggest(toks).filter((s) => !/^[a-z0-9]{8,}$/i.test(s.replace(/\s/g, ""))); return sq.length ? sq : ["hero", "pricing", "landing page"]; };
587
+ if (fallback) {
588
+ out.fallback = fallback; out.isFallback = true;
589
+ const scoredTop = out.items[0];
590
+ const hits = matchOverlap(kind, toks, scoredTop);
591
+ out.fallback_score = "token-overlap:" + hits + "/" + toks.length + "-terms" + (effTheme ? ";theme:" + effTheme : "");
592
+ if (fallback === "popular-safety-net") out.suggested_queries = suggestList();
593
+ }
594
+ else if ((!rows || !rows.length) && toks.length) { out.isFallback = false; out.suggested_queries = suggestList(); }
595
+ // Zero-overlap refusal: fallback engaged but top row matches 0/N tokens —
596
+ // honest empty instead of irrelevant popular rows. Direct hits never refused.
597
+ // Kind-aware: components/assets stay title-only strict; skills/systems use the
598
+ // wider net (title+description+tags+snippets) via matchOverlap.
599
+ if (fallback != null && toks.length > 0) {
600
+ const top = out.items[0];
601
+ const hits2 = matchOverlap(kind, toks, top);
602
+ if (hits2 === 0) {
603
+ const refused = fallback;
604
+ out.items = [];
605
+ out.total = 0;
606
+ out.isFallback = false;
607
+ delete out.fallback;
608
+ delete out.fallback_score;
609
+ out.no_match = true;
610
+ out.refused_fallback = refused;
611
+ out.suggested_queries = suggestList();
612
+ out.note = "Top fallback result matched 0/" + toks.length + " title tokens — empty instead of irrelevant popular rows. Try a suggested query.";
613
+ }
614
+ }
615
+ if (slim) out.items = slimRows(kind, out.items);
616
+ cacheSet(key, out);
617
+ return out;
618
+ })();
619
+ inflight.set(key, p);
620
+ try { return await p; } finally { inflight.delete(key); }
621
+ }
622
+
623
+ async function getItem(kind, idOrSlug, opts = {}) {
624
+ const chunk = Math.max(0, opts.chunk ?? 0);
625
+ const wantFull = !!opts.full;
626
+ const key = ["get", kind, String(idOrSlug), "c" + chunk, wantFull ? "full" : "trunc"].join("|");
627
+ const hit = cacheGet(key);
628
+ if (hit) return { ...hit, cached: true };
629
+ const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(String(idOrSlug));
630
+ const isNum = /^\d+$/.test(String(idOrSlug));
631
+ const sel = DETAIL_COLS[kind];
632
+ // design_systems + skills: uuid id vs slug; components: numeric id vs slug
633
+ const filt = (kind === "design_systems" || kind === "skills")
634
+ ? (isUuid ? "id=eq." + encodeURIComponent(String(idOrSlug)) : "slug=eq." + encodeURIComponent(String(idOrSlug)))
635
+ : (kind === "components" && !isNum
636
+ ? "slug=eq." + encodeURIComponent(String(idOrSlug))
637
+ : "id=eq." + encodeURIComponent(String(idOrSlug)));
638
+ const url = base + "/rest/v1/" + kind + "?select=" + encodeURIComponent(sel) + "&" + filt + "&limit=1";
639
+ const { rows } = await fetcher.getJson(url, headers);
640
+ if (!rows.length) { const e = new Error("not found: " + kind + " " + idOrSlug); e.code = "NOT_FOUND"; throw e; }
641
+ assertFreeRow(kind, rows[0]); // before enrich/cache: refusals are never cached
642
+ if (!isWellFormedRow(kind, rows[0])) throw new Error("malformed upstream row for " + kind + " " + idOrSlug + ": missing identity/title fields");
643
+ const enriched = await enrichAuthors(rows);
644
+ const full = enriched[0];
645
+ // Chunked fetch: re-slice full blobs per requested chunk so agents can page
646
+ // through 18k-70k payloads instead of hitting one silent truncation wall.
647
+ const item = shapeRow(kind, full, config, facetsFn);
648
+ if (chunk > 0 && !wantFull) {
649
+ if (kind === "components" && typeof full.code === "string") {
650
+ const ch = chunkText(full.code, config.codeChars, chunk);
651
+ item.code = ch.text;
652
+ item.code_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: ch.chunk, truncated: ch.truncated };
653
+ }
654
+ if ((kind === "skills") && typeof full.content === "string") {
655
+ const ch = chunkText(full.content, config.contentChars, chunk);
656
+ item.content = ch.text;
657
+ item.content_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: ch.chunk, truncated: ch.truncated };
658
+ }
659
+ if (kind === "design_systems") {
660
+ if (typeof full.content === "string") {
661
+ const ch = chunkText(full.content, config.contentChars, chunk);
662
+ item.content = ch.text;
663
+ item.content_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: ch.chunk, truncated: ch.truncated };
664
+ }
665
+ if (typeof full.preview_html === "string") {
666
+ const ch = chunkText(full.preview_html, config.codeChars, chunk);
667
+ item.preview_html = ch.text;
668
+ item.preview_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: ch.chunk, truncated: ch.truncated };
669
+ }
670
+ }
671
+ }
672
+ if (wantFull) {
673
+ if (kind === "components" && typeof full.code === "string") {
674
+ item.code = full.code;
675
+ item.code_info = { full_length: full.code.length, chunks: 1, chunk: 0, truncated: false };
676
+ }
677
+ if (kind === "skills" && typeof full.content === "string") {
678
+ item.content = full.content;
679
+ item.content_info = { full_length: full.content.length, chunks: 1, chunk: 0, truncated: false };
680
+ }
681
+ if (kind === "design_systems") {
682
+ if (typeof full.content === "string") {
683
+ item.content = full.content;
684
+ item.content_info = { full_length: full.content.length, chunks: 1, chunk: 0, truncated: false };
685
+ }
686
+ if (typeof full.preview_html === "string") {
687
+ item.preview_html = full.preview_html;
688
+ item.preview_info = { full_length: full.preview_html.length, chunks: 1, chunk: 0, truncated: false };
689
+ }
690
+ }
691
+ }
692
+ const out = { kind, item, cached: false };
693
+ cacheSet(key, out);
694
+ return out;
695
+ }
696
+
697
+ async function getStatus() {
698
+ const key = "status";
699
+ const hit = cacheGet(key);
700
+ if (hit) return { ...hit, cached: true };
701
+ const counts = {};
702
+ const jobs = [
703
+ ["components_free", "components?select=id&private=eq.false&premium=eq.false&limit=1"],
704
+ ["components_pro", "components?select=id&private=eq.false&premium=eq.true&limit=1"],
705
+ ["skills", "skills?select=id&private=eq.false&limit=1"],
706
+ ["assets", "assets?select=id&private=eq.false&limit=1"],
707
+ ["design_systems", "design_systems?select=id&private=eq.false&limit=1"],
708
+ ];
709
+ await Promise.all(jobs.map(async ([k, path]) => {
710
+ try { const { total } = await fetcher.getJson(base + "/rest/v1/" + path, headers); counts[k] = total; }
711
+ catch { counts[k] = null; }
712
+ }));
713
+ // Freshness: best-effort MAX(updated_at) per table so agents see staleness.
714
+ // Never throws; null on failure; cached with the status entry.
715
+ const freshness = { components: null, skills: null, assets: null, design_systems: null };
716
+ await Promise.all(Object.keys(freshness).map(async (table) => {
717
+ try {
718
+ const { rows } = await fetcher.getJson(base + "/rest/v1/" + table + "?select=updated_at&order=updated_at.desc&limit=1", headers);
719
+ const v = rows && rows[0] ? rows[0].updated_at : null;
720
+ freshness[table] = v ? String(v) : null;
721
+ } catch { freshness[table] = null; }
722
+ }));
723
+ const out = { site: SITE, catalogue: "aura.build public catalogue (PostgREST, anon)", counts, freshness, cached: false };
724
+ cacheSet(key, out);
725
+ return out;
726
+ }
727
+
728
+ async function categoryCounts() {
729
+ const key = 'categories';
730
+ const hit = cacheGet(key);
731
+ if (hit) return hit.categories;
732
+ const cats = ['hero','section','button','card','background','header','logo','feature','pricing','testimonial','footer','form','heading'];
733
+ const out = await Promise.all(cats.map(async (c) => {
734
+ try {
735
+ const url = base + '/rest/v1/components?select=id&private=eq.false&premium=eq.false&tags=cs.{' + c + '}&limit=1';
736
+ const { total } = await fetcher.getJson(url, headers);
737
+ return { category: c, free: total };
738
+ } catch { return { category: c, free: null }; }
739
+ }));
740
+ cacheSet(key, { categories: out });
741
+ return out;
742
+ }
743
+
744
+ // Gap 2: editorial picks — human-grade component leads for when views-signal is
745
+ // noise (free trending total=2). Top free by forks (remix = real reuse), then views.
746
+ async function editorialPicks(limit = 5) {
747
+ const lim = Math.max(1, Math.min(limit || 5, 8));
748
+ const sel = encodeURIComponent(LIST_COLS.components);
749
+ const url = base + "/rest/v1/components?select=" + sel + "&private=eq.false&premium=eq.false&order=forks.desc,views.desc&limit=" + lim;
750
+ try {
751
+ const { rows } = await fetcher.getJson(url, headers);
752
+ const enriched = await enrichAuthors(rows);
753
+ return enriched.filter((r) => isWellFormedRow("components", r)).map((r) => slimRows("components", [shapeRow("components", r, config, facetsFn)])[0]);
754
+ } catch { return []; }
755
+ }
756
+
757
+ // Bulk fetch: N details in one parallel round (backs aura_bundle + aura_scaffold_page).
758
+ // Limit is 8 end-to-end (matches normRefs validation and the 2–8 docs):
759
+ // duplicates collapse to one fetch, order follows the request, and
760
+ // per-id errors are captured, never thrown: { ok:true, item } or { ok:false, id, error }.
761
+ async function bundleItems(kind, ids, max = 8, opts = {}) {
762
+ const list = [...new Set((ids || []).map(String))].slice(0, Math.max(1, Math.min(max, 8)));
763
+ const out = await Promise.all(list.map(async (id) => {
764
+ try { const got = await getItem(kind, id, { full: !!opts.full }); return { ok: true, id, item: got.item }; }
765
+ catch (e) { return { ok: false, id, error: (e && e.code === "NOT_FOUND") ? "not found" : String((e && e.message) || e).slice(0, 160) }; }
766
+ }));
767
+ return out;
768
+ }
769
+
770
+ // Related: same-tag / same-text overlap, views-ranked, excluding self.
771
+ // ISS10: freeOnly default true — free flows never get Pro recommendations.
772
+ async function relatedItems(kind, idOrSlug, limit = 3, freeOnly = true) {
773
+ const got = await getItem(kind, idOrSlug);
774
+ const item = got.item;
775
+ const toks = queryTokens(item.title + " " + (item.description || "")).slice(0, 3);
776
+ const orParts = toks.map((t) => { const v = ilike(t); return "title.ilike." + v + ",description.ilike." + v; });
777
+ const tagF = (kind === "components" && item.tags && item.tags.length) ? "&tags=cs.{" + String(item.tags[0]).toLowerCase() + "}" : "";
778
+ const lim = Math.max(1, Math.min(limit || 3, 8));
779
+ const sel = LIST_COLS[kind];
780
+ const idCol = (kind === "components") ? "id" : "id";
781
+ const selfId = encodeURIComponent(String(item.id));
782
+ const premF = (freeOnly && (kind === "components" || kind === "assets")) ? "&premium=eq.false" : "";
783
+ const url = base + "/rest/v1/" + kind + "?select=" + encodeURIComponent(sel) + "&private=eq.false" + premF + tagF + "&" + idCol + "=neq." + selfId + (orParts.length ? "&or=(" + orParts.join(",") + ")" : "") + "&order=views.desc&limit=" + lim;
784
+ try {
785
+ const { rows } = await fetcher.getJson(url, headers);
786
+ const enriched = await enrichAuthors(rows);
787
+ const wellFormed = enriched.filter((r) => isWellFormedRow(kind, r));
788
+ return { item: shapeRow(kind, item, config), related: slimRows(kind, wellFormed.map((r) => shapeRow(kind, r, config))) };
789
+ } catch { return { item: shapeRow(kind, item, config), related: [] }; }
790
+ }
791
+
792
+ return { searchCatalog, getItem, getStatus, categoryCounts, bundleItems, relatedItems, editorialPicks };
793
+ }
794
+
795
+ // ---- protocol.mjs ----
796
+ const SERVER_INFO = { name: "aura-components-mcp", version: "1.7.1" };
797
+ // Module: protocol — stdio JSON-RPC framing + MCP handshake + error codes.
798
+ // One interface: createSession(send) -> { dispatch(msg) }. No business logic.
799
+
800
+ const PROTOCOL_VERSION = "2024-11-05";
801
+
802
+ const protocolFn_err = (code, message, data) => ({ code, message, ...(data === undefined ? {} : { data }) });
803
+ const Errors = {
804
+ parse: (m) => protocolFn_err(-32700, "Parse error" + (m ? ": " + m : "")),
805
+ invalidRequest: (m) => protocolFn_err(-32600, "Invalid Request" + (m ? ": " + m : "")),
806
+ methodNotFound: (m) => protocolFn_err(-32601, "Method not found: " + m),
807
+ invalidParams: (m) => protocolFn_err(-32602, "Invalid params" + (m ? ": " + m : "")),
808
+ upstream: (m) => protocolFn_err(-32000, "Upstream error" + (m ? ": " + m : "")),
809
+ notFound: (m) => protocolFn_err(-32004, m || "Not found"),
810
+ };
811
+
812
+ function textResult(obj) {
813
+ return { content: [{ type: "text", text: JSON.stringify(obj, null, 2) }] };
814
+ }
815
+
816
+ function createSession(send, { tools, handlers }) {
817
+ let initialized = false;
818
+ const ok = (id, result) => send({ jsonrpc: "2.0", id, result });
819
+ const fail = (id, e) => send({ jsonrpc: "2.0", id, error: e });
820
+
821
+ async function dispatch(msg) {
822
+ if (!msg || typeof msg !== "object" || msg.jsonrpc !== "2.0") {
823
+ if (msg && "id" in msg && msg.id !== undefined && msg.id !== null) fail(msg.id, Errors.invalidRequest("bad envelope"));
824
+ return;
825
+ }
826
+ const { id, method, params } = msg;
827
+ const isCall = id !== undefined && id !== null;
828
+ try {
829
+ if (method === "initialize") {
830
+ initialized = true;
831
+ if (!isCall) return;
832
+ return ok(id, { protocolVersion: PROTOCOL_VERSION, capabilities: { tools: {} }, serverInfo: SERVER_INFO });
833
+ }
834
+ if (method === "notifications/initialized") return;
835
+ if (method === "ping") { if (isCall) return ok(id, {}); return; }
836
+ if (!initialized) { if (isCall) fail(id, Errors.invalidRequest("not initialized")); return; }
837
+ if (method === "tools/list") { if (isCall) return ok(id, { tools }); return; }
838
+ if (method === "tools/call") {
839
+ if (!isCall) return;
840
+ const name = params?.name;
841
+ const args = params?.arguments ?? {};
842
+ const h = handlers[name];
843
+ if (!h) return fail(id, Errors.methodNotFound("tool " + name));
844
+ try {
845
+ const result = await h(args);
846
+ return ok(id, result);
847
+ } catch (e) {
848
+ if (e && e.code === "NOT_FOUND") return fail(id, Errors.notFound(e.message));
849
+ if (e && e.code === "BAD_ARGS") return fail(id, Errors.invalidParams(e.message));
850
+ return fail(id, Errors.upstream(e?.message ?? String(e)));
851
+ }
852
+ }
853
+ if (isCall) return fail(id, Errors.methodNotFound(method));
854
+ } catch (e) {
855
+ if (isCall) fail(id, Errors.upstream(e?.message ?? String(e)));
856
+ }
857
+ }
858
+
859
+ return { dispatch };
860
+ }
861
+
862
+ // ---- guide.mjs ----
863
+ // Module: guide — small interface (installGuide, tokenHints, detectNeeds)
864
+ // turning raw catalogue rows into paste-ready help: setup steps, file map,
865
+ // dependency list, token starter. Derived from row fields, no extra fetches.
866
+
867
+ const GUIDE_CDN = {
868
+ tailwind: 'https://cdn.tailwindcss.com',
869
+ iconify: 'https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js',
870
+ };
871
+
872
+ // Font allowlist: real families only. Tailwind weight/size utilities (medium, semibold,
873
+ // normal, light-as-weight, size-*) are NOT families — old regex leaked them into fonts[].
874
+ const KNOWN_FONTS = ['inter', 'geist', 'manrope', 'sora', 'space-grotesk', 'space-mono', 'poppins', 'montserrat', 'playfair-display', 'instrument-serif', 'dm-sans', 'dm-serif-display', 'jetbrains-mono', 'ibm-plex-mono', 'ibm-plex-serif', 'plus-jakarta-sans', 'bricolage-grotesque', 'newsreader', 'fraunces', 'lora', 'merriweather', 'libre-baskerville', 'cormorant-garamond', 'gfs-didot', 'bodoni-moda', 'cardo', 'marcellus', 'gloock', 'aboreto', 'syne', 'urbanist', 'sora', 'anton', 'bebas-neue', 'archivo-black', 'league-gothic', 'fredoka', 'chewy', 'foldit', 'oi', 'honk', 'nabla', 'quicksand', 'nunito', 'work-sans', 'oswald', 'lora', 'roboto', 'open-sans', 'figtree', 'outfit', 'lexend', 'gloria-hallelujah', 'caveat'];
875
+ const FONT_RE = /font-(?:family-)?\[?['"]?([a-z][a-z0-9-]*)/g;
876
+
877
+ function detectNeeds(code, tags) {
878
+ const c = String(code || '').toLowerCase();
879
+ const t = new Set((tags || []).map((x) => String(x).toLowerCase()));
880
+ const needsTailwind = c.includes('class=') || t.has('tailwind');
881
+ // Probes must work on a 2k prefix too: icon svgs usually appear in the first screen of markup.
882
+ // svg alone under-matches in lists (no code fetched) — but must not over-claim either:
883
+ // 'svg' counts only with an icon-ish neighbor (iconify, <svg, lucide, heroicons, feather).
884
+ const needsIcons = c.includes('iconify') || c.includes('<svg') || c.includes('lucide') || c.includes('heroicon') || c.includes('data-lucide');
885
+ const needsKeyframes = c.includes('keyframes') || c.includes('animation:');
886
+ const fonts = new Set();
887
+ for (const m of c.matchAll(FONT_RE)) {
888
+ const fam = m[1].replace(/['"\]]/g, '');
889
+ if (KNOWN_FONTS.includes(fam)) fonts.add(fam);
890
+ }
891
+ // font-family: 'X', Y declarations (not utility classes)
892
+ for (const m of c.matchAll(/font-family\s*:\s*([^;}]{1,80})/g)) {
893
+ for (const part of m[1].split(',')) {
894
+ const fam = part.trim().replace(/['"]/g, '').toLowerCase().replace(/\s+/g, '-');
895
+ if (KNOWN_FONTS.includes(fam)) fonts.add(fam);
896
+ }
897
+ }
898
+ return { needsTailwind, needsIcons, needsKeyframes, fonts: [...fonts].slice(0, 6) };
899
+ };
900
+
901
+ // Facets: cheap derived signals so search lists answer "dark? heavy? pro?" without a get.
902
+ // theme: dark|light|mixed|unknown from background field + code palette probes.
903
+ // weight: code_chars bucket (s/m/l) so agents can prefer light embeds.
904
+ function facets(kind, row, opts) {
905
+ const f = {};
906
+ if (kind === 'components') {
907
+ const bg = String(row.background || '').toLowerCase();
908
+ const tags = (row.tags || []).map((x) => String(x).toLowerCase());
909
+ const code = String(row.code || '').toLowerCase();
910
+ const tagDark = tags.includes('dark');
911
+ const tagLight = tags.includes('light');
912
+ const darkHits = (code.match(/#0{3,6}\b|#1[0-9a-f]{5}\b|bg-black|bg-neutral-9|bg-zinc-9|bg-slate-9|text-white|slate-300/g) || []).length;
913
+ const lightHits = (code.match(/bg-white|bg-neutral-50|bg-slate-50|bg-gray-50|text-black|text-neutral-9/g) || []).length;
914
+ const bgDark = bg.includes('000') && !bg.includes('fff');
915
+ const bgLight = bg.includes('fff') && !bg.includes('000');
916
+ f.theme = bgDark || tagDark ? 'dark' : (bgLight || tagLight ? 'light' : (code ? (darkHits > lightHits * 2 ? 'dark' : (lightHits > darkHits * 2 ? 'light' : (darkHits || lightHits ? 'mixed' : 'unknown'))) : (tags.includes('saas') || tags.includes('minimal') ? 'light' : 'unknown')));
917
+ const n = (opts && opts.fullLength) || String(row.code || '').length;
918
+ f.weight = n > 20000 ? 'l' : (n > 8000 ? 'm' : 's');
919
+ f.code_chars = n;
920
+ const needs = detectNeeds(row.code || '', row.tags || []);
921
+ f.needsTailwind = needs.needsTailwind;
922
+ f.needsIcons = needs.needsIcons;
923
+ f.fonts = needs.fonts;
924
+ f.license = 'all-rights-reserved (Aura Terms §4) — check page_url before commercial reuse';
925
+ }
926
+ // ISS14 lives here too: license is top-level on every asset row, not buried.
927
+ // ISS13: rewrite hotlinked demo images to stable asset references where possible.
928
+ // i.pravatar.cc + bare demo URLs 404 in production; flag them in the install plan.
929
+ if (kind === 'components' && typeof row.code === 'string') {
930
+ const hot = [...row.code.matchAll(/<img[^>]+src=["']([^"']+)["']/gi)].map((m) => m[1]).filter((u) => /pravatar|placehold|dummyimage|unsplash\/photos\/..\/download|example\.com/i.test(u));
931
+ if (hot.length) row = { ...row, _hotlinked: [...new Set(hot)].slice(0, 5) };
932
+ }
933
+ if (kind === 'assets' || row.image_800w || row.image_original || row.video_url) {
934
+ // Resolved 2026-09-13 from primary source https://www.aura.build/terms §4-5:
935
+ // catalogue content is the exclusive property of DESIGNCODE IO PTE. LTD.;
936
+ // no per-asset license column exists, so commercial reuse needs Aura's permission.
937
+ f.license = 'all-rights-reserved (Aura Terms §4: DESIGNCODE IO PTE. LTD.) — personal/preview use via page_url; commercial reuse needs Aura permission (support@designcode.io)';
938
+ // Gap 5: machine-readable license gate. commercial_ok:false always (no per-asset
939
+ // column exists to prove otherwise) — agents filter without parsing prose.
940
+ f.commercial_ok = false;
941
+ f.terms_url = 'https://www.aura.build/terms';
942
+ f.download = row.image_original || row.image_1600w || row.image_800w || row.video_url || null;
943
+ f.preview = row.image_800w || row.video_poster_url || null;
944
+ }
945
+ if (kind === 'design_systems') {
946
+ // ISS4: lists never fetch preview_html (24KB/row). has_preview:true is authoritative
947
+ // only on detail; on lists report 'unknown-list' so agents fetch instead of trusting false.
948
+ f.has_preview = row.preview_html !== undefined ? Boolean(row.preview_html) : 'unknown-list';
949
+ }
950
+ return f;
951
+ }
952
+
953
+ function installGuide(kind, row) {
954
+ const steps = [];
955
+ const files = [];
956
+ const deps = [];
957
+ const url = row.page_url || '';
958
+ if (kind === 'components') {
959
+ const needs = detectNeeds(row.code || '', row.tags || []);
960
+ if (needs.needsTailwind) {
961
+ deps.push({ name: 'tailwindcss', via: 'CDN or project setup', cdn: GUIDE_CDN.tailwind });
962
+ steps.push('Add the Tailwind CDN script to head for a quick preview, or paste the markup into a Tailwind project.');
963
+ }
964
+ if (needs.needsIcons) {
965
+ deps.push({ name: 'iconify-icon', via: 'CDN', cdn: GUIDE_CDN.iconify });
966
+ steps.push('Add the iconify-icon CDN script so the icon tags render.');
967
+ }
968
+ if (needs.needsKeyframes) steps.push('Keep the embedded style block with the markup: that is where the keyframes live.');
969
+ // ISS11+12: pagination + CSS scoping. Code ships truncated at codeChars; the scope
970
+ // prefix below prevents .button/.inner/.point collisions in multi-component scaffolds.
971
+ const scope = 'aura-' + String(row.slug || row.id).toLowerCase().replace(/[^a-z0-9]+/g, '');
972
+ const fullLen = String(row.code || '').length;
973
+ const isTrunc = /…\[truncated\]$/.test(String(row.code || ''));
974
+ if (isTrunc) {
975
+ files.push({ path: 'components/' + (row.slug || row.id) + '.part1.html', contains: 'markup part 1 of N — fetch aura_get_component again and concatenate code chunks' });
976
+ steps.push('Code is paginated (' + fullLen + ' chars shown of more): install part files in order, or re-fetch for the full body.');
977
+ } else {
978
+ files.push({ path: 'components/' + (row.slug || row.id) + '.html', contains: 'section markup plus its style block' });
979
+ }
980
+ steps.push('Scope global CSS before pasting: prefix bare selectors (.button, .inner, .point) with .' + scope + ' to avoid collisions in multi-component pages.');
981
+ steps.push('Paste the markup where the section belongs and update the CTA link (it ships as #).');
982
+ if (url) steps.push('Preview first: ' + url);
983
+ if (row._hotlinked && row._hotlinked.length) steps.push('Replace demo images before shipping (' + row._hotlinked.slice(0, 3).join(', ') + '): hotlinked placeholders 404 in production — swap for aura_search_assets results.');
984
+ if (needs.fonts.length) files.push({ path: 'styles/fonts.css', contains: 'font families referenced: ' + needs.fonts.join(', ') });
985
+ // Chunk concat helper: truncated installs carry the exact re-fetch plan so no
986
+ // agent is left guessing how part files map to chunks.
987
+ const ci = row.code_info || null;
988
+ const concat = isTrunc ? {
989
+ tool: 'aura_get_component',
990
+ id: row.slug || row.id,
991
+ chunks: (ci && ci.chunks) || 'unknown — call once with chunk:0 and read code_info.chunks',
992
+ how: 'call aura_get_component with chunk:0..chunks-1 and concatenate code in order; part files above map 1:1 to chunks',
993
+ } : null;
994
+ const css_scope_how = 'in the pasted <style>, rewrite every bare class selector as .' + scope + ' <selector> (e.g. .button -> .' + scope + ' .button); leave Tailwind utilities, :root vars, and @keyframes names untouched';
995
+ return { kind, title: row.title, page_url: url, free: row.premium === false, steps, deps, files, needs, css_scope: scope, css_scope_how, code_truncated: isTrunc, ...(concat ? { concat } : {}) };
996
+ }
997
+ if (kind === 'skills') {
998
+ steps.push('Read the SKILL.md content from aura_get_skill first: it names its own triggers and pitfalls.');
999
+ steps.push('Save it as SKILL.md inside your agent skills folder so the agent can load it.');
1000
+ if (row.source_url) steps.push('Upstream source: ' + row.source_url);
1001
+ return { kind, title: row.title, page_url: url, steps, deps, files: [{ path: 'skills/' + row.id + '/SKILL.md', contains: 'full skill content' }] };
1002
+ }
1003
+ if (kind === 'design_systems') {
1004
+ // ISS6: derive preview deps from preview_html so agents don't ship static pages.
1005
+ const ph = String(row.preview_html || '');
1006
+ const pl = ph.toLowerCase();
1007
+ const has = (...ss) => ss.some((s) => pl.includes(s));
1008
+ if (has('cdn.tailwindcss.com', 'tailwind')) deps.push({ name: 'tailwindcss', via: 'CDN (in preview_html)', cdn: GUIDE_CDN.tailwind });
1009
+ if (has('iconify', '<svg', 'lucide', 'data-lucide')) deps.push({ name: 'iconify-icon', via: 'CDN (in preview_html)', cdn: GUIDE_CDN.iconify });
1010
+ if (has('gsap')) deps.push({ name: 'gsap', via: 'CDN (in preview_html)', cdn: 'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js' });
1011
+ if (has('scrolltrigger')) deps.push({ name: 'gsap-ScrollTrigger', via: 'CDN (in preview_html)', cdn: 'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js' });
1012
+ const fonts = new Set();
1013
+ for (const m of pl.matchAll(/family=([a-z+]+)/g)) fonts.add(m[1].replace(/\+/g, '-'));
1014
+ if (fonts.size) deps.push({ name: 'google-fonts:' + [...fonts].slice(0, 4).join(','), via: 'link (in preview_html)' });
1015
+ steps.push('Copy the DESIGN.md content into your repo as DESIGN.md and treat it as the source of truth.');
1016
+ steps.push('Apply the color, type, and spacing tokens before copying any component markup.');
1017
+ if (deps.length) steps.push('Install preview deps first (' + deps.map((d) => d.name).join(', ') + ') — the preview_html needs them.');
1018
+ return { kind, title: row.title, page_url: url, steps, deps, files: [{ path: 'DESIGN.md', contains: 'tokens and rules' }] };
1019
+ }
1020
+ steps.push('Use the image_800w URL for previews and image_original for production.');
1021
+ return { kind, title: row.title, page_url: url, steps, deps, files };
1022
+ };
1023
+
1024
+ // Scaffold merge: one ordered page from a system + component rows. Owns the
1025
+ // dep-dedupe and file-collection rules so the scaffold handler stays a thin
1026
+ // adapter (fetch + freeOnly policy) and tests cross this seam, not the wire.
1027
+ function combineScaffold(sysItem, comps) {
1028
+ const guides = (comps || []).map((c) => installGuide('components', c));
1029
+ const deps = []; const seen = new Set();
1030
+ for (const g of guides) for (const d of (g.deps || [])) if (!seen.has(d.name)) { seen.add(d.name); deps.push(d); }
1031
+ const files = [{ path: 'styles/tokens.css', contains: 'tokens.css from use_design_system' }];
1032
+ if (sysItem) files.push({ path: 'DESIGN.md', contains: 'system rules' });
1033
+ for (const g of guides) for (const f of (g.files || [])) files.push(f);
1034
+ return { guides, combined_deps: deps, files };
1035
+ }
1036
+
1037
+ // ISS5: full token surface — colors + typography + spacing + radius — so scaffolded
1038
+ // pages don't lose the system. Also detects preview dark/light mode so agents don't
1039
+ // apply light-cream tokens to a dark preview body.
1040
+ function previewMode(previewHtml) {
1041
+ const h = String(previewHtml || '').toLowerCase();
1042
+ if (/bg-black|bg-neutral-950|bg-zinc-950|bg-slate-950|bg-\[#0/.test(h)) return 'dark';
1043
+ if (/bg-white|bg-neutral-50|bg-slate-50/.test(h)) return 'light';
1044
+ return 'unknown';
1045
+ }
1046
+
1047
+ function tokenHints(content, previewHtml) {
1048
+ const Q = String.fromCharCode(39);
1049
+ const lines = String(content || '').split(Q + 'n' === Q + 'n' ? '\n' : '\n');
1050
+ const get = (key) => {
1051
+ const line = lines.find((l) => { const t = l.trim().toLowerCase(); return t === key + ':' || t.startsWith(key + ': ') || t.startsWith(key + ' :') || t.startsWith(key + ':\"') || t.startsWith(key + ":'"); });
1052
+ if (!line) return null;
1053
+ const t = line.trim();
1054
+ const ci = t.toLowerCase().indexOf(key);
1055
+ let v = t.slice(ci + key.length).trim();
1056
+ if (v.startsWith(':')) v = v.slice(1).trim();
1057
+ while (v.startsWith(Q) || v.startsWith('"')) v = v.slice(1);
1058
+ while (v.endsWith(Q) || v.endsWith('"')) v = v.slice(0, -1);
1059
+ return v.trim() || null;
1060
+ };
1061
+ const tokens = {
1062
+ primary: get('primary'),
1063
+ secondary: get('secondary'),
1064
+ accent: get('accent'),
1065
+ background: get('background'),
1066
+ surface: get('surface'),
1067
+ text_primary: get('text-primary') || get('text_primary'),
1068
+ text_secondary: get('text-secondary') || get('text_secondary'),
1069
+ border: get('border'),
1070
+ display_font: get('display_lg_fontfamily') || get('display-font'),
1071
+ body_font: get('body_md_fontfamily') || get('body-font'),
1072
+ mono_font: get('label_md_fontfamily') || get('mono-font'),
1073
+ section_padding: get('section-padding') || get('section_padding'),
1074
+ card_padding: get('card-padding') || get('card_padding'),
1075
+ card_radius: get('card') === null ? get('card_radius') : null,
1076
+ control_radius: get('control'),
1077
+ pill_radius: get('pill'),
1078
+ };
1079
+ const pick = (obj, keys) => { const o = {}; for (const k of keys) if (obj[k]) o[k] = obj[k]; return o; };
1080
+ const colors = pick(tokens, ['primary', 'secondary', 'accent', 'background', 'surface', 'text_primary', 'text_secondary', 'border']);
1081
+ const rows = [];
1082
+ for (const [k, v] of Object.entries(colors)) rows.push(' --aura-' + k.replace(/_/g, '-') + ': ' + v + ';');
1083
+ if (tokens.body_font) rows.push(' --aura-font-body: ' + tokens.body_font + ';');
1084
+ if (tokens.display_font) rows.push(' --aura-font-display: ' + tokens.display_font + ';');
1085
+ if (tokens.mono_font) rows.push(' --aura-font-mono: ' + tokens.mono_font + ';');
1086
+ if (tokens.section_padding) rows.push(' --aura-section-padding: ' + tokens.section_padding + ';');
1087
+ if (tokens.card_padding) rows.push(' --aura-card-padding: ' + tokens.card_padding + ';');
1088
+ if (tokens.card_radius) rows.push(' --aura-radius-card: ' + tokens.card_radius + ';');
1089
+ if (tokens.control_radius) rows.push(' --aura-radius-control: ' + tokens.control_radius + ';');
1090
+ if (tokens.pill_radius) rows.push(' --aura-radius-pill: ' + tokens.pill_radius + ';');
1091
+ const css = rows.length ? ':root{' + '\n' + rows.join('\n') + '\n}' : '';
1092
+ const mode = previewMode(previewHtml);
1093
+ const warn = (mode === 'dark' && tokens.background && !/000|0f1115|111827|1a1a1a|09090b/i.test(tokens.background))
1094
+ ? 'tokens say light background ' + tokens.background + ' but preview_html renders dark — follow the preview body, not the tokens, or ask which mode is wanted'
1095
+ : ((mode === 'light' && tokens.background && /000000|0f1115/i.test(tokens.background))
1096
+ ? 'tokens say dark background but preview renders light — follow the preview body'
1097
+ : null);
1098
+ return { tokens, css, ...(mode === 'unknown' ? {} : { preview_mode: mode }), mode_warning: warn };
1099
+ }
1100
+
1101
+ // Module: prompt — deep module, small interface (buildPrompt) over all
1102
+ // need-based prompt rules. Pure, no fetch: same seam for tool + tests + docs.
1103
+ // Pass 1 (no answers): draft + questions[<=4] + next_call. Agent MUST ask the
1104
+ // user via native question UI before Pass 2. Pass 2 (with answers): final.
1105
+ const PROMPT_SOURCES = [
1106
+ 'https://www.aura.build/learn/tips-for-prompting',
1107
+ 'https://www.aura.build/learn/introduction',
1108
+ 'https://www.aura.build/design-systems',
1109
+ ];
1110
+ const PROMPT_CREDIT = 'Built by swastiksingh-dev — catalogue by Aura (aura.build)';
1111
+ const PROMPT_ORDER = ['1 tokens.css', '2 system preview_html', '3 component markup in listed order'];
1112
+
1113
+ function promptThemeHint(goal, explicit) {
1114
+ if (explicit === 'dark' || explicit === 'light') return explicit;
1115
+ const t = String(goal || '').toLowerCase();
1116
+ if (/\bdark\b|\bmidnight\b|\bnoir\b|\bcinematic\b/.test(t) && !/\blight\b|\bbright\b|\bairy\b/.test(t)) return 'dark';
1117
+ if (/\blight\b|\bbright\b|\bairy\b/.test(t)) return 'light';
1118
+ return null;
1119
+ }
1120
+
1121
+ function draftPromptText(goal, theme) {
1122
+ const th = theme ? theme + ' theme' : 'theme to confirm with user';
1123
+ return 'Build: ' + goal + ' (' + th + '). Use Tailwind CSS with container mx-auto px-4, hex palette, numeric color scale 50-900, 8px radius, 768px hamburger breakpoint, hover scale 1.05 + shadow. Typography: Inter 40-60 / 28-36 / 14-16, weights 640/560/460. Motion <500ms + prefers-reduced-motion. Tokens first (DESIGN.md), then markup. Confirm theme/sections/system with user before scaffolding.';
1124
+ }
1125
+
1126
+ function finalPromptText(goal, a) {
1127
+ const theme = a.theme || 'confirm with user';
1128
+ const sections = (a.sections && a.sections.length ? a.sections.join(', ') : 'hero + 2 sections');
1129
+ const system = a.system || 'auto-pick via aura_search_design_systems';
1130
+ const assets = a.assets || 'only if needed via aura_search_assets';
1131
+ return 'GOAL: ' + goal + '\n'
1132
+ + 'THEME: ' + theme + ' (tokens first, follow preview body on mismatch)\n'
1133
+ + 'SECTIONS: ' + sections + '\n'
1134
+ + 'DESIGN SYSTEM: ' + system + ' — copy DESIGN.md as source of truth, apply color/type/spacing before markup\n'
1135
+ + 'ASSETS: ' + assets + '\n'
1136
+ + 'RULES: Tailwind container mx-auto px-4; hex palette 50-900; radius 8px; breakpoints 1024+/768-1023/<768, 44px touch targets; hover scale 1.05 + shadow; Inter 40-48 H1 / 16 body; motion <500ms + prefers-reduced-motion; vanilla HTML/Tailwind/JS, no proprietary runtime\n'
1137
+ + 'ORDER: 1 tokens.css, 2 system preview_html, 3 component markup\n'
1138
+ + 'CALLS: aura_recommend -> aura_get_* finalists -> aura_use_design_system -> aura_scaffold_page';
1139
+ }
1140
+
1141
+ function buildPrompt(input = {}) {
1142
+ const goal = typeof input.goal === 'string' ? input.goal.trim() : '';
1143
+ if (!goal) { const e = new Error('goal (string) is required'); e.code = 'BAD_ARGS'; throw e; }
1144
+ const themeIn = input.theme === 'dark' || input.theme === 'light' ? input.theme : undefined;
1145
+ if (input.theme !== undefined && themeIn === undefined) { const e = new Error('theme must be dark|light'); e.code = 'BAD_ARGS'; throw e; }
1146
+ const theme = promptThemeHint(goal, themeIn);
1147
+ const system = typeof input.system === 'string' && input.system.trim() ? input.system.trim() : null;
1148
+ const surfaces = Array.isArray(input.surfaces) ? input.surfaces.filter((s) => typeof s === 'string').slice(0, 4) : [];
1149
+ const answers = input.answers && typeof input.answers === 'object' && !Array.isArray(input.answers) ? input.answers : null;
1150
+
1151
+ if (!answers) {
1152
+ const questions = [
1153
+ { key: 'theme', question: 'Which theme — dark or light?', options: ['dark', 'light'] },
1154
+ { key: 'sections', question: 'Which sections (e.g. hero, pricing, testimonial, footer)?', options: [] },
1155
+ { key: 'system', question: 'Which DESIGN system, or auto-pick?', options: system ? [system, 'auto-pick'] : ['auto-pick'] },
1156
+ { key: 'assets', question: 'Need images/video?', options: ['no', 'images', 'video'] },
1157
+ { key: 'macrostructure', question: 'Which page macrostructure (e.g. bento-grid, marquee-hero, long-document), or auto-pick from Inspo evidence?', options: ['auto-pick', 'bento-grid', 'marquee-hero', 'long-document', 'photographic', 'stat-led'] },
1158
+ { key: 'vibe', question: 'Which mood — calm, serious, luxe, playful, technical?', options: ['calm', 'serious', 'luxe', 'playful', 'technical', 'warm'] },
1159
+ ];
1160
+ return {
1161
+ phase: 'draft',
1162
+ goal,
1163
+ theme_hint: theme,
1164
+ draft_prompt: draftPromptText(goal, theme),
1165
+ questions,
1166
+ missing: ['theme', 'sections', 'system', 'assets', 'macrostructure', 'vibe'],
1167
+ next_call: { tool: 'aura_prompt', arguments: { goal, answers: { theme: theme || 'dark', sections: [], system: system || 'auto-pick', assets: 'no', macrostructure: 'auto-pick', vibe: 'calm' } } },
1168
+ plan_order: PROMPT_ORDER,
1169
+ sources: PROMPT_SOURCES,
1170
+ credit: PROMPT_CREDIT,
1171
+ inspo_axes: { mode_maps_to: 'theme', macrostructures: INSPO_MACROS.slice(0, 6), note: 'Run inspo.recommend first for evidence, then aura_inspo_recommend to map it onto Aura calls. Mode folds into theme.' },
1172
+ note: 'Ask the user these questions via native question UI, then call aura_prompt again with goal + answers. Never skip confirmation.',
1173
+ };
1174
+ }
1175
+
1176
+ const a = {
1177
+ theme: answers.theme === 'dark' || answers.theme === 'light' ? answers.theme : (theme || 'dark'),
1178
+ sections: Array.isArray(answers.sections) ? answers.sections.filter((s) => typeof s === 'string').slice(0, 6) : [],
1179
+ system: typeof answers.system === 'string' && answers.system.trim() ? answers.system.trim() : (system || 'auto-pick'),
1180
+ assets: typeof answers.assets === 'string' ? answers.assets : 'no',
1181
+ macrostructure: typeof answers.macrostructure === 'string' && answers.macrostructure.trim() ? answers.macrostructure.trim() : 'auto-pick',
1182
+ vibe: typeof answers.vibe === 'string' && answers.vibe.trim() ? answers.vibe.trim() : 'calm',
1183
+ };
1184
+ return {
1185
+ phase: 'final',
1186
+ goal,
1187
+ answers: a,
1188
+ final_prompt: finalPromptText(goal, a),
1189
+ plan_order: PROMPT_ORDER,
1190
+ suggested_calls: [
1191
+ { tool: 'inspo.recommend', arguments: { brief: goal, mode: a.theme, vibe: a.vibe, ...(a.macrostructure !== 'auto-pick' ? { macrostructure: a.macrostructure } : {}) } },
1192
+ { tool: 'aura_inspo_recommend', arguments: { brief: goal, mode: a.theme, vibe: a.vibe, ...(a.macrostructure !== 'auto-pick' ? { macrostructure: a.macrostructure } : {}) } },
1193
+ { tool: 'aura_recommend', arguments: { goal } },
1194
+ { tool: 'aura_search_all', arguments: { query: goal, limit: 3 } },
1195
+ { tool: 'aura_use_design_system', arguments: a.system !== 'auto-pick' ? { slug: a.system } : { slug: 'auto-pick: search first' } },
1196
+ { tool: 'aura_scaffold_page', arguments: { goal, system: a.system } },
1197
+ ],
1198
+ page_urls: ['https://www.aura.build/design-systems', 'https://www.aura.build/components'],
1199
+ sources: PROMPT_SOURCES,
1200
+ surfaces: surfaces.length ? surfaces : ['components', 'design_systems', 'assets', 'skills'],
1201
+ credit: PROMPT_CREDIT,
1202
+ };
1203
+ };
1204
+
1205
+ // Module: inspo-mappers — pure zero-dep translators between Inspo evidence and
1206
+ // Aura calls. No fetch: the agent pastes slim Inspo evidence in, gets Aura call
1207
+ // plans out. Same seam for tools + tests + docs (mirrors buildPrompt).
1208
+ const INSPO_MACROS = ['bento-grid', 'long-document', 'marquee-hero', 'stat-led', 'conversational-faq', 'manifesto', 'photographic', 'specimen', 'catalogue', 'letter', 'index-first', 'narrative-workflow', 'split-studio', 'feature-stack', 'type-specimen', 'portfolio-grid', 'map-diagram', 'ecosystem-index', 'component-playground'];
1209
+ const INSPO_VIBES = ['calm', 'loud', 'playful', 'serious', 'luxe', 'raw', 'soft', 'technical', 'warm', 'cold'];
1210
+ const INSPO_MODES = ['light', 'dark'];
1211
+ const INSPO_COMP_TYPES = ['hero', 'pricing', 'features', 'cta', 'nav', 'footer', 'testimonial', 'logo-cloud', 'faq', 'stat'];
1212
+ const INSPO_STYLES = ['minimalism', 'editorial', 'brutalism', 'glassmorphism', 'swiss', 'maximalism', 'playful', 'dark-mode', 'monochrome', 'vintage', 'futurist'];
1213
+ const INSPO_MACRO_TO_CATEGORY = {
1214
+ 'bento-grid': 'feature', 'long-document': 'section', 'marquee-hero': 'hero',
1215
+ 'stat-led': 'feature', 'conversational-faq': 'form', 'manifesto': 'heading',
1216
+ 'photographic': 'background', 'specimen': 'card', 'catalogue': 'card',
1217
+ 'letter': 'heading', 'index-first': 'header', 'narrative-workflow': 'section',
1218
+ 'split-studio': 'hero', 'feature-stack': 'feature', 'type-specimen': 'heading',
1219
+ 'portfolio-grid': 'card', 'map-diagram': 'feature', 'ecosystem-index': 'section',
1220
+ 'component-playground': 'section',
1221
+ };
1222
+ const INSPO_COMP_TO_CATEGORY = {
1223
+ hero: 'hero', pricing: 'pricing', features: 'feature', cta: 'section',
1224
+ nav: 'header', footer: 'footer', testimonial: 'testimonial',
1225
+ 'logo-cloud': 'logo', faq: 'form', stat: 'feature',
1226
+ };
1227
+ const INSPO_LICENSE = 'Reference JSX is Inspo-canonical (copy-pasteable, tweak tokens to the brand). Aura markup/assets are all-rights-reserved per Aura Terms §4 — fine to prototype, check each page_url before commercial ship.';
1228
+ const INSPO_SLIM_KEYS = ['slug', 'palette', 'fonts', 'macro', 'macrostructure', 'mode', 'vibe', 'style', 'type', 'id', 'brief', 'brand_hex', 'background', 'text'];
1229
+ // Palette priority (locked): brand hex > Inspo evidence > Aura tokens. Aura keeps structure/spacing.
1230
+ const INSPO_ORDER = ['1 tokens.css', '2 system preview_html', '3 component markup in listed order'];
1231
+ const INSPO_IMAGE_PLAN = { draft: 'image_800w', prod: 'image_original', srcset: [320, 800, 1600], alt_from: 'title', swap_hotlinked: ['pravatar', 'placehold', 'dummyimage', 'example.com'], weight_cap: 's|m only — full:true on the winner alone' };
1232
+
1233
+ function inspoBad(msg) { const e = new Error(msg); e.code = 'BAD_ARGS'; throw e; }
1234
+ function inspoHex(v, name) {
1235
+ if (v === undefined) return undefined;
1236
+ if (typeof v !== 'string' || !/^#?[0-9a-f]{3}([0-9a-f]{3})?$|^#?[0-9a-f]{6}$/i.test(v.trim())) inspoBad(name + ' must be a hex color like "#c7402f"');
1237
+ const h = v.trim().replace(/^#/, '');
1238
+ return '#' + (h.length === 3 ? h.split('').map((c) => c + c).join('') : h).toLowerCase();
1239
+ }
1240
+ function inspoLum(hex) {
1241
+ const n = parseInt(hex.slice(1), 16);
1242
+ const f = (c) => { const s = c / 255; return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4); };
1243
+ return 0.2126 * f((n >> 16) & 255) + 0.7152 * f((n >> 8) & 255) + 0.0722 * f(n & 255);
1244
+ }
1245
+ function inspoContrastWarn(bg, text) {
1246
+ if (!bg || !text) return null;
1247
+ const l1 = inspoLum(bg), l2 = inspoLum(text);
1248
+ const ratio = (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
1249
+ return ratio < 4.5 ? 'contrast ' + ratio.toFixed(2) + ':1 (' + bg + ' on ' + text + ') is below 4.5 — ask the user which side moves' : null;
1250
+ }
1251
+ function inspoSlimCheck(ev, name) {
1252
+ if (!ev || typeof ev !== 'object' || Array.isArray(ev)) inspoBad(name + ' must be an object with slim keys only: ' + INSPO_SLIM_KEYS.join(','));
1253
+ for (const k of Object.keys(ev)) {
1254
+ if (!INSPO_SLIM_KEYS.includes(k)) inspoBad(name + '.' + k + ' is not slim — pass slugs+palette+fonts+macro+mode+vibe only, no autopsy/screenshots');
1255
+ }
1256
+ return ev;
1257
+ }
1258
+
1259
+ function buildInspoRecommend(input = {}) {
1260
+ const brief = typeof input.brief === 'string' ? input.brief.trim() : '';
1261
+ if (!brief) inspoBad('brief (string) is required');
1262
+ const mode = input.mode === undefined ? undefined : (INSPO_MODES.includes(input.mode) ? input.mode : inspoBad('mode must be light|dark'));
1263
+ const vibe = input.vibe === undefined ? undefined : (INSPO_VIBES.includes(input.vibe) ? input.vibe : inspoBad('vibe must be one of ' + INSPO_VIBES.join('|')));
1264
+ const macro = input.macrostructure === undefined ? undefined : (INSPO_MACROS.includes(input.macrostructure) ? input.macrostructure : inspoBad('macrostructure must be one of ' + INSPO_MACROS.join('|')));
1265
+ const brand = inspoHex(input.brand_hex, 'brand_hex');
1266
+ // Strict macro gate (locked): no macro on file means run Inspo first — never scaffold generic.
1267
+ if (!macro) {
1268
+ return {
1269
+ need_macro: true,
1270
+ brief, mode: mode || null, vibe: vibe || null, brand_hex: brand || null,
1271
+ inspo_call: { tool: 'inspo.recommend', arguments: { brief, ...(mode ? { mode } : {}), ...(vibe ? { vibe } : {}) } },
1272
+ note: 'No macrostructure yet — call inspo.recommend first, then call aura_inspo_recommend again with its macrostructure. Never scaffold without a macro.',
1273
+ palette_priority: ['brand_hex', 'inspo_evidence', 'aura_tokens'],
1274
+ order: INSPO_ORDER,
1275
+ license_note: INSPO_LICENSE,
1276
+ };
1277
+ }
1278
+ const category = INSPO_MACRO_TO_CATEGORY[macro];
1279
+ return {
1280
+ need_macro: false,
1281
+ brief, mode: mode || null, vibe: vibe || null, brand_hex: brand || null,
1282
+ macrostructure: macro, aura_category: category,
1283
+ inspo_call: { tool: 'inspo.recommend', arguments: { brief, macrostructure: macro, ...(mode ? { mode } : {}), ...(vibe ? { vibe } : {}) } },
1284
+ aura_calls: [
1285
+ { tool: 'aura_search_all', arguments: { query: brief, limit: 3, ...(mode ? { theme: mode } : {}) } },
1286
+ { tool: 'aura_recommend', arguments: { goal: brief, ...(mode ? { theme: mode } : {}) } },
1287
+ { tool: 'aura_search_components', arguments: { query: brief, category, ...(mode ? { theme: mode } : {}) } },
1288
+ ],
1289
+ palette_priority: ['brand_hex', 'inspo_evidence', 'aura_tokens'],
1290
+ image_plan: INSPO_IMAGE_PLAN,
1291
+ order: INSPO_ORDER,
1292
+ next: { tool: 'aura_inspo_components', arguments: { evidence: { type: 'hero', macrostructure: macro, ...(mode ? { mode } : {}) }, brief } },
1293
+ license_note: INSPO_LICENSE,
1294
+ };
1295
+ }
1296
+
1297
+ function buildInspoComponents(input = {}) {
1298
+ const ev = input.evidence && typeof input.evidence === 'object' && !Array.isArray(input.evidence) ? input.evidence : inspoBad('evidence {type, ...} is required');
1299
+ if (!INSPO_COMP_TYPES.includes(ev.type)) inspoBad('evidence.type must be one of ' + INSPO_COMP_TYPES.join('|'));
1300
+ if (ev.style !== undefined && !INSPO_STYLES.includes(ev.style)) inspoBad('evidence.style must be one of ' + INSPO_STYLES.join('|'));
1301
+ if (ev.macrostructure !== undefined && !INSPO_MACROS.includes(ev.macrostructure)) inspoBad('evidence.macrostructure must be one of ' + INSPO_MACROS.join('|'));
1302
+ if (ev.mode !== undefined && !INSPO_MODES.includes(ev.mode)) inspoBad('evidence.mode must be light|dark');
1303
+ for (const k of Object.keys(ev)) {
1304
+ if (!['type', 'style', 'macrostructure', 'mode', 'macro', 'vibe'].includes(k)) inspoBad('evidence.' + k + ' is not slim — type/style/macrostructure/mode only');
1305
+ }
1306
+ const brief = typeof input.brief === 'string' && input.brief.trim() ? input.brief.trim() : ev.type;
1307
+ const category = INSPO_COMP_TO_CATEGORY[ev.type];
1308
+ return {
1309
+ evidence: { ...ev },
1310
+ aura_category: category,
1311
+ inspo_call: { tool: 'inspo.find_components', arguments: { ...ev } },
1312
+ aura_search: { tool: 'aura_search_components', arguments: { query: brief, category, ...(ev.mode ? { theme: ev.mode } : {}) } },
1313
+ scaffold_hint: { order: INSPO_ORDER, weight_cap: 's|m only', tokens_first: true },
1314
+ image_plan: INSPO_IMAGE_PLAN,
1315
+ license_note: INSPO_LICENSE,
1316
+ };
1317
+ }
1318
+
1319
+ function buildInspoReference(input = {}) {
1320
+ const ev = input.evidence && typeof input.evidence === 'object' && !Array.isArray(input.evidence) ? input.evidence : inspoBad('evidence {type, id} is required');
1321
+ if (!INSPO_COMP_TYPES.includes(ev.type)) inspoBad('evidence.type must be one of ' + INSPO_COMP_TYPES.join('|'));
1322
+ if (typeof ev.id !== 'string' || !ev.id.trim()) inspoBad('evidence.id (string) is required');
1323
+ const ac = input.aura_component && typeof input.aura_component === 'object' && !Array.isArray(input.aura_component) ? input.aura_component : inspoBad('aura_component {id|slug} is required');
1324
+ const ref = typeof ac.id === 'string' && ac.id ? ac.id : (typeof ac.slug === 'string' && ac.slug ? ac.slug : inspoBad('aura_component needs id or slug'));
1325
+ const scope = 'aura-' + String(ref).toLowerCase().replace(/[^a-z0-9]+/g, '');
1326
+ return {
1327
+ evidence: { type: ev.type, id: ev.id.trim() },
1328
+ aura_component: ref,
1329
+ inspo_call: { tool: 'inspo.get_reference_jsx', arguments: { type: ev.type, id: ev.id.trim() } },
1330
+ aura_call: { tool: 'aura_get_component', arguments: { id: ref } },
1331
+ adapt_plan: {
1332
+ scope, css_scope_how: 'rewrite bare selectors as .' + scope + ' <selector>; leave Tailwind utilities, :root vars, @keyframes untouched',
1333
+ token_swaps: ['--inspo-* -> --aura-* (brand hex wins ties)'],
1334
+ files: [{ path: 'styles/tokens.css', contains: 'merged tokens first' }, { path: 'components/' + ref + '.html', contains: 'adapted markup' }],
1335
+ steps: ['Fetch both sources', 'Swap Inspo tokens to brand/Aura tokens', 'Scope CSS with .' + scope, 'Replace hotlinked demo images (see image_plan)'],
1336
+ },
1337
+ image_plan: INSPO_IMAGE_PLAN,
1338
+ license_note: INSPO_LICENSE,
1339
+ };
1340
+ }
1341
+
1342
+ function buildInspoSystem(input = {}) {
1343
+ const toks = input.inspo_tokens && typeof input.inspo_tokens === 'object' && !Array.isArray(input.inspo_tokens) ? inspoSlimCheck(input.inspo_tokens, 'inspo_tokens') : inspoBad('inspo_tokens {palette, fonts, ...} is required');
1344
+ const slug = typeof input.aura_system === 'string' && input.aura_system.trim() ? input.aura_system.trim()
1345
+ : (input.aura_system && typeof input.aura_system.slug === 'string' && input.aura_system.slug.trim() ? input.aura_system.slug.trim() : inspoBad('aura_system slug (string) is required'));
1346
+ const palette = Array.isArray(toks.palette) ? toks.palette.map((c) => inspoHex(c, 'inspo_tokens.palette[]')) : [];
1347
+ const brand = inspoHex(toks.brand_hex, 'inspo_tokens.brand_hex');
1348
+ const bg = toks.background ? inspoHex(toks.background, 'inspo_tokens.background') : (brand || palette[0]);
1349
+ const tx = toks.text ? inspoHex(toks.text, 'inspo_tokens.text') : null;
1350
+ const contrast_warning = bg && tx ? inspoContrastWarn(bg, tx) : null;
1351
+ return {
1352
+ aura_system: slug,
1353
+ palette_priority: ['brand_hex', 'inspo_evidence', 'aura_tokens'],
1354
+ optionA_inspo: { palette, fonts: Array.isArray(toks.fonts) ? toks.fonts.slice(0, 4) : [] },
1355
+ optionB_aura: { fetch: { tool: 'aura_get_design_system', arguments: { slug } }, note: 'Fetch Aura tokens, then compare side by side.' },
1356
+ optionC_blend: { colors_from: brand ? 'brand_hex' : 'inspo palette', structure_from: 'aura DESIGN.md', note: 'Brand/Inspo colors on Aura spacing/type skeleton.' },
1357
+ contrast_warning,
1358
+ question: { key: 'system_choice', question: 'Inspo palette and Aura tokens disagree — which wins?', options: ['A-inspo', 'B-aura', 'C-blend'] },
1359
+ files: [{ path: 'styles/tokens.css', contains: 'merged winning tokens — paste first' }, { path: 'DESIGN.md', contains: 'winning system rules' }],
1360
+ order: INSPO_ORDER,
1361
+ tokens_first: true,
1362
+ license_note: INSPO_LICENSE,
1363
+ };
1364
+ }
1365
+
1366
+ function buildInspoCompare(input = {}) { const list = Array.isArray(input.evidence) ? input.evidence : inspoBad('evidence must be an array of 2-4 slim site objects');
1367
+ if (list.length < 2 || list.length > 4) inspoBad('evidence must hold 2-4 sites — got ' + list.length);
1368
+ const sites = list.map((s, i) => {
1369
+ const o = inspoSlimCheck(s, 'evidence[' + i + ']');
1370
+ if (typeof o.slug !== 'string' || !o.slug.trim()) inspoBad('evidence[' + i + '].slug (string) is required');
1371
+ if (o.mode !== undefined && !INSPO_MODES.includes(o.mode)) inspoBad('evidence[' + i + '].mode must be light|dark');
1372
+ if (o.macrostructure !== undefined && !INSPO_MACROS.includes(o.macrostructure)) inspoBad('evidence[' + i + '].macrostructure must be one of ' + INSPO_MACROS.join('|'));
1373
+ return o;
1374
+ });
1375
+ const votes = {};
1376
+ for (const s of sites) for (const k of ['macrostructure', 'macro', 'mode']) {
1377
+ if (s[k]) votes[s[k]] = (votes[s[k]] || 0) + 1;
1378
+ }
1379
+ const top = Object.entries(votes).sort((a, b) => b[1] - a[1])[0];
1380
+ const house = { macrostructure: (sites.map((s) => s.macrostructure || s.macro).find(Boolean)) || null, mode: sites.map((s) => s.mode).find(Boolean) || null, agreement: top ? top[0] + ' x' + top[1] + '/' + sites.length : 'none' };
1381
+ const macro = house.macrostructure;
1382
+ const category = macro && INSPO_MACRO_TO_CATEGORY[macro] ? INSPO_MACRO_TO_CATEGORY[macro] : null;
1383
+ return {
1384
+ sites: sites.map((s) => s.slug),
1385
+ house_style: house,
1386
+ inspo_call: { tool: 'inspo.compare', arguments: { slugs: sites.map((s) => s.slug) } },
1387
+ closest_aura_systems_query: { tool: 'aura_search_design_systems', arguments: { query: macro || house.agreement, limit: 3 } },
1388
+ ranked_components_query: category ? { tool: 'aura_search_components', arguments: { query: macro, category } } : null,
1389
+ order: INSPO_ORDER,
1390
+ license_note: INSPO_LICENSE,
1391
+ };
1392
+ }
1393
+
1394
+ // Module: docs-helpers — pure planners linking Aura output to fresh Context7
1395
+ // docs. No fetch: the agent calls Context7, Aura shapes the lookup and applies
1396
+ // the answer. Same seam for tools + tests + docs.
1397
+ const DOCS_TRUSTED = {
1398
+ tailwind: { label: 'Tailwind CSS', resolve: 'tailwindcss', id: null, cdn: 'https://cdn.tailwindcss.com', aura: ['installGuide tailwindcss dep', 'styles/tokens.css', 'scaffold order'] },
1399
+ gsap: { label: 'GSAP + ScrollTrigger', resolve: 'gsap', id: null, cdn: 'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js', aura: ['installGuide gsap dep', 'preview_html motion'] },
1400
+ lenis: { label: 'Lenis smooth scroll', resolve: 'lenis', id: '/darkroomengineering/lenis', cdn: 'unpkg lenis@1 (confirm exact file via lookup)', aura: ['aura_motion_smooth wiring'] },
1401
+ iconify: { label: 'Iconify icons', resolve: 'iconify', id: null, cdn: 'https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js', aura: ['installGuide iconify-icon dep'] },
1402
+ };
1403
+ // Markers verified against renames we know happened; version pins below are
1404
+ // never declared stale — they go to needs_check for a live lookup instead.
1405
+ const DOCS_STALE_MARKERS = [
1406
+ { lib: 'lenis', re: /studio-freight/i, fix: 'Package renamed studio-freight -> lenis. Swap the import/CDN path, keep option names; look unchanged.' },
1407
+ ];
1408
+ const DOCS_PIN_HINTS = [
1409
+ { lib: 'tailwind', re: /cdn\.tailwindcss\.com/i, check: 'Confirm v3 Play CDN vs v4 @tailwindcss/browser build via lookup.' },
1410
+ { lib: 'gsap', re: /cdnjs\.cloudflare\.com\/ajax\/libs\/gsap\/[\d.]+/i, check: 'Confirm latest 3.x pin via lookup.' },
1411
+ { lib: 'lenis', re: /unpkg\.com\/lenis/i, check: 'Confirm latest 1.x file via lookup.' },
1412
+ { lib: 'iconify', re: /iconify-icon\/[\d.]+/i, check: 'Confirm latest iconify-icon pin via lookup.' },
1413
+ ];
1414
+ const DOCS_DEP_TO_LIB = { tailwindcss: 'tailwind', gsap: 'gsap', 'gsap-ScrollTrigger': 'gsap', 'iconify-icon': 'iconify' };
1415
+
1416
+ function docsBad(msg) { const e = new Error(msg); e.code = 'BAD_ARGS'; throw e; }
1417
+
1418
+ function docsStampFor(deps) {
1419
+ const libs = [];
1420
+ for (const d of (deps || [])) {
1421
+ const lib = DOCS_DEP_TO_LIB[d && d.name];
1422
+ if (lib && !libs.includes(lib)) libs.push(lib);
1423
+ }
1424
+ return {
1425
+ status: 'unchecked',
1426
+ needs_check: libs.map((l) => l + ': confirm CDN/version via aura_docs_plan + Context7'),
1427
+ note: 'Vendored Aura code is a snapshot — verify pins before shipping.',
1428
+ };
1429
+ }
1430
+
1431
+ function buildDocsPlan(input = {}) {
1432
+ const raw = typeof input.library === 'string' ? input.library.trim().toLowerCase() : '';
1433
+ if (!raw) docsBad('library (string) is required');
1434
+ const need = typeof input.need === 'string' ? input.need.trim() : '';
1435
+ if (!need) docsBad('need (string) is required — what question should the docs answer?');
1436
+ const t = DOCS_TRUSTED[raw] || null;
1437
+ return {
1438
+ library: raw,
1439
+ trusted: !!t,
1440
+ context7: t
1441
+ ? { resolve: t.resolve, ...(t.id ? { id: t.id } : {}), query: need, note: t.id ? 'ID verified — query it directly.' : 'Resolve the id first via Context7, then query.' }
1442
+ : { resolve: raw, query: need, note: 'Unlisted library — resolve the id first via Context7, then query. Apply guidance is generic.' },
1443
+ apply: t
1444
+ ? { aura_touchpoints: t.aura, steps: ['Look up via Context7', 'Adapt Aura code to the fresh API (keep the look identical)', 'Update the docs_stamp from unchecked to checked'] }
1445
+ : { aura_touchpoints: [], steps: ['Look up via Context7', 'Apply carefully — no Aura mapping exists for this library'] },
1446
+ stamp: { status: 'planned', needs_check: [raw + ': ' + need] },
1447
+ };
1448
+ }
1449
+
1450
+ function buildDocsCheck(input = {}) {
1451
+ const raw = typeof input.library === 'string' ? input.library.trim().toLowerCase() : '';
1452
+ if (!raw) docsBad('library (string) is required');
1453
+ const code = typeof input.code_excerpt === 'string' ? input.code_excerpt : docsBad('code_excerpt (string) is required');
1454
+ if (!code.trim()) docsBad('code_excerpt must not be empty');
1455
+ if (code.length > 20000) docsBad('code_excerpt is ' + code.length + ' chars — paste excerpts under 20000 chars, not full pages');
1456
+ const note = typeof input.docs_note === 'string' && input.docs_note.trim() ? input.docs_note.trim() : null;
1457
+ const fixes = [];
1458
+ for (const m of DOCS_STALE_MARKERS) {
1459
+ if (m.re.test(code)) fixes.push({ lib: m.lib, fix: m.fix });
1460
+ }
1461
+ const needs = [];
1462
+ for (const h of DOCS_PIN_HINTS) {
1463
+ if (h.re.test(code) && !needs.includes(h.lib + ': ' + h.check)) needs.push(h.lib + ': ' + h.check);
1464
+ }
1465
+ if (note) needs.push('agent-reported docs change: ' + note.slice(0, 200));
1466
+ return {
1467
+ library: raw,
1468
+ verdict: fixes.length ? 'needs-update' : 'current',
1469
+ fixes,
1470
+ needs_check: needs,
1471
+ rule: 'Fresh docs win on API, but edits must preserve the look — adapt Aura code, never restyle.',
1472
+ stamp: { status: fixes.length || note ? 'checked-with-findings' : 'checked', findings: fixes.length + (note ? 1 : 0) },
1473
+ };
1474
+ }
1475
+
1476
+ // ---- icons.mjs ----
1477
+ // Module: icons — live icon fetch (Font Awesome Free + HackerNoon pixel
1478
+ // library) over jsDelivr. One interface: getIcon({name, source, style}).
1479
+ // URLs verified live 2026-10-10; licenses verified against the repos'
1480
+ // own license pages (FA Free: CC BY 4.0 icons; pixel .svg/.png: CC BY 4.0).
1481
+
1482
+ const ICON_TTL_MS = 24 * 60 * 60 * 1000; // icons are static — remember 24h
1483
+ const ICON_MAX_KEYS = 500;
1484
+ const ICON_MAX_BYTES = 50 * 1024;
1485
+ const ICON_FA_BASE = "https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6/svgs";
1486
+ const ICON_FA_CSS = "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css";
1487
+ const ICON_PX_BASE = "https://cdn.jsdelivr.net/gh/hackernoon/pixel-icon-library@main/icons/SVG";
1488
+ const ICON_PX_CSS = "https://cdn.jsdelivr.net/npm/@hackernoon/pixel-icon-library@latest/fonts/iconfont.css";
1489
+ const ICON_FA_STYLES = ["solid", "regular", "brands"];
1490
+ const ICON_SOURCES = ["auto", "fontawesome", "pixel"];
1491
+ const ICON_STYLES = ["auto", "solid", "regular", "brands"];
1492
+
1493
+ function iconBad(msg) { const e = new Error(msg); e.code = "BAD_ARGS"; throw e; }
1494
+ function iconMissing(msg) { const e = new Error(msg); e.code = "NOT_FOUND"; throw e; }
1495
+
1496
+ const ICON_CREDITS = {
1497
+ fontawesome: {
1498
+ license: "CC BY 4.0 (icons) — https://fontawesome.com/license/free",
1499
+ credit: "Icon by Font Awesome Free (CC BY 4.0) — https://fontawesome.com",
1500
+ },
1501
+ pixel: {
1502
+ license: "CC BY 4.0 — https://github.com/hackernoon/pixel-icon-library#license-free--starter--pro",
1503
+ credit: "Icon by HackerNoon Pixel Icon Library (CC BY 4.0) — https://pixeliconlibrary.com (give credit + link, note changes)",
1504
+ },
1505
+ };
1506
+
1507
+ function iconCredit(source) {
1508
+ const c = ICON_CREDITS[source];
1509
+ if (!c) iconBad("source must be fontawesome|pixel");
1510
+ return { ...c };
1511
+ }
1512
+
1513
+ // Candidate URLs in fallback order. Pixel solid variants use a -solid suffix
1514
+ // (see iconfont.css hn-*-solid classes); FA uses per-style directories.
1515
+ function iconCandidates(name, source, style) {
1516
+ const out = [];
1517
+ const faStyles = style === "auto" ? ICON_FA_STYLES : [style];
1518
+ const pxCats = style === "auto" ? ["regular", "solid", "brands"] : [style];
1519
+ if (source === "auto" || source === "fontawesome") {
1520
+ for (const s of faStyles) out.push({ source: "fontawesome", style: s, url: ICON_FA_BASE + "/" + s + "/" + name + ".svg" });
1521
+ }
1522
+ if (source === "auto" || source === "pixel") {
1523
+ for (const c of pxCats) {
1524
+ out.push({ source: "pixel", style: c, url: ICON_PX_BASE + "/" + c + "/" + name + ".svg" });
1525
+ out.push({ source: "pixel", style: c, url: ICON_PX_BASE + "/" + c + "/" + name + "-solid.svg" });
1526
+ }
1527
+ }
1528
+ return out;
1529
+ }
1530
+
1531
+ function createIcons({ fetcher, now = () => Date.now() }) {
1532
+ if (!fetcher || typeof fetcher.getText !== "function") throw new Error("fetcher.getText is required");
1533
+ const cache = new Map();
1534
+
1535
+ async function getIcon(q = {}) {
1536
+ const raw = typeof q.name === "string" ? q.name.trim().toLowerCase() : "";
1537
+ if (!raw) iconBad("name (string) is required — e.g. {name:'bolt'}");
1538
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(raw)) iconBad("name must be kebab-case like 'bolt' or 'arrow-down'");
1539
+ const source = q.source === undefined ? "auto" : q.source;
1540
+ const style = q.style === undefined ? "auto" : q.style;
1541
+ if (!ICON_SOURCES.includes(source)) iconBad("source must be one of " + ICON_SOURCES.join("|"));
1542
+ if (!ICON_STYLES.includes(style)) iconBad("style must be one of " + ICON_STYLES.join("|"));
1543
+
1544
+ const key = [source, style, raw].join("|");
1545
+ const hit = cache.get(key);
1546
+ if (hit && now() - hit.t < ICON_TTL_MS) return { ...hit.v, cached: true };
1547
+ if (hit) cache.delete(key);
1548
+
1549
+ const tried = [];
1550
+ let lastFetchErr = null;
1551
+ for (const c of iconCandidates(raw, source, style)) {
1552
+ let body = null;
1553
+ try {
1554
+ body = await fetcher.getText(c.url, {});
1555
+ } catch (e) {
1556
+ // Network-level failure is not a miss: remember it, keep walking the
1557
+ // chain in case only one host is down, and rethrow at the end if
1558
+ // nothing else explains the failure (never disguise an outage as
1559
+ // "not found").
1560
+ lastFetchErr = e instanceof Error ? e : new Error(String(e));
1561
+ tried.push({ url: c.url, ok: false, why: "fetch-error" });
1562
+ continue;
1563
+ }
1564
+ if (body === null || body === undefined) { tried.push({ url: c.url, ok: false, why: "404" }); continue; }
1565
+ const text = String(body).replace(/^\s*<\?xml[^?]*\?>\s*/, "");
1566
+ if (!text.trim().startsWith("<svg")) { tried.push({ url: c.url, ok: false, why: "not-svg" }); continue; }
1567
+ if (text.length > ICON_MAX_BYTES) { tried.push({ url: c.url, ok: false, why: "oversize" }); continue; }
1568
+ const lic = ICON_CREDITS[c.source];
1569
+ const out = {
1570
+ name: raw, source: c.source, style: c.style, svg: text, bytes: text.length,
1571
+ source_url: c.url, license: lic.license, credit: lic.credit,
1572
+ alt: c.source === "pixel"
1573
+ ? { iconfont_css: ICON_PX_CSS, class: "hn hn-" + raw, use: '<link rel="stylesheet" href="' + ICON_PX_CSS + '"> + <i class="hn hn-' + raw + '"></i>' }
1574
+ : { css: ICON_FA_CSS, class: "fa-" + c.style + " fa-" + raw, use: '<link rel="stylesheet" href="' + ICON_FA_CSS + '"> + <i class="fa-' + c.style + " fa-" + raw + '"></i>' },
1575
+ tried: tried.map((t) => t.url),
1576
+ cached: false,
1577
+ };
1578
+ cache.set(key, { t: now(), v: out });
1579
+ while (cache.size > ICON_MAX_KEYS) cache.delete(cache.keys().next().value);
1580
+ return out;
1581
+ }
1582
+ if (lastFetchErr && tried.every((t) => t.why === "fetch-error")) throw lastFetchErr;
1583
+ iconMissing("icon not found: '" + raw + "' (" + source + "/" + style + ") — checked " + tried.length + " URLs; check spelling (FA names use dashes, e.g. 'arrow-down')");
1584
+ }
1585
+
1586
+ return { getIcon };
1587
+ }
1588
+
1589
+ // ---- version.mjs ----
1590
+ // Module: version — running vs npm-published version check. One small
1591
+ // interface (getVersion) over a registry fetch with a long cache. Never
1592
+ // throws: offline or unpublished registries return latest:null and a note.
1593
+
1594
+ const VERSION_TTL_MS = 60 * 60 * 1000; // re-check npm at most hourly
1595
+ const VERSION_URL = "https://registry.npmjs.org/aura-components-mcp/latest";
1596
+ const VERSION_INSTALL = "npm i -g aura-components-mcp@latest (or npx -y aura-components-mcp@latest), then restart the MCP client";
1597
+
1598
+ function createVersion({ fetcher, running, now = () => Date.now() }) {
1599
+ if (!fetcher || typeof fetcher.getText !== "function") throw new Error("fetcher.getText is required");
1600
+ const run = typeof running === "string" && running ? running : "0.0.0-dev";
1601
+ let cached = null;
1602
+
1603
+ function cmp(a, b) {
1604
+ const pa = String(a).split(".").map((x) => parseInt(x, 10) || 0);
1605
+ const pb = String(b).split(".").map((x) => parseInt(x, 10) || 0);
1606
+ for (let i = 0; i < 3; i++) {
1607
+ if (pa[i] !== pb[i]) return pa[i] < pb[i] ? -1 : 1;
1608
+ }
1609
+ return 0;
1610
+ }
1611
+
1612
+ async function getVersion() {
1613
+ if (cached && now() - cached.t < VERSION_TTL_MS) return { ...cached.v, cached: true };
1614
+ let latest = null;
1615
+ try {
1616
+ const body = await fetcher.getText(VERSION_URL, { accept: "application/json" });
1617
+ const j = body ? JSON.parse(body) : null;
1618
+ if (j && typeof j.version === "string" && /^\d+\.\d+\.\d+/.test(j.version)) latest = j.version;
1619
+ } catch { latest = null; }
1620
+ const behind = latest !== null && cmp(run, latest) < 0;
1621
+ const v = latest === null
1622
+ ? { running: run, latest: null, update_available: false, cached: false, checked_at: new Date(now()).toISOString(), notice: "Could not reach the npm registry — running v" + run + ". Prefer the latest published version when you can check." }
1623
+ : behind
1624
+ ? { running: run, latest, update_available: true, cached: false, checked_at: new Date(now()).toISOString(), notice: "Update available: running v" + run + ", npm publishes v" + latest + ". Prefer latest — " + VERSION_INSTALL + "." }
1625
+ : { running: run, latest, update_available: false, cached: false, checked_at: new Date(now()).toISOString(), notice: "Running v" + run + " — latest published. Prefer latest on every new session." };
1626
+ cached = { t: now(), v };
1627
+ return { ...v };
1628
+ }
1629
+
1630
+ return { getVersion };
1631
+ }
1632
+
1633
+ // ---- tools.mjs ----
1634
+ // Module: tools — thin adapters. Each tool validates args at the seam,
1635
+ // calls one catalog/guide method, wraps textResult. No retries/caching here
1636
+ // (except aura_recommend's single bounded auto-retry + editorial fill).
1637
+ const TOOL_SORTS = ['popular', 'recent', 'trending', 'updated'];
1638
+ const TOOL_CATS = ['hero','section','button','card','background','header','logo','feature','pricing','testimonial','footer','form','heading'];
1639
+ function toolFn_bad(msg) { const e = new Error(msg); e.code = 'BAD_ARGS'; throw e; }
1640
+ function toolFn_str(v, name) { if (v !== undefined && typeof v !== 'string') toolFn_bad(name + ' must be a string'); return v; }
1641
+ function toolFn_num(v, name) { if (v !== undefined && typeof v !== 'number') toolFn_bad(name + ' must be a number'); return v; }
1642
+ function toolFn_bool(v, name) { if (v !== undefined && typeof v !== 'boolean') toolFn_bad(name + ' must be a boolean'); return v; }
1643
+ function toolFn_sort(v) { if (v !== undefined && !TOOL_SORTS.includes(v)) toolFn_bad('sort must be one of ' + TOOL_SORTS.join('|')); return v; }
1644
+ function theme(v) { if (v !== undefined && v !== 'dark' && v !== 'light') toolFn_bad('theme must be dark|light'); return v; }
1645
+ function normRefs(v, name) {
1646
+ if (v === undefined) return v;
1647
+ let preview = '';
1648
+ try { preview = JSON.stringify(v).slice(0, 120); } catch { preview = String(v).slice(0, 120); }
1649
+ const ex = name + ' must be an array like ["732","1364"] or [{id:"732"}] — got ' + preview;
1650
+ if (!Array.isArray(v) || !v.length || v.length > 8) toolFn_bad(ex);
1651
+ const out = [];
1652
+ for (const e of v) {
1653
+ if (typeof e === 'string' && e) out.push(e);
1654
+ else if (typeof e === 'number' && Number.isFinite(e)) out.push(String(e));
1655
+ else if (e && typeof e === 'object' && !Array.isArray(e)) {
1656
+ if (typeof e.id === 'string' && e.id) out.push(e.id);
1657
+ else if (typeof e.id === 'number' && Number.isFinite(e.id)) out.push(String(e.id));
1658
+ else if (typeof e.slug === 'string' && e.slug) out.push(e.slug);
1659
+ else toolFn_bad(ex);
1660
+ } else toolFn_bad(ex);
1661
+ }
1662
+ if (!out.length) toolFn_bad(ex);
1663
+ return out;
1664
+ }
1665
+ function toolFn_cat(v) { if (v !== undefined && !TOOL_CATS.includes(v)) toolFn_bad('category must be one of ' + TOOL_CATS.join('|')); return v; }
1666
+ function inspoMode(v) { if (v !== undefined && !INSPO_MODES.includes(v)) toolFn_bad('mode must be light|dark'); return v; }
1667
+ function inspoVibe(v) { if (v !== undefined && !INSPO_VIBES.includes(v)) toolFn_bad('vibe must be one of ' + INSPO_VIBES.join('|')); return v; }
1668
+ function inspoMacro(v) { if (v !== undefined && !INSPO_MACROS.includes(v)) toolFn_bad('macrostructure must be one of ' + INSPO_MACROS.join('|')); return v; }
1669
+ function inspoEvidence(v, name) {
1670
+ if (!v || typeof v !== 'object' || Array.isArray(v)) toolFn_bad((name || 'evidence') + ' must be an object (slim keys only)');
1671
+ return v;
1672
+ }
1673
+ const TOOL_DEFS = [
1674
+ { name: 'aura_status', description: 'Catalogue health plus free counts (components, skills, assets, design systems). Free only, no login. Start here.', inputSchema: { type: 'object', properties: {}, additionalProperties: false } },
1675
+ { name: 'aura_search_components', description: 'Search free Aura UI components (2,495). Text over title and description, optional category tag, theme dark|light filter, negative_theme exclusion, min_views floor, sorts. Slim rows by default (excerpt + fetch pointer, no full code; slim:false for full). exact_match:true skips fallbacks (honest empty).', inputSchema: { type: 'object', properties: { query: { type: 'string' }, category: { type: 'string', enum: TOOL_CATS }, theme: { type: 'string', enum: ['dark', 'light'] }, negative_theme: { type: 'string', enum: ['dark', 'light'] }, min_views: { type: 'number' }, sort: { type: 'string', enum: TOOL_SORTS }, limit: { type: 'number' }, offset: { type: 'number' }, slim: { type: 'boolean' }, exact_match: { type: 'boolean' } }, additionalProperties: false } },
1676
+ { name: 'aura_get_component', description: 'Full free component detail with HTML/Tailwind source, preview image, page URL. Numeric id or slug. Chunk param pages large code (see code_info). full:true returns full code (ignores chunk).', inputSchema: { type: 'object', properties: { id: {}, slug: { type: 'string' }, chunk: { type: 'number' }, full: { type: 'boolean' } }, additionalProperties: false } },
1677
+ { name: 'aura_search_skills', description: 'Search free Aura agent skills (192). Metadata only, slim by default; use aura_get_skill for the full SKILL.md content. exact_match:true skips fallbacks.', inputSchema: { type: 'object', properties: { query: { type: 'string' }, sort: { type: 'string', enum: TOOL_SORTS }, limit: { type: 'number' }, offset: { type: 'number' }, slim: { type: 'boolean' }, exact_match: { type: 'boolean' } }, additionalProperties: false } },
1678
+ { name: 'aura_get_skill', description: 'Full free agent-skill content (SKILL.md body) plus source_url and page URL. Skill id. Chunk param pages large bodies (see content_info).', inputSchema: { type: 'object', properties: { id: { type: 'string' }, chunk: { type: 'number' } }, required: ['id'], additionalProperties: false } },
1679
+ { name: 'aura_search_assets', description: 'Search free Aura assets (images and video). Keywords, media_type image or video. Slim rows by default. exact_match:true skips fallbacks.', inputSchema: { type: 'object', properties: { query: { type: 'string' }, mediaType: { type: 'string', enum: ['image', 'video'] }, commercial_ok: { type: 'boolean' }, sort: { type: 'string', enum: TOOL_SORTS }, limit: { type: 'number' }, offset: { type: 'number' }, slim: { type: 'boolean' }, exact_match: { type: 'boolean' } }, additionalProperties: false } },
1680
+ { name: 'aura_search_design_systems', description: 'Search free Aura DESIGN.md systems (725). Metadata + snippets, slim by default; use aura_get_design_system for content. exact_match:true skips fallbacks.', inputSchema: { type: 'object', properties: { query: { type: 'string' }, sort: { type: 'string', enum: TOOL_SORTS }, limit: { type: 'number' }, offset: { type: 'number' }, slim: { type: 'boolean' }, exact_match: { type: 'boolean' } }, additionalProperties: false } },
1681
+ { name: 'aura_get_design_system', description: 'Full free DESIGN.md content plus preview_html, tokens, and page URL. System id or slug. Chunk param pages large preview_html (see preview_info).', inputSchema: { type: 'object', properties: { id: { type: 'string' }, slug: { type: 'string' }, chunk: { type: 'number' } }, additionalProperties: false } },
1682
+ { name: 'aura_search_all', description: 'One call across components, skills, assets, and design systems in parallel. Free only. Slim rows by default (excerpts, no blobs). Each surface runs its own fallback pipeline, so mixed outcomes are normal (skills may fall back while components go honestly empty) — read isFallback/no_match per surface, not once globally.', inputSchema: { type: 'object', properties: { query: { type: 'string' }, limit: { type: 'number' }, slim: { type: 'boolean' }, theme: { type: 'string', enum: ['dark', 'light'] }, negative_theme: { type: 'string', enum: ['dark', 'light'] }, min_views: { type: 'number' }, exact_match: { type: 'boolean' } }, additionalProperties: false } },
1683
+ { name: 'aura_recommend', description: 'Starter kit for a goal: top 3 free hits per surface with page URLs and rationale. Slim rows + commercial-use note. Empty components auto-retry once via suggested_queries, then editorial picks.', inputSchema: { type: 'object', properties: { goal: { type: 'string' }, theme: { type: 'string', enum: ['dark', 'light'] }, negative_theme: { type: 'string', enum: ['dark', 'light'] }, min_views: { type: 'number' }, slim: { type: 'boolean' } }, required: ['goal'], additionalProperties: false } },
1684
+ { name: 'aura_install_component', description: 'Paste-ready setup for a free component: dependency list, setup steps, file map, fonts. Goes beyond the official Aura MCP, which only reads project source.', inputSchema: { type: 'object', properties: { id: {}, slug: { type: 'string' } }, additionalProperties: false } },
1685
+ { name: 'aura_install_skill', description: 'Save-and-load plan for a free skill: where to put SKILL.md per client plus upstream source.', inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'], additionalProperties: false } },
1686
+ { name: 'aura_use_design_system', description: 'Apply a free DESIGN.md system: token starter CSS plus copy order (tokens first, then markup).', inputSchema: { type: 'object', properties: { id: { type: 'string' }, slug: { type: 'string' } }, additionalProperties: false } },
1687
+ { name: 'aura_bundle', description: 'Bulk-fetch 2-8 component details in one call (ids or slugs). Per-item errors never fail the batch.', inputSchema: { type: 'object', properties: { ids: { type: 'array', items: {} }, slugs: { type: 'array', items: { type: 'string' } } }, additionalProperties: false } },
1688
+ { name: 'aura_scaffold_page', description: 'One ordered page build: DESIGN.md tokens.css + system preview + component markup in dependency order, combined deps + files[]. Merges install_* + use_* in a single turn. components accepts string|number|{id}|{slug} refs, e.g. ["732","1364"] or [{id:"732"}] or [{slug:"..."}]; full:true returns full bodies.', inputSchema: { type: 'object', properties: { goal: { type: 'string' }, system: { type: 'string' }, components: { type: 'array', items: {} }, full: { type: 'boolean' } }, required: ['goal'], additionalProperties: false } },
1689
+ { name: 'aura_related', description: 'More-like-this: 3 related items for a component/skill/design-system by tag + text overlap. Discovery never dead-ends.', inputSchema: { type: 'object', properties: { kind: { type: 'string', enum: ['components', 'skills', 'design_systems'] }, id: {} }, required: ['kind', 'id'], additionalProperties: false } },
1690
+ { name: 'aura_bulk_fetch', description: 'Bulk-fetch 2-8 details in one call for components, design_systems, or assets. Alias-friendly name for aura_bundle. Per-item errors never fail the batch.', inputSchema: { type: 'object', properties: { kind: { type: 'string', enum: ['components', 'design_systems', 'assets'] }, ids: { type: 'array', items: {} } }, required: ['kind', 'ids'], additionalProperties: false } },
1691
+ { name: 'aura_install_asset', description: 'Legal drop-in plan for an asset: direct download URL, preview URL, license (all-rights-reserved per Aura Terms §4 — check page before commercial use), suggested file path.', inputSchema: { type: 'object', properties: { id: { type: 'number' } }, required: ['id'], additionalProperties: false } },
1692
+ { name: 'aura_categories', description: 'The 13 component categories with live free counts. Pick one, then search within it.', inputSchema: { type: 'object', properties: {}, additionalProperties: false } },
1693
+ { name: 'aura_prompt', description: 'Need-based design prompt in 2 passes: Pass 1 (goal only) returns draft + 6 questions (theme, sections, system, assets, macrostructure, vibe — mode folds into theme); ask the user via native question UI, then Pass 2 (goal + answers) returns copy-paste final_prompt + plan order + suggested calls starting at inspo.recommend. Always confirm with user before scaffolding.', inputSchema: { type: 'object', properties: { goal: { type: 'string' }, theme: { type: 'string', enum: ['dark', 'light'] }, surfaces: { type: 'array', items: { type: 'string' } }, system: { type: 'string' }, answers: { type: 'object' } }, required: ['goal'], additionalProperties: false } },
1694
+ { name: 'aura_inspo_recommend', description: 'Map a brief onto Inspo + Aura calls (pure, no fetch). Strict macro gate: without macrostructure returns need_macro:true — run inspo.recommend first, never scaffold generic. With macro returns inspo_call + 3 aura_calls + palette priority (brand > Inspo > Aura).', inputSchema: { type: 'object', properties: { brief: { type: 'string' }, mode: { type: 'string', enum: ['dark', 'light'] }, vibe: { type: 'string' }, macrostructure: { type: 'string' }, brand_hex: { type: 'string' } }, required: ['brief'], additionalProperties: false } },
1695
+ { name: 'aura_inspo_components', description: 'Map an Inspo component filter onto Aura search (pure). evidence.type hero|pricing|features|cta|nav|footer|testimonial|logo-cloud|faq|stat plus style/macrostructure/mode. Returns inspo.find_components call + aura_search + scaffold_hint (tokens first, weight s|m).', inputSchema: { type: 'object', properties: { evidence: { type: 'object' }, brief: { type: 'string' } }, required: ['evidence'], additionalProperties: false } },
1696
+ { name: 'aura_inspo_reference', description: 'Adapt one Inspo reference JSX onto one Aura component (pure). evidence {type, id} + aura_component {id|slug}. Returns inspo.get_reference_jsx + aura_get_component calls plus scoped adapt_plan (token swaps, css scope, files).', inputSchema: { type: 'object', properties: { evidence: { type: 'object' }, aura_component: { type: 'object' } }, required: ['evidence', 'aura_component'], additionalProperties: false } },
1697
+ { name: 'aura_inspo_system', description: 'Resolve Inspo tokens vs Aura DESIGN system (pure). inspo_tokens {palette, fonts, background?, text?, brand_hex?} + aura_system slug. Returns option A (Inspo) vs B (Aura fetch) vs C (blend) plus one system_choice question payload (A-inspo|B-aura|C-blend) and contrast warning. Tokens first.', inputSchema: { type: 'object', properties: { inspo_tokens: { type: 'object' }, aura_system: {} }, required: ['inspo_tokens', 'aura_system'], additionalProperties: false } },
1698
+ { name: 'aura_inspo_compare', description: 'Triangulate a house style from 2-4 slim Inspo sites (pure). evidence[] of {slug, palette?, fonts?, macrostructure?, mode?} — slim keys only, no autopsy. Returns inspo.compare call + closest Aura system/component queries.', inputSchema: { type: 'object', properties: { evidence: { type: 'array', items: { type: 'object' } } }, required: ['evidence'], additionalProperties: false } },
1699
+ { name: 'aura_docs_plan', description: 'Plan a fresh-docs lookup (pure, no fetch). library + need -> Context7 resolve/query plan (trusted: tailwind, gsap, lenis, iconify) + apply steps for Aura files + docs stamp slot. The agent calls Context7, Aura shapes the rest.', inputSchema: { type: 'object', properties: { library: { type: 'string' }, need: { type: 'string' } }, required: ['library', 'need'], additionalProperties: false } },
1700
+ { name: 'aura_docs_check', description: 'Check pasted code against fresh docs (pure). library + code_excerpt (under 20000 chars, excerpts not full pages) + optional docs_note -> verdict current|needs-update, look-preserving fixes, version-pin needs_check. Fresh API wins, look stays identical.', inputSchema: { type: 'object', properties: { library: { type: 'string' }, code_excerpt: { type: 'string' }, docs_note: { type: 'string' } }, required: ['library', 'code_excerpt'], additionalProperties: false } },
1701
+ { name: 'aura_icon', description: 'Fetch one free icon as ready-to-paste SVG (live). name in kebab-case (bolt, arrow-down). source auto|fontawesome|pixel, style auto|solid|regular|brands. Auto-fallback across styles then libraries; every hit carries license + credit (CC BY 4.0, credit always) plus a font/CSS alternative. 24h icon cache.', inputSchema: { type: 'object', properties: { name: { type: 'string' }, source: { type: 'string', enum: ['auto', 'fontawesome', 'pixel'] }, style: { type: 'string', enum: ['solid', 'regular', 'brands', 'auto'] } }, required: ['name'], additionalProperties: false } },
1702
+ { name: 'aura_version', description: 'Running vs npm-published version (live, hourly cache). Returns update_available + notice telling the agent to prefer latest (npm i -g aura-components-mcp@latest) when behind. Never fails — offline registries return latest:null. aura_status already carries this verdict on every call.', inputSchema: { type: 'object', properties: {}, additionalProperties: false } },
1703
+ ];
1704
+ function createHandlers(catalog, icons, version) {
1705
+ // Automatic update notice: every aura_status carries the cached version
1706
+ // verdict (hourly npm re-check), so agents learn about updates unprompted.
1707
+ const versionNote = async () => {
1708
+ try {
1709
+ if (version && typeof version.getVersion === "function") return await version.getVersion();
1710
+ } catch { /* status must never fail on version check */ }
1711
+ return null;
1712
+ };
1713
+ return {
1714
+ aura_status: async () => {
1715
+ const [status, update] = await Promise.all([catalog.getStatus(), versionNote()]);
1716
+ return textResult(update ? { ...status, update } : status);
1717
+ },
1718
+ aura_search_components: async (a) => { a = a || {}; return textResult(await catalog.searchCatalog('components', {
1719
+ query: toolFn_str(a.query, 'query'), tag: toolFn_cat(a.category), freeOnly: true, theme: theme(a.theme),
1720
+ negative_theme: a.negative_theme === undefined ? undefined : theme(a.negative_theme),
1721
+ min_views: toolFn_num(a.min_views, 'min_views'),
1722
+ sort: toolFn_sort(a.sort), limit: toolFn_num(a.limit, 'limit'), offset: toolFn_num(a.offset, 'offset'),
1723
+ slim: a.slim === undefined ? undefined : toolFn_bool(a.slim, 'slim'),
1724
+ exact_match: a.exact_match === undefined ? undefined : toolFn_bool(a.exact_match, 'exact_match'),
1725
+ })); },
1726
+ aura_get_component: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
1727
+ if (id === undefined || (typeof id !== 'string' && typeof id !== 'number')) toolFn_bad('provide id (number) or slug (string)');
1728
+ const full = a.full === undefined ? undefined : toolFn_bool(a.full, 'full');
1729
+ if (full === true) return textResult(await catalog.getItem('components', id, { full: true }));
1730
+ const chunk = a.chunk === undefined ? 0 : toolFn_num(a.chunk, 'chunk');
1731
+ return textResult(await catalog.getItem('components', id, { chunk })); },
1732
+ aura_search_skills: async (a) => { a = a || {}; return textResult(await catalog.searchCatalog('skills', {
1733
+ query: toolFn_str(a.query, 'query'), sort: toolFn_sort(a.sort), limit: toolFn_num(a.limit, 'limit'), offset: toolFn_num(a.offset, 'offset'),
1734
+ slim: a.slim === undefined ? undefined : toolFn_bool(a.slim, 'slim'),
1735
+ exact_match: a.exact_match === undefined ? undefined : toolFn_bool(a.exact_match, 'exact_match'),
1736
+ })); },
1737
+ aura_get_skill: async (a) => { a = a || {}; if (typeof a.id !== 'string' || !a.id) toolFn_bad('id (string) is required');
1738
+ const chunk = a.chunk === undefined ? 0 : toolFn_num(a.chunk, 'chunk');
1739
+ return textResult(await catalog.getItem('skills', a.id, { chunk })); },
1740
+ aura_search_assets: async (a) => { a = a || {}; const co = a.commercial_ok === undefined ? undefined : toolFn_bool(a.commercial_ok, 'commercial_ok');
1741
+ const r = await catalog.searchCatalog('assets', { query: toolFn_str(a.query, 'query'), mediaType: a.mediaType === undefined ? undefined : (a.mediaType === 'image' || a.mediaType === 'video' ? a.mediaType : toolFn_bad('mediaType must be image|video')), freeOnly: true, sort: toolFn_sort(a.sort), limit: toolFn_num(a.limit, 'limit'), offset: toolFn_num(a.offset, 'offset'), slim: a.slim === undefined ? undefined : toolFn_bool(a.slim, 'slim'), exact_match: a.exact_match === undefined ? undefined : toolFn_bool(a.exact_match, 'exact_match') });
1742
+ // Gap 5: commercial_ok filter (all-rights-reserved per Aura Terms S4).
1743
+ // commercial_ok:true returns [] with explanation, never silence.
1744
+ if (co === true) return textResult({ ...r, items: [], commercial_ok: false, commercial_note: 'All Aura catalogue assets are all-rights-reserved per Aura Terms S4. Preview/personal use via page_url; commercial reuse needs Aura permission.' });
1745
+ return textResult(r); },
1746
+ aura_search_design_systems: async (a) => { a = a || {}; return textResult(await catalog.searchCatalog('design_systems', {
1747
+ query: toolFn_str(a.query, 'query'), sort: toolFn_sort(a.sort), limit: toolFn_num(a.limit, 'limit'), offset: toolFn_num(a.offset, 'offset'),
1748
+ slim: a.slim === undefined ? undefined : toolFn_bool(a.slim, 'slim'),
1749
+ exact_match: a.exact_match === undefined ? undefined : toolFn_bool(a.exact_match, 'exact_match'),
1750
+ })); },
1751
+ aura_get_design_system: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
1752
+ if (typeof id !== 'string' || !id) toolFn_bad('provide id or slug (string)');
1753
+ const chunk = a.chunk === undefined ? 0 : toolFn_num(a.chunk, 'chunk');
1754
+ return textResult(await catalog.getItem('design_systems', id, { chunk })); },
1755
+ aura_search_all: async (a) => { a = a || {}; const query = toolFn_str(a.query, 'query'), limit = toolFn_num(a.limit, 'limit') || 5;
1756
+ const slim = a.slim === undefined ? undefined : toolFn_bool(a.slim, 'slim');
1757
+ const themeV = a.theme === undefined ? undefined : theme(a.theme);
1758
+ const negV = a.negative_theme === undefined ? undefined : theme(a.negative_theme);
1759
+ const minV = toolFn_num(a.min_views, 'min_views');
1760
+ const exactV = a.exact_match === undefined ? undefined : toolFn_bool(a.exact_match, 'exact_match');
1761
+ const r = await Promise.all([
1762
+ catalog.searchCatalog('components', { query, freeOnly: true, limit, slim, theme: themeV, negative_theme: negV, min_views: minV, exact_match: exactV }),
1763
+ catalog.searchCatalog('skills', { query, limit, slim, min_views: minV, exact_match: exactV }),
1764
+ catalog.searchCatalog('assets', { query, freeOnly: true, limit, slim, min_views: minV, exact_match: exactV }),
1765
+ catalog.searchCatalog('design_systems', { query, limit, slim, min_views: minV, exact_match: exactV }),
1766
+ ]);
1767
+ return textResult({ query: query || null, components: r[0], skills: r[1], assets: r[2], design_systems: r[3] }); },
1768
+ aura_recommend: async (a) => { a = a || {}; if (typeof a.goal !== 'string' || !a.goal.trim()) toolFn_bad('goal (string) is required');
1769
+ // Gap 1: recommend hard-filters theme (not just reranks) — starter kits must not
1770
+ // mix light rows into a dark ask. min_views gates noise; both are explicit params.
1771
+ const themeV = a.theme === undefined ? undefined : theme(a.theme);
1772
+ const negative_themeV = a.negative_theme === undefined ? undefined : theme(a.negative_theme);
1773
+ const min_views = toolFn_num(a.min_views, 'min_views');
1774
+ const slim = a.slim === undefined ? undefined : toolFn_bool(a.slim, 'slim');
1775
+ const r = await Promise.all([
1776
+ catalog.searchCatalog('components', { query: a.goal, freeOnly: true, limit: 3, theme: themeV, negative_theme: negative_themeV, min_views, slim }),
1777
+ catalog.searchCatalog('skills', { query: a.goal, limit: 3, slim }),
1778
+ catalog.searchCatalog('assets', { query: a.goal, freeOnly: true, limit: 3, slim }),
1779
+ catalog.searchCatalog('design_systems', { query: a.goal, limit: 3, slim }),
1780
+ ]);
1781
+ // P0 auto-retry (bounded: max 1 retry + 1 picks call, no loops): an honest-empty
1782
+ // components surface (no_match) retries once via suggested_queries[0] with the
1783
+ // same filters, then falls back to human-grade editorial picks so the kit fills.
1784
+ if (r[0] && r[0].no_match === true && (!Array.isArray(r[0].items) || !r[0].items.length)) {
1785
+ const sq = Array.isArray(r[0].suggested_queries) ? r[0].suggested_queries.filter((q) => typeof q === 'string' && q.trim()) : [];
1786
+ let filled = false;
1787
+ if (sq.length) {
1788
+ try {
1789
+ const retry = await catalog.searchCatalog('components', { query: sq[0], freeOnly: true, limit: 3, theme: themeV, negative_theme: negative_themeV, min_views, slim });
1790
+ if (retry && Array.isArray(retry.items) && retry.items.length) {
1791
+ retry.retried = true;
1792
+ retry.retried_from = sq[0];
1793
+ r[0] = retry;
1794
+ filled = true;
1795
+ }
1796
+ } catch { /* keep the original honest-empty */ }
1797
+ }
1798
+ if (!filled && (!Array.isArray(r[0].items) || !r[0].items.length)) {
1799
+ try {
1800
+ const picks = typeof catalog.editorialPicks === 'function' ? await catalog.editorialPicks(3) : [];
1801
+ if (Array.isArray(picks) && picks.length) {
1802
+ r[0] = { kind: 'components', total: picks.length, items: picks, editorial_fallback: true, editorial_note: 'Goal matched nothing — showing human-grade free picks instead.' };
1803
+ }
1804
+ } catch { /* keep the original honest-empty */ }
1805
+ }
1806
+ }
1807
+ r[0].items = (r[0].items || []).slice(0, 3);
1808
+ const pick = (x) => ((x && x.items) || []).slice(0, 3).map((i) => ({ title: i.title, page_url: i.page_url }));
1809
+ return textResult({ goal: a.goal, freeOnly: true,
1810
+ rationale: 'Top 3 free catalogue hits per surface, ranked by Aura views. Open the page_url previews, then fetch full source or content for finalists.',
1811
+ commercial_note: 'Assets (and component art) are all-rights-reserved per Aura Terms §4: fine to prototype, get Aura permission before commercial ship.',
1812
+ components: r[0], skills: r[1], assets: r[2], design_systems: r[3],
1813
+ starter_kit: { components: pick(r[0]), skills: pick(r[1]), assets: pick(r[2]), design_systems: pick(r[3]) } }); },
1814
+ aura_install_component: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
1815
+ if (id === undefined || (typeof id !== 'string' && typeof id !== 'number')) toolFn_bad('provide id (number) or slug (string)');
1816
+ const got = await catalog.getItem('components', id);
1817
+ // Pro refusal enforced centrally in catalog.getItem (fail-closed).
1818
+ const compInstall = installGuide('components', got.item);
1819
+ return textResult({ item: got.item, install: compInstall, docs_stamp: docsStampFor(compInstall.deps) }); },
1820
+ aura_install_skill: async (a) => { a = a || {}; if (typeof a.id !== 'string' || !a.id) toolFn_bad('id (string) is required');
1821
+ const got = await catalog.getItem('skills', a.id);
1822
+ return textResult({ item: got.item, install: installGuide('skills', got.item), docs_stamp: { status: 'unchecked', needs_check: ['SKILL.md is a vendored snapshot — re-check source_url for updates'], note: 'No library pins in skill installs.' } }); },
1823
+ aura_use_design_system: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
1824
+ if (typeof id !== 'string' || !id) toolFn_bad('provide id or slug (string)');
1825
+ const got = await catalog.getItem('design_systems', id);
1826
+ const dsInstall = installGuide('design_systems', got.item);
1827
+ return textResult({ item: got.item, install: dsInstall, tokens: tokenHints(got.item.content || '', got.item.preview_html || ''), docs_stamp: docsStampFor(dsInstall.deps) }); },
1828
+ aura_categories: async () => { const cats = await catalog.categoryCounts();
1829
+ const s = (cats || []).reduce((n, c) => n + (typeof c.free === 'number' ? c.free : 0), 0);
1830
+ return textResult({ categories: cats, overlap_note: 'Counts overlap: components carry multiple tags, so per-category free counts sum to ' + s + ' (> 2495 free total). They triage depth, not a partition.' }); },
1831
+ aura_prompt: async (a) => { a = a || {}; return textResult(buildPrompt({ goal: typeof a.goal === 'string' ? a.goal : undefined, theme: a.theme, surfaces: a.surfaces, system: a.system, answers: a.answers })); },
1832
+ aura_inspo_recommend: async (a) => { a = a || {}; return textResult(buildInspoRecommend({
1833
+ brief: typeof a.brief === 'string' ? a.brief : undefined, mode: inspoMode(a.mode), vibe: inspoVibe(a.vibe),
1834
+ macrostructure: inspoMacro(a.macrostructure), brand_hex: toolFn_str(a.brand_hex, 'brand_hex'),
1835
+ })); },
1836
+ aura_inspo_components: async (a) => { a = a || {}; return textResult(buildInspoComponents({ evidence: inspoEvidence(a.evidence, 'evidence'), brief: toolFn_str(a.brief, 'brief') })); },
1837
+ aura_inspo_reference: async (a) => { a = a || {}; return textResult(buildInspoReference({ evidence: inspoEvidence(a.evidence, 'evidence'), aura_component: inspoEvidence(a.aura_component, 'aura_component') })); },
1838
+ aura_inspo_system: async (a) => { a = a || {}; const sys = (typeof a.aura_system === 'string' || (a.aura_system && typeof a.aura_system === 'object')) ? a.aura_system : toolFn_bad('aura_system slug (string) is required');
1839
+ return textResult(buildInspoSystem({ inspo_tokens: inspoEvidence(a.inspo_tokens, 'inspo_tokens'), aura_system: sys })); },
1840
+ aura_inspo_compare: async (a) => { a = a || {}; if (!Array.isArray(a.evidence)) toolFn_bad('evidence must be an array of 2-4 slim site objects');
1841
+ return textResult(buildInspoCompare({ evidence: a.evidence })); },
1842
+ aura_docs_plan: async (a) => { a = a || {}; return textResult(buildDocsPlan({ library: typeof a.library === 'string' ? a.library : undefined, need: typeof a.need === 'string' ? a.need : undefined })); },
1843
+ aura_docs_check: async (a) => { a = a || {}; return textResult(buildDocsCheck({ library: typeof a.library === 'string' ? a.library : undefined, code_excerpt: typeof a.code_excerpt === 'string' ? a.code_excerpt : undefined, docs_note: toolFn_str(a.docs_note, 'docs_note') })); },
1844
+ aura_icon: async (a) => { a = a || {}; if (!icons || typeof icons.getIcon !== 'function') { const e = new Error('icons backend not wired'); throw e; }
1845
+ const name = typeof a.name === 'string' ? a.name : toolFn_bad('name (string) is required — e.g. {name:"bolt"}');
1846
+ const source = a.source === undefined ? undefined : (a.source === 'auto' || a.source === 'fontawesome' || a.source === 'pixel' ? a.source : toolFn_bad('source must be auto|fontawesome|pixel'));
1847
+ const style = a.style === undefined ? undefined : (a.style === 'auto' || a.style === 'solid' || a.style === 'regular' || a.style === 'brands' ? a.style : toolFn_bad('style must be auto|solid|regular|brands'));
1848
+ return textResult(await icons.getIcon({ name, source, style })); },
1849
+ aura_version: async () => { if (!version || typeof version.getVersion !== 'function') { const e = new Error('version backend not wired'); throw e; }
1850
+ return textResult(await version.getVersion()); },
1851
+ aura_bundle: async (a) => { a = a || {}; const ids = normRefs(a.ids, 'ids') || normRefs(a.slugs, 'slugs');
1852
+ if (!ids || !ids.length) toolFn_bad('provide ids (array of 1-8 numbers/strings) or slugs (array of strings)');
1853
+ return textResult({ kind: 'components', results: await catalog.bundleItems('components', ids) }); },
1854
+ aura_related: async (a) => { a = a || {}; const k = a.kind;
1855
+ if (k !== 'components' && k !== 'skills' && k !== 'design_systems') toolFn_bad('kind must be components|skills|design_systems');
1856
+ const id = a.id; if (typeof id !== 'string' && typeof id !== 'number') toolFn_bad('id is required');
1857
+ return textResult(await catalog.relatedItems(k, id, 3)); },
1858
+ aura_install_asset: async (a) => { a = a || {}; if (typeof a.id !== 'number') toolFn_bad('id (number) is required');
1859
+ const got = await catalog.getItem('assets', a.id);
1860
+ const it = got.item; const fx = (it.facets || {});
1861
+ return textResult({ item: it, install: { kind: 'assets', title: it.title, page_url: it.page_url, license: fx.license || 'all-rights-reserved (Aura Terms §4)', terms_url: 'https://www.aura.build/terms', download: fx.download || null, preview: fx.preview || null, files: [{ path: 'assets/' + it.id + '-' + String(it.title || 'asset').toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 40) + '.jpg', contains: 'downloaded original' }], steps: ['Check the license on the Aura asset page before commercial use — this server reports unknown, never assumes free-to-sell.', 'Download the download URL into the suggested path.', 'Use the preview URL for <img> srcset while drafting.'] }, docs_stamp: { status: 'unchecked', needs_check: ['license per page_url — terms can change'], note: 'No library pins in asset installs.' } }); },
1862
+ aura_bulk_fetch: async (a) => { a = a || {}; const kind = a.kind;
1863
+ if (kind !== 'components' && kind !== 'design_systems' && kind !== 'assets') toolFn_bad('kind must be components|design_systems|assets');
1864
+ const ids = normRefs(a.ids, 'ids');
1865
+ if (!ids || !ids.length) toolFn_bad('provide ids (array of 1-8)');
1866
+ return textResult({ kind, results: await catalog.bundleItems(kind, ids) }); },
1867
+ aura_scaffold_page: async (a) => { a = a || {}; if (typeof a.goal !== 'string' || !a.goal.trim()) toolFn_bad('goal (string) is required');
1868
+ const sysRef = (typeof a.system === 'string' && a.system) ? a.system : null;
1869
+ const compRefs = normRefs(a.components, 'components') || [];
1870
+ const full = a.full === undefined ? undefined : toolFn_bool(a.full, 'full');
1871
+ const fullOpt = full === undefined ? {} : { full };
1872
+ const [sys, bundle] = await Promise.all([
1873
+ sysRef ? catalog.getItem('design_systems', sysRef, fullOpt).catch(() => null) : catalog.searchCatalog('design_systems', { query: a.goal, limit: 1 }).then((r) => (r.items[0] ? { item: r.items[0] } : null)),
1874
+ compRefs.length ? catalog.bundleItems('components', compRefs, 8, fullOpt).then((rs) => ({ items: rs.filter((x) => x.ok).map((x) => x.item), dropped: rs.filter((x) => !x.ok).length })) : catalog.searchCatalog('components', { query: a.goal, freeOnly: true, limit: 3 }).then((r) => ({ items: r.items, dropped: 0 })),
1875
+ ]);
1876
+ const comps = bundle.items;
1877
+ // Pro guard: explicit-id bundles can name Pro rows — free flows never ship them.
1878
+ const freeComps = (comps || []).filter((c) => c && c.premium === false);
1879
+ const proFiltered = (comps || []).length - freeComps.length;
1880
+ const sysItem = sys && sys.item ? sys.item : null;
1881
+ const toks = sysItem ? tokenHints(sysItem.content || '') : { tokens: {}, css: '' };
1882
+ const { guides, combined_deps: deps, files } = combineScaffold(sysItem, freeComps);
1883
+ return textResult({ goal: a.goal, order: ['1 tokens.css', '2 system preview_html', '3 component markup in listed order'], tokens_css: toks.css || '', system: sysItem, components: freeComps || [], guides, combined_deps: deps, files, docs_stamp: docsStampFor(deps), ...(bundle.dropped ? { dropped_refs: bundle.dropped, dropped_note: 'Some requested refs resolved to missing or Pro (paid) components and were omitted — see per-id errors via aura_bundle.' } : {}), ...(proFiltered ? { pro_filtered_out: proFiltered, pro_note: 'Pro (paid) components were refused — free-only scaffold ships free rows.' } : {}), license_note: 'Markup and any pulled assets are all-rights-reserved per Aura Terms §4 — fine to prototype, check each page_url before commercial ship.' }); },
1884
+ };
1885
+ };
1886
+
1887
+ // ---- server.mjs (main) ----
1888
+ // Module: server — composition root. Wires config -> fetcher -> catalog ->
1889
+ // tools -> protocol session over stdio. Only framing + wiring lives here.
1890
+
1891
+
1892
+
1893
+
1894
+
1895
+
1896
+
1897
+
1898
+
1899
+ const config = loadConfig();
1900
+ const fetcher = createFetcher({ fetchImpl: globalThis.fetch, timeoutMs: config.timeoutMs, retries: config.retries, userAgent: config.userAgent });
1901
+ const catalog = createCatalog({ fetcher, config, facetsFn: facets });
1902
+ const icons = createIcons({ fetcher });
1903
+ const version = createVersion({ fetcher, running: SERVER_INFO.version });
1904
+ const handlers = createHandlers(catalog, icons, version);
1905
+
1906
+ let buffer = "";
1907
+ const send = (msg) => process.stdout.write(JSON.stringify(msg) + "\n");
1908
+ const { dispatch } = createSession(send, { tools: TOOL_DEFS, handlers });
1909
+
1910
+ process.stdin.setEncoding("utf8");
1911
+ process.stdin.on("data", (chunk) => {
1912
+ buffer += chunk;
1913
+ let i;
1914
+ while ((i = buffer.indexOf("\n")) >= 0) {
1915
+ const line = buffer.slice(0, i).trim();
1916
+ buffer = buffer.slice(i + 1);
1917
+ if (!line) continue;
1918
+ let msg;
1919
+ try { msg = JSON.parse(line); }
1920
+ catch { send({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } }); continue; }
1921
+ dispatch(msg);
1922
+ }
1923
+ });
1924
+ process.stdin.on("end", () => process.exit(0));
1925
+ process.stdin.resume();
1926
+
1927
+ // Startup cinematics live on stderr only: stdout is reserved for JSON-RPC.
1928
+ // Sequence: aura moment -> catalogue credit -> builder credit -> running.
1929
+ const BANNER = [
1930
+ " <<< aura-components-mcp >>>",
1931
+ " free Aura catalogue, every AI agent",
1932
+ ];
1933
+ const sleepMs = (ms) => new Promise((r) => setTimeout(r, ms));
1934
+ const paint = (s) => process.stderr.write(s + "\n");
1935
+ async function typeLine(text, cps = 90) {
1936
+ for (const ch of text) { process.stderr.write(ch); await sleepMs(1000 / cps); }
1937
+ process.stderr.write("\n");
1938
+ }
1939
+ async function dots(label, n = 3) {
1940
+ process.stderr.write(label);
1941
+ for (let i = 0; i < n; i++) { await sleepMs(160); process.stderr.write("."); }
1942
+ process.stderr.write("\n");
1943
+ }
1944
+ async function boot() {
1945
+ if (process.env.AURA_QUIET === "1" || process.argv.includes("--quiet")) {
1946
+ paint(SERVER_INFO.name + " v" + SERVER_INFO.version + " listening on stdio");
1947
+ return;
1948
+ }
1949
+ for (const l of BANNER) paint(l);
1950
+ await typeLine("starting your aura for designing...");
1951
+ await dots("warming catalogue cache");
1952
+ paint("catalogue credit: Aura (aura.build) — components, skills, assets, systems");
1953
+ await sleepMs(180);
1954
+ paint("built by swastiksingh-dev — free to use, MIT, no paywall");
1955
+ await sleepMs(180);
1956
+ paint(SERVER_INFO.name + " v" + SERVER_INFO.version + " running — " + TOOL_DEFS.length + " tools on stdio");
1957
+ }
1958
+ boot();