aura-components-mcp 1.6.2 → 1.6.3

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,871 +1,1103 @@
1
- #!/usr/bin/env node
2
- // aura-components-mcp v1.6.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.0.0",
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
- function windowAgoIso(days) {
159
- const d = new Date();
160
- d.setDate(d.getDate() - days);
161
- return d.toISOString();
162
- }
163
- function weekAgoIso() { return windowAgoIso(7); }
164
- function trendingIso(days = 90) { return windowAgoIso(days); } // 7d seed is empty; callers pass window_days (default 30 for components now)
165
-
166
- function buildSearchParams(kind, q = {}) {
167
- const p = new URLSearchParams();
168
- p.set("select", LIST_COLS[kind]);
169
- const limit = q.limit;
170
- const offset = q.offset ?? 0;
171
- p.set("limit", String(limit));
172
- p.set("offset", String(offset));
173
- const sort = CATALOG_SORTS[kind][q.sort] ?? CATALOG_SORTS[kind].popular;
174
- p.set("order", sort);
175
- const filters = [];
176
- if (kind === "components" || kind === "assets") {
177
- filters.push("private=eq.false");
178
- if (q.freeOnly) filters.push("premium=eq.false");
179
- } else if (kind === "skills" || kind === "design_systems") {
180
- filters.push("private=eq.false");
181
- }
182
- if (q.tag && kind === "components") filters.push("tags=cs.{" + q.tag.toLowerCase() + "}");
183
- if (q.mediaType && kind === "assets") filters.push("media_type=eq." + q.mediaType);
184
- const ors = [];
185
- if (q.query && q.query.trim()) {
186
- const v = ilike(q.query);
187
- if (kind === "components") ors.push("title.ilike." + v + ",description.ilike." + v);
188
- else if (kind === "skills") ors.push("title.ilike." + v + ",description.ilike." + v);
189
- else if (kind === "assets") ors.push("title.ilike." + v + ",description.ilike." + v);
190
- else ors.push("title.ilike." + v + ",description.ilike." + v);
191
- }
192
- if (q.sort === "trending") filters.push("created_at=gte." + trendingIso(q.window_days ?? 90));
193
- return { params: p, filters, ors };
194
- }
195
-
196
- function buildUrl(base, table, params, filters, ors) {
197
- let qs = params.toString();
198
- for (const f of filters) qs += "&" + f;
199
- if (ors.length) qs += "&or=(" + ors.join(",") + ")";
200
- return base + "/rest/v1/" + table + "?" + qs;
201
- }
202
-
203
- // Truncation with cursors: agents see full_length + chunk info instead of a silent
204
- // marker, and can fetch the rest via getItem with { chunk } (see getItem below).
205
- const trunc = (s, n) => (s && s.length > n ? s.slice(0, n) + "…[truncated]" : s);
206
- function chunkText(s, n, chunk = 0) {
207
- const full = String(s || "");
208
- const total = Math.max(1, Math.ceil(full.length / n));
209
- const i = Math.max(0, Math.min(chunk, total - 1));
210
- return { text: full.slice(i * n, (i + 1) * n) + (i < total - 1 ? "…[truncated]" : ""), chunk: i, chunks: total, full_length: full.length, truncated: total > 1 };
211
- }
212
-
213
- function pageUrl(kind, row) {
214
- if (kind === "components") return SITE + "/component/" + row.slug;
215
- if (kind === "skills") return SITE + "/skills/" + row.id;
216
- // ISS9: asset pages are query-routed (no /assets/ID route exists). Keep the q= form
217
- // but ALSO expose a stable key so agents can cite: id + slug + title together.
218
- if (kind === "assets") return SITE + "/assets?q=" + encodeURIComponent(row.title ?? "");
219
- return SITE + "/design-systems/" + (row.slug ?? row.id);
220
- }
221
-
222
- function assetKey(row) {
223
- return { id: row.id, slug: row.slug ?? null, title: row.title ?? null };
224
- }
225
-
226
- function shapeRow(kind, row, cfg, withFacets) {
227
- const base = { ...row };
228
- // Facets probe the first 600 chars of FULL code before truncation, so search facets
229
- // equal detail facets for icons/fonts/theme. Weight uses the true length (ISS1/2/3).
230
- if (kind === "components" && typeof base.code === "string") {
231
- base.code_chars = base.code.length;
232
- if (withFacets) { try { base.facets = withFacets(kind, { ...base, code: base.code.slice(0, 600) }, { fullLength: base.code.length }); } catch { /* never break rows */ } }
233
- // Gap 3: code_excerpt (first 1200 chars, readable) + full_fetch flag. Agents skim
234
- // the excerpt, then get chunk N — no more blind 18k-70k dumps or silent cuts.
235
- base.code_excerpt = String(base.code).slice(0, 1200);
236
- base.full_fetch = { tool: "aura_get_component", id: base.id ?? base.slug, chunks: Math.max(1, Math.ceil(base.code.length / cfg.codeChars)) };
237
- const ch = chunkText(base.code, cfg.codeChars, 0);
238
- base.code = ch.text;
239
- base.code_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: 0, truncated: ch.truncated };
240
- if (base.facets) base.facets.deps_summary = base.facets.needsTailwind || base.facets.needsIcons ? [base.facets.needsTailwind && "tailwindcss", base.facets.needsIcons && "iconify-icon"].filter(Boolean) : [];
241
- }
242
- if (kind === "skills" && typeof base.content === "string") {
243
- const ch = chunkText(base.content, cfg.contentChars, 0);
244
- base.content = ch.text;
245
- base.content_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: 0, truncated: ch.truncated };
246
- }
247
- if (kind === "design_systems") {
248
- if (typeof base.content === "string") {
249
- const ch = chunkText(base.content, cfg.contentChars, 0);
250
- base.content = ch.text;
251
- base.content_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: 0, truncated: ch.truncated };
252
- }
253
- if (typeof base.preview_html === "string") {
254
- const ch = chunkText(base.preview_html, cfg.codeChars, 0);
255
- base.preview_html = ch.text;
256
- base.preview_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: 0, truncated: ch.truncated };
257
- }
258
- }
259
- base.page_url = pageUrl(kind, row);
260
- if (kind === "assets") base.asset_key = assetKey(row);
261
- if (withFacets && !base.facets) { try { base.facets = withFacets(kind, base); } catch { /* facets never break rows */ } }
262
- // ISS8: force absolute Supabase URLs — relative variant paths break fetch.
263
- if (kind === "assets") {
264
- const abs = (u) => (!u ? u : (/^https?:\/\//i.test(u) ? u : ("https://hoirqrkdgbmvpwutwuwj-all.supabase.co/storage/v1/object/public/assets/" + String(u).replace(/^\/+/, ""))));
265
- 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]);
266
- if (base.facets) {
267
- if (base.facets.download) base.facets.download = abs(base.facets.download);
268
- if (base.facets.preview) base.facets.preview = abs(base.facets.preview);
269
- }
270
- }
271
- return base;
272
- }
273
-
274
- function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = null }) {
275
- const base = config.supabaseUrl.replace(/\/$/, "");
276
- const headers = { apikey: config.anonKey, Authorization: "Bearer " + config.anonKey, Prefer: "count=exact" };
277
- const cache = new Map();
278
- const MAX_KEYS = 300;
279
- const cacheGet = (k) => {
280
- if (!config.cacheTtlMs) return null;
281
- const e = cache.get(k);
282
- if (!e) return null;
283
- if (now() - e.t > config.cacheTtlMs) { cache.delete(k); return null; }
284
- cache.delete(k); cache.set(k, e); // LRU refresh
285
- return e.v;
286
- };
287
- const cacheSet = (k, v) => {
288
- if (!config.cacheTtlMs) return;
289
- if (cache.has(k)) cache.delete(k);
290
- cache.set(k, { t: now(), v });
291
- while (cache.size > MAX_KEYS) cache.delete(cache.keys().next().value);
292
- };
293
- const authorCache = new Map(); // created_by -> profile (shared across searches)
294
- const inflight = new Map(); // cacheKey -> Promise (coalesce concurrent identical calls)
295
-
296
- const clampLimit = (l) => Math.min(config.maxLimit, Math.max(1, l ?? config.defaultLimit));
297
-
298
- async function enrichAuthors(rows) {
299
- const ids = [...new Set(rows.map((r) => r.created_by).filter(Boolean).filter((id) => !authorCache.has(id)))];
300
- if (ids.length) {
301
- try {
302
- const url = base + "/rest/v1/public_author_profiles?select=id,full_name,avatar_url,slug&id=in.(" + ids.map(esc).join(",") + ")";
303
- const { rows: authors } = await fetcher.getJson(url, headers);
304
- for (const a of authors) { authorCache.set(a.id, a); if (authorCache.size > 500) break; }
305
- } catch { /* authors stay null, rows still return */ }
306
- }
307
- return rows.map((r) => ({ ...r, author: (r.created_by && authorCache.get(r.created_by)) || null }));
308
- }
309
-
310
- async function searchCatalog(kind, q = {}) {
311
- const limit = clampLimit(q.limit);
312
- const offset = Math.max(0, q.offset ?? 0);
313
- const sort = CATALOG_SORTS[kind][q.sort] ? q.sort : "popular";
314
- const freeOnly = q.freeOnly ?? config.freeOnlyDefault;
315
- // Gap 1: negative_theme (exclude a theme) + min_views (quality floor). Both ride
316
- // the cache key so filtered/unfiltered variants never collide.
317
- const negTheme = q.negative_theme ? String(q.negative_theme).toLowerCase() : "";
318
- const minViews = Math.max(0, q.min_views ?? 0);
319
- const key = ["search", kind, q.query ?? "", q.tag ?? "", q.mediaType ?? "", sort, limit, offset, freeOnly, negTheme, minViews].join("|");
320
- const hit = cacheGet(key);
321
- if (hit) return { ...hit, cached: true };
322
- if (inflight.has(key)) return inflight.get(key);
323
- const p = (async () => {
324
- const runOnce = async (qq) => {
325
- const { params, filters, ors } = buildSearchParams(kind, { ...qq, limit, offset, sort, freeOnly });
326
- const url = buildUrl(base, kind, params, filters, ors);
327
- return fetcher.getJson(url, headers);
328
- };
329
- let { rows, total } = await runOnce(q);
330
- let fallback = null;
331
- // AND-phrase queries like "dark cinematic portfolio" match nothing literally:
332
- // retry as OR over significant tokens so every surface degrades instead of emptying.
333
- // Theme words (dark/light/cinematic) are rank signals, not match tokens.
334
- const toks = (q.query && q.query.trim() && !q.tag) ? queryTokens(q.query) : [];
335
- const themeHint = (q.query && q.query.trim()) ? detectThemeHint(q.query) : null;
336
- const effTheme = q.theme || themeHint;
337
- const suggest = (t) => {
338
- const s = [];
339
- if (t.length > 1) s.push(t.slice(0, Math.min(3, t.length)).join(" "));
340
- for (const x of t.slice(0, 3)) s.push(x);
341
- return [...new Set(s)].slice(0, 4);
342
- };
343
- if ((!rows || !rows.length) && toks.length) {
344
- // Pass 1: OR over tokens, same filters (drops the AND-phrase requirement).
345
- const orParts = [];
346
- for (const t of toks) {
347
- const v = ilike(t);
348
- orParts.push("title.ilike." + v + ",description.ilike." + v);
349
- }
350
- const b1 = buildSearchParams(kind, { ...q, query: "", limit, offset, sort, freeOnly });
351
- try {
352
- const fb = await fetcher.getJson(buildUrl(base, kind, b1.params, b1.filters, orParts), headers);
353
- if (fb.rows && fb.rows.length) { rows = fb.rows; total = fb.total; fallback = "or-tokens:" + toks.join(","); }
354
- } catch { /* try pass 2 */ }
355
- // Pass 2: still empty (e.g. trending window + rare tokens): drop sort window,
356
- // keep OR tokens, rank by views. Guarantees aggregators return something useful.
357
- if ((!rows || !rows.length)) {
358
- const b2 = buildSearchParams(kind, { ...q, query: "", limit, offset, sort: "popular", freeOnly });
359
- try {
360
- const fb2 = await fetcher.getJson(buildUrl(base, kind, b2.params, b2.filters, orParts), headers);
361
- if (fb2.rows && fb2.rows.length) { rows = fb2.rows; total = fb2.total; fallback = "or-tokens-unwindowed:" + toks.join(","); }
362
- } catch { /* keep original empty result */ }
363
- }
364
- }
365
- // ISS7: dedupe skills by source_url (same SKILL.md listed twice). First (most-viewed)
366
- // wins, gets canonical:true; dupes are dropped and counted in deduped.
367
- let deduped = 0;
368
- if (kind === "skills") {
369
- const seen = new Set();
370
- const kept = [];
371
- for (const r of rows) {
372
- const u = (r.source_url || "").trim().toLowerCase();
373
- if (u && seen.has(u)) { deduped++; continue; }
374
- if (u) seen.add(u);
375
- kept.push(r);
376
- }
377
- const byUrl = new Map();
378
- for (const r of kept) {
379
- const u = (r.source_url || "").trim().toLowerCase();
380
- if (u && (!byUrl.has(u) || (r.views || 0) > (byUrl.get(u).views || 0))) byUrl.set(u, r);
381
- }
382
- rows = kept.map((r) => {
383
- const u = (r.source_url || "").trim().toLowerCase();
384
- return (u && byUrl.get(u) === r) ? { ...r, canonical: true } : r;
385
- });
386
- }
387
- // Gap 4: inline snippets — one extra batched fetch fills preview/tokens glimpses
388
- // into search rows (top N only) so agents decide without a second call per row.
389
- // Bounded: top 5 rows, client-side sliced to 800 chars. No extra call when empty.
390
- if ((kind === "design_systems" || kind === "skills") && rows.length) {
391
- const topIds = rows.slice(0, 5).map((r) => r.id);
392
- try {
393
- const idList = topIds.map((id) => encodeURIComponent(String(id))).join(",");
394
- const sel = kind === "design_systems" ? "id,content,preview_html" : "id,content,source_url";
395
- const { rows: full } = await fetcher.getJson(base + "/rest/v1/" + kind + "?select=" + encodeURIComponent(sel) + "&id=in.(" + idList + ")", headers);
396
- const byId = new Map(full.map((r) => [String(r.id), r]));
397
- rows = rows.map((r) => {
398
- const f = byId.get(String(r.id));
399
- if (!f) return r;
400
- if (kind === "design_systems") return { ...r, preview_snippet: String(f.preview_html || "").slice(0, 800), tokens_snippet: String(f.content || "").slice(0, 800) };
401
- return { ...r, content_snippet: String(f.content || "").slice(0, 800) };
402
- });
403
- } catch { /* snippets are best-effort; rows still return */ }
404
- }
405
- const enriched = await enrichAuthors(rows);
406
- const out = { kind, total, limit, offset, items: enriched.map((r) => shapeRow(kind, r, config, facetsFn)), cached: false };
407
- if (deduped) out.deduped = deduped;
408
- // Gap 1: min_views quality floor (views-gated surfaces only) + negative_theme
409
- // hard exclusion. Both apply before theme re-rank so counts stay honest.
410
- if (minViews > 0) out.items = out.items.filter((it) => (it.views || 0) >= minViews);
411
- if (negTheme && kind === "components") {
412
- const before = out.items.length;
413
- out.items = out.items.filter((it) => !it.facets || it.facets.theme !== negTheme);
414
- if (out.items.length < before) out.negative_theme_filtered_out = before - out.items.length;
415
- }
416
- // Theme-aware re-rank (components): theme hint from query (dark/light/cinematic)
417
- // or explicit q.theme reorders so matches come first — never silently drops rows,
418
- // but reports how many were filtered out of the top for transparency.
419
- if (kind === "components" && effTheme) {
420
- const scored = scoreRows(kind, out.items, toks, effTheme);
421
- const topTheme = scored.filter((s) => s.themeOk).map((s) => s.r);
422
- const dropped = out.items.length - topTheme.length;
423
- if (topTheme.length) {
424
- out.items = topTheme;
425
- out.theme_hint = effTheme;
426
- if (dropped) out.theme_filtered_out = dropped;
427
- } else {
428
- out.theme_hint = effTheme;
429
- out.theme_warning = "no " + effTheme + "-theme rows in top results; showing unfiltered, check facets.theme per row";
430
- }
431
- }
432
- // ISS15: transparent fallback — agents see what happened, not a magic string.
433
- if (fallback) {
434
- out.fallback = fallback; out.isFallback = true;
435
- const scoredTop = out.items[0];
436
- const topTitle = scoredTop ? String(scoredTop.title || "").toLowerCase() : "";
437
- const hits = toks.filter((t) => topTitle.includes(t)).length;
438
- out.fallback_score = "token-overlap:" + hits + "/" + toks.length + "-terms" + (effTheme ? ";theme:" + effTheme : "");
439
- }
440
- else if ((!rows || !rows.length) && toks.length) { out.isFallback = false; out.suggested_queries = suggest(toks).filter((s) => !/^[a-z0-9]{8,}$/i.test(s.replace(/\s/g, ""))); if (!out.suggested_queries.length) out.suggested_queries = ["hero", "pricing", "landing page"]; }
441
- cacheSet(key, out);
442
- return out;
443
- })();
444
- inflight.set(key, p);
445
- try { return await p; } finally { inflight.delete(key); }
446
- }
447
-
448
- async function getItem(kind, idOrSlug, opts = {}) {
449
- const chunk = Math.max(0, opts.chunk ?? 0);
450
- const key = ["get", kind, String(idOrSlug), "c" + chunk].join("|");
451
- const hit = cacheGet(key);
452
- if (hit) return { ...hit, cached: true };
453
- 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));
454
- const isNum = /^\d+$/.test(String(idOrSlug));
455
- const sel = DETAIL_COLS[kind];
456
- // design_systems + skills: uuid id vs slug; components: numeric id vs slug
457
- const filt = (kind === "design_systems" || kind === "skills")
458
- ? (isUuid ? "id=eq." + encodeURIComponent(String(idOrSlug)) : "slug=eq." + encodeURIComponent(String(idOrSlug)))
459
- : (kind === "components" && !isNum
460
- ? "slug=eq." + encodeURIComponent(String(idOrSlug))
461
- : "id=eq." + encodeURIComponent(String(idOrSlug)));
462
- const url = base + "/rest/v1/" + kind + "?select=" + encodeURIComponent(sel) + "&" + filt + "&limit=1";
463
- const { rows } = await fetcher.getJson(url, headers);
464
- if (!rows.length) { const e = new Error("not found: " + kind + " " + idOrSlug); e.code = "NOT_FOUND"; throw e; }
465
- const enriched = await enrichAuthors(rows);
466
- const full = enriched[0];
467
- // Chunked fetch: re-slice full blobs per requested chunk so agents can page
468
- // through 18k-70k payloads instead of hitting one silent truncation wall.
469
- const item = shapeRow(kind, full, config, facetsFn);
470
- if (chunk > 0) {
471
- if (kind === "components" && typeof full.code === "string") {
472
- const ch = chunkText(full.code, config.codeChars, chunk);
473
- item.code = ch.text;
474
- item.code_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: ch.chunk, truncated: ch.truncated };
475
- }
476
- if ((kind === "skills") && typeof full.content === "string") {
477
- const ch = chunkText(full.content, config.contentChars, chunk);
478
- item.content = ch.text;
479
- item.content_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: ch.chunk, truncated: ch.truncated };
480
- }
481
- if (kind === "design_systems") {
482
- if (typeof full.content === "string") {
483
- const ch = chunkText(full.content, config.contentChars, chunk);
484
- item.content = ch.text;
485
- item.content_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: ch.chunk, truncated: ch.truncated };
486
- }
487
- if (typeof full.preview_html === "string") {
488
- const ch = chunkText(full.preview_html, config.codeChars, chunk);
489
- item.preview_html = ch.text;
490
- item.preview_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: ch.chunk, truncated: ch.truncated };
491
- }
492
- }
493
- }
494
- const out = { kind, item, cached: false };
495
- cacheSet(key, out);
496
- return out;
497
- }
498
-
499
- async function getStatus() {
500
- const key = "status";
501
- const hit = cacheGet(key);
502
- if (hit) return { ...hit, cached: true };
503
- const counts = {};
504
- const jobs = [
505
- ["components_free", "components?select=id&private=eq.false&premium=eq.false&limit=1"],
506
- ["components_pro", "components?select=id&private=eq.false&premium=eq.true&limit=1"],
507
- ["skills", "skills?select=id&private=eq.false&limit=1"],
508
- ["assets", "assets?select=id&private=eq.false&limit=1"],
509
- ["design_systems", "design_systems?select=id&private=eq.false&limit=1"],
510
- ];
511
- await Promise.all(jobs.map(async ([k, path]) => {
512
- try { const { total } = await fetcher.getJson(base + "/rest/v1/" + path, headers); counts[k] = total; }
513
- catch { counts[k] = null; }
514
- }));
515
- const out = { site: SITE, catalogue: "aura.build public catalogue (PostgREST, anon)", counts, cached: false };
516
- cacheSet(key, out);
517
- return out;
518
- }
519
-
520
- async function categoryCounts() {
521
- const key = 'categories';
522
- const hit = cacheGet(key);
523
- if (hit) return hit.categories;
524
- const cats = ['hero','section','button','card','background','header','logo','feature','pricing','testimonial','footer','form','heading'];
525
- const out = await Promise.all(cats.map(async (c) => {
526
- try {
527
- const url = base + '/rest/v1/components?select=id&private=eq.false&premium=eq.false&tags=cs.{' + c + '}&limit=1';
528
- const { total } = await fetcher.getJson(url, headers);
529
- return { category: c, free: total };
530
- } catch { return { category: c, free: null }; }
531
- }));
532
- cacheSet(key, { categories: out });
533
- return out;
534
- }
535
-
536
- // Gap 2: editorial picks — human-grade component leads for when views-signal is
537
- // noise (free trending total=2). Top free by forks (remix = real reuse), then views.
538
- async function editorialPicks(limit = 5) {
539
- const lim = Math.max(1, Math.min(limit || 5, 8));
540
- const sel = encodeURIComponent(LIST_COLS.components);
541
- const url = base + "/rest/v1/components?select=" + sel + "&private=eq.false&premium=eq.false&order=forks.desc,views.desc&limit=" + lim;
542
- try {
543
- const { rows } = await fetcher.getJson(url, headers);
544
- const enriched = await enrichAuthors(rows);
545
- return enriched.map((r) => shapeRow("components", r, config, facetsFn));
546
- } catch { return []; }
547
- }
548
-
549
- // Bulk fetch: N details in one parallel round (backs aura_bundle + aura_scaffold_page).
550
- // Per-id errors are captured, never thrown: { ok:true, item } or { ok:false, id, error }.
551
- async function bundleItems(kind, ids, max = 5) {
552
- const list = [...new Set((ids || []).map(String))].slice(0, Math.max(1, Math.min(max, 8)));
553
- const out = await Promise.all(list.map(async (id) => {
554
- try { const got = await getItem(kind, id); return { ok: true, id, item: got.item }; }
555
- catch (e) { return { ok: false, id, error: (e && e.code === "NOT_FOUND") ? "not found" : String((e && e.message) || e).slice(0, 160) }; }
556
- }));
557
- return out;
558
- }
559
-
560
- // Related: same-tag / same-text overlap, views-ranked, excluding self.
561
- // ISS10: freeOnly default true — free flows never get Pro recommendations.
562
- async function relatedItems(kind, idOrSlug, limit = 3, freeOnly = true) {
563
- const got = await getItem(kind, idOrSlug);
564
- const item = got.item;
565
- const toks = queryTokens(item.title + " " + (item.description || "")).slice(0, 3);
566
- const orParts = toks.map((t) => { const v = ilike(t); return "title.ilike." + v + ",description.ilike." + v; });
567
- const tagF = (kind === "components" && item.tags && item.tags.length) ? "&tags=cs.{" + String(item.tags[0]).toLowerCase() + "}" : "";
568
- const lim = Math.max(1, Math.min(limit || 3, 8));
569
- const sel = LIST_COLS[kind];
570
- const idCol = (kind === "components") ? "id" : "id";
571
- const selfId = encodeURIComponent(String(item.id));
572
- const premF = (freeOnly && (kind === "components" || kind === "assets")) ? "&premium=eq.false" : "";
573
- 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;
574
- try {
575
- const { rows } = await fetcher.getJson(url, headers);
576
- const enriched = await enrichAuthors(rows);
577
- return { item: shapeRow(kind, item, config), related: enriched.map((r) => shapeRow(kind, r, config)) };
578
- } catch { return { item: shapeRow(kind, item, config), related: [] }; }
579
- }
580
-
581
- return { searchCatalog, getItem, getStatus, categoryCounts, bundleItems, relatedItems, editorialPicks };
582
- }
583
-
584
- // ---- protocol.mjs ----
585
- const SERVER_INFO = { name: "aura-components-mcp", version: "1.6.1" };
586
- // Module: protocol — stdio JSON-RPC framing + MCP handshake + error codes.
587
- // One interface: createSession(send) -> { dispatch(msg) }. No business logic.
588
-
589
- const PROTOCOL_VERSION = "2024-11-05";
590
-
591
- const protocolFn_err = (code, message, data) => ({ code, message, ...(data === undefined ? {} : { data }) });
592
- const Errors = {
593
- parse: (m) => protocolFn_err(-32700, "Parse error" + (m ? ": " + m : "")),
594
- invalidRequest: (m) => protocolFn_err(-32600, "Invalid Request" + (m ? ": " + m : "")),
595
- methodNotFound: (m) => protocolFn_err(-32601, "Method not found: " + m),
596
- invalidParams: (m) => protocolFn_err(-32602, "Invalid params" + (m ? ": " + m : "")),
597
- upstream: (m) => protocolFn_err(-32000, "Upstream error" + (m ? ": " + m : "")),
598
- notFound: (m) => protocolFn_err(-32004, m || "Not found"),
599
- };
600
-
601
- function textResult(obj) {
602
- return { content: [{ type: "text", text: JSON.stringify(obj, null, 2) }] };
603
- }
604
-
605
- function createSession(send, { tools, handlers }) {
606
- let initialized = false;
607
- const ok = (id, result) => send({ jsonrpc: "2.0", id, result });
608
- const fail = (id, e) => send({ jsonrpc: "2.0", id, error: e });
609
-
610
- async function dispatch(msg) {
611
- if (!msg || typeof msg !== "object" || msg.jsonrpc !== "2.0") {
612
- if (msg && "id" in msg && msg.id !== undefined && msg.id !== null) fail(msg.id, Errors.invalidRequest("bad envelope"));
613
- return;
614
- }
615
- const { id, method, params } = msg;
616
- const isCall = id !== undefined && id !== null;
617
- try {
618
- if (method === "initialize") {
619
- initialized = true;
620
- if (!isCall) return;
621
- return ok(id, { protocolVersion: PROTOCOL_VERSION, capabilities: { tools: {} }, serverInfo: SERVER_INFO });
622
- }
623
- if (method === "notifications/initialized") return;
624
- if (method === "ping") { if (isCall) return ok(id, {}); return; }
625
- if (!initialized) { if (isCall) fail(id, Errors.invalidRequest("not initialized")); return; }
626
- if (method === "tools/list") { if (isCall) return ok(id, { tools }); return; }
627
- if (method === "tools/call") {
628
- if (!isCall) return;
629
- const name = params?.name;
630
- const args = params?.arguments ?? {};
631
- const h = handlers[name];
632
- if (!h) return fail(id, Errors.methodNotFound("tool " + name));
633
- try {
634
- const result = await h(args);
635
- return ok(id, result);
636
- } catch (e) {
637
- if (e && e.code === "NOT_FOUND") return fail(id, Errors.notFound(e.message));
638
- if (e && e.code === "BAD_ARGS") return fail(id, Errors.invalidParams(e.message));
639
- return fail(id, Errors.upstream(e?.message ?? String(e)));
640
- }
641
- }
642
- if (isCall) return fail(id, Errors.methodNotFound(method));
643
- } catch (e) {
644
- if (isCall) fail(id, Errors.upstream(e?.message ?? String(e)));
645
- }
646
- }
647
-
648
- return { dispatch };
649
- }
650
-
651
- // ---- guide.mjs ----
652
- // Module: guide — small interface (installGuide, tokenHints, detectNeeds)
653
- // turning raw catalogue rows into paste-ready help: setup steps, file map,
654
- // dependency list, token starter. Derived from row fields, no extra fetches.
655
-
656
- const GUIDE_CDN = {
657
- tailwind: 'https://cdn.tailwindcss.com',
658
- iconify: 'https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js',
659
- };
660
-
661
- // Font allowlist: real families only. Tailwind weight/size utilities (medium, semibold,
662
- // normal, light-as-weight, size-*) are NOT families — old regex leaked them into fonts[].
663
- 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'];
664
- const FONT_RE = /font-(?:family-)?\[?['"]?([a-z][a-z0-9-]*)/g;
665
-
666
- function detectNeeds(code, tags) {
667
- const c = String(code || '').toLowerCase();
668
- const t = new Set((tags || []).map((x) => String(x).toLowerCase()));
669
- const needsTailwind = c.includes('class=') || t.has('tailwind');
670
- // Probes must work on a 2k prefix too: icon svgs usually appear in the first screen of markup.
671
- // svg alone under-matches in lists (no code fetched) — but must not over-claim either:
672
- // 'svg' counts only with an icon-ish neighbor (iconify, <svg, lucide, heroicons, feather).
673
- const needsIcons = c.includes('iconify') || c.includes('<svg') || c.includes('lucide') || c.includes('heroicon') || c.includes('data-lucide');
674
- const needsKeyframes = c.includes('keyframes') || c.includes('animation:');
675
- const fonts = new Set();
676
- for (const m of c.matchAll(FONT_RE)) {
677
- const fam = m[1].replace(/['"\]]/g, '');
678
- if (KNOWN_FONTS.includes(fam)) fonts.add(fam);
679
- }
680
- // font-family: 'X', Y declarations (not utility classes)
681
- for (const m of c.matchAll(/font-family\s*:\s*([^;}]{1,80})/g)) {
682
- for (const part of m[1].split(',')) {
683
- const fam = part.trim().replace(/['"]/g, '').toLowerCase().replace(/\s+/g, '-');
684
- if (KNOWN_FONTS.includes(fam)) fonts.add(fam);
685
- }
686
- }
687
- return { needsTailwind, needsIcons, needsKeyframes, fonts: [...fonts].slice(0, 6) };
688
- };
689
-
690
- // Facets: cheap derived signals so search lists answer "dark? heavy? pro?" without a get.
691
- // theme: dark|light|mixed|unknown from background field + code palette probes.
692
- // weight: code_chars bucket (s/m/l) so agents can prefer light embeds.
693
- function facets(kind, row, opts) {
694
- const f = {};
695
- if (kind === 'components') {
696
- const bg = String(row.background || '').toLowerCase();
697
- const tags = (row.tags || []).map((x) => String(x).toLowerCase());
698
- const code = String(row.code || '').toLowerCase();
699
- const tagDark = tags.includes('dark');
700
- const tagLight = tags.includes('light');
701
- 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;
702
- const lightHits = (code.match(/bg-white|bg-neutral-50|bg-slate-50|bg-gray-50|text-black|text-neutral-9/g) || []).length;
703
- const bgDark = bg.includes('000') && !bg.includes('fff');
704
- const bgLight = bg.includes('fff') && !bg.includes('000');
705
- 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')));
706
- const n = (opts && opts.fullLength) || String(row.code || '').length;
707
- f.weight = n > 20000 ? 'l' : (n > 8000 ? 'm' : 's');
708
- f.code_chars = n;
709
- const needs = detectNeeds(row.code || '', row.tags || []);
710
- f.needsTailwind = needs.needsTailwind;
711
- f.needsIcons = needs.needsIcons;
712
- f.fonts = needs.fonts;
713
- }
714
- // ISS14 lives here too: license is top-level on every asset row, not buried.
715
- // ISS13: rewrite hotlinked demo images to stable asset references where possible.
716
- // i.pravatar.cc + bare demo URLs 404 in production; flag them in the install plan.
717
- if (kind === 'components' && typeof row.code === 'string') {
718
- 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));
719
- if (hot.length) row = { ...row, _hotlinked: [...new Set(hot)].slice(0, 5) };
720
- }
721
- if (kind === 'assets' || row.image_800w || row.image_original || row.video_url) {
722
- // Resolved 2026-09-13 from primary source https://www.aura.build/terms §4-5:
723
- // catalogue content is the exclusive property of DESIGNCODE IO PTE. LTD.;
724
- // no per-asset license column exists, so commercial reuse needs Aura's permission.
725
- 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)';
726
- // Gap 5: machine-readable license gate. commercial_ok:false always (no per-asset
727
- // column exists to prove otherwise) — agents filter without parsing prose.
728
- f.commercial_ok = false;
729
- f.terms_url = 'https://www.aura.build/terms';
730
- f.download = row.image_original || row.image_1600w || row.image_800w || row.video_url || null;
731
- f.preview = row.image_800w || row.video_poster_url || null;
732
- }
733
- if (kind === 'design_systems') {
734
- // ISS4: lists never fetch preview_html (24KB/row). has_preview:true is authoritative
735
- // only on detail; on lists report 'unknown-list' so agents fetch instead of trusting false.
736
- f.has_preview = row.preview_html !== undefined ? Boolean(row.preview_html) : 'unknown-list';
737
- }
738
- return f;
739
- }
740
-
741
- function installGuide(kind, row) {
742
- const steps = [];
743
- const files = [];
744
- const deps = [];
745
- const url = row.page_url || '';
746
- if (kind === 'components') {
747
- const needs = detectNeeds(row.code || '', row.tags || []);
748
- if (needs.needsTailwind) {
749
- deps.push({ name: 'tailwindcss', via: 'CDN or project setup', cdn: GUIDE_CDN.tailwind });
750
- steps.push('Add the Tailwind CDN script to head for a quick preview, or paste the markup into a Tailwind project.');
751
- }
752
- if (needs.needsIcons) {
753
- deps.push({ name: 'iconify-icon', via: 'CDN', cdn: GUIDE_CDN.iconify });
754
- steps.push('Add the iconify-icon CDN script so the icon tags render.');
755
- }
756
- if (needs.needsKeyframes) steps.push('Keep the embedded style block with the markup: that is where the keyframes live.');
757
- // ISS11+12: pagination + CSS scoping. Code ships truncated at codeChars; the scope
758
- // prefix below prevents .button/.inner/.point collisions in multi-component scaffolds.
759
- const scope = 'aura-' + String(row.slug || row.id).toLowerCase().replace(/[^a-z0-9]+/g, '');
760
- const fullLen = String(row.code || '').length;
761
- const isTrunc = /…\[truncated\]$/.test(String(row.code || ''));
762
- if (isTrunc) {
763
- 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' });
764
- steps.push('Code is paginated (' + fullLen + ' chars shown of more): install part files in order, or re-fetch for the full body.');
765
- } else {
766
- files.push({ path: 'components/' + (row.slug || row.id) + '.html', contains: 'section markup plus its style block' });
767
- }
768
- steps.push('Scope global CSS before pasting: prefix bare selectors (.button, .inner, .point) with .' + scope + ' to avoid collisions in multi-component pages.');
769
- steps.push('Paste the markup where the section belongs and update the CTA link (it ships as #).');
770
- if (url) steps.push('Preview first: ' + url);
771
- 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.');
772
- if (needs.fonts.length) files.push({ path: 'styles/fonts.css', contains: 'font families referenced: ' + needs.fonts.join(', ') });
773
- return { kind, title: row.title, page_url: url, free: row.premium === false, steps, deps, files, needs, css_scope: scope, code_truncated: isTrunc };
774
- }
775
- if (kind === 'skills') {
776
- steps.push('Read the SKILL.md content from aura_get_skill first: it names its own triggers and pitfalls.');
777
- steps.push('Save it as SKILL.md inside your agent skills folder so the agent can load it.');
778
- if (row.source_url) steps.push('Upstream source: ' + row.source_url);
779
- return { kind, title: row.title, page_url: url, steps, deps, files: [{ path: 'skills/' + row.id + '/SKILL.md', contains: 'full skill content' }] };
780
- }
781
- if (kind === 'design_systems') {
782
- // ISS6: derive preview deps from preview_html so agents don't ship static pages.
783
- const ph = String(row.preview_html || '');
784
- const pl = ph.toLowerCase();
785
- const has = (...ss) => ss.some((s) => pl.includes(s));
786
- if (has('cdn.tailwindcss.com', 'tailwind')) deps.push({ name: 'tailwindcss', via: 'CDN (in preview_html)', cdn: GUIDE_CDN.tailwind });
787
- if (has('iconify', '<svg', 'lucide', 'data-lucide')) deps.push({ name: 'iconify-icon', via: 'CDN (in preview_html)', cdn: GUIDE_CDN.iconify });
788
- 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' });
789
- 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' });
790
- const fonts = new Set();
791
- for (const m of pl.matchAll(/family=([a-z+]+)/g)) fonts.add(m[1].replace(/\+/g, '-'));
792
- if (fonts.size) deps.push({ name: 'google-fonts:' + [...fonts].slice(0, 4).join(','), via: 'link (in preview_html)' });
793
- steps.push('Copy the DESIGN.md content into your repo as DESIGN.md and treat it as the source of truth.');
794
- steps.push('Apply the color, type, and spacing tokens before copying any component markup.');
795
- if (deps.length) steps.push('Install preview deps first (' + deps.map((d) => d.name).join(', ') + ') — the preview_html needs them.');
796
- return { kind, title: row.title, page_url: url, steps, deps, files: [{ path: 'DESIGN.md', contains: 'tokens and rules' }] };
797
- }
798
- steps.push('Use the image_800w URL for previews and image_original for production.');
799
- return { kind, title: row.title, page_url: url, steps, deps, files };
800
- };
801
-
802
- // ISS5: full token surface — colors + typography + spacing + radius — so scaffolded
803
- // pages don't lose the system. Also detects preview dark/light mode so agents don't
804
- // apply light-cream tokens to a dark preview body.
805
- function previewMode(previewHtml) {
806
- const h = String(previewHtml || '').toLowerCase();
807
- if (/bg-black|bg-neutral-950|bg-zinc-950|bg-slate-950|bg-\[#0/.test(h)) return 'dark';
808
- if (/bg-white|bg-neutral-50|bg-slate-50/.test(h)) return 'light';
809
- return 'unknown';
810
- }
811
-
812
- function tokenHints(content, previewHtml) {
813
- const Q = String.fromCharCode(39);
814
- const lines = String(content || '').split(Q + 'n' === Q + 'n' ? '\n' : '\n');
815
- const get = (key) => {
816
- 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 + ":'"); });
817
- if (!line) return null;
818
- const t = line.trim();
819
- const ci = t.toLowerCase().indexOf(key);
820
- let v = t.slice(ci + key.length).trim();
821
- if (v.startsWith(':')) v = v.slice(1).trim();
822
- while (v.startsWith(Q) || v.startsWith('"')) v = v.slice(1);
823
- while (v.endsWith(Q) || v.endsWith('"')) v = v.slice(0, -1);
824
- return v.trim() || null;
825
- };
826
- const tokens = {
827
- primary: get('primary'),
828
- secondary: get('secondary'),
829
- accent: get('accent'),
830
- background: get('background'),
831
- surface: get('surface'),
832
- text_primary: get('text-primary') || get('text_primary'),
833
- text_secondary: get('text-secondary') || get('text_secondary'),
834
- border: get('border'),
835
- display_font: get('display_lg_fontfamily') || get('display-font'),
836
- body_font: get('body_md_fontfamily') || get('body-font'),
837
- mono_font: get('label_md_fontfamily') || get('mono-font'),
838
- section_padding: get('section-padding') || get('section_padding'),
839
- card_padding: get('card-padding') || get('card_padding'),
840
- card_radius: get('card') === null ? get('card_radius') : null,
841
- control_radius: get('control'),
842
- pill_radius: get('pill'),
843
- };
844
- const pick = (obj, keys) => { const o = {}; for (const k of keys) if (obj[k]) o[k] = obj[k]; return o; };
845
- const colors = pick(tokens, ['primary', 'secondary', 'accent', 'background', 'surface', 'text_primary', 'text_secondary', 'border']);
846
- const rows = [];
847
- for (const [k, v] of Object.entries(colors)) rows.push(' --aura-' + k.replace(/_/g, '-') + ': ' + v + ';');
848
- if (tokens.body_font) rows.push(' --aura-font-body: ' + tokens.body_font + ';');
849
- if (tokens.display_font) rows.push(' --aura-font-display: ' + tokens.display_font + ';');
850
- if (tokens.mono_font) rows.push(' --aura-font-mono: ' + tokens.mono_font + ';');
851
- if (tokens.section_padding) rows.push(' --aura-section-padding: ' + tokens.section_padding + ';');
852
- if (tokens.card_padding) rows.push(' --aura-card-padding: ' + tokens.card_padding + ';');
853
- if (tokens.card_radius) rows.push(' --aura-radius-card: ' + tokens.card_radius + ';');
854
- if (tokens.control_radius) rows.push(' --aura-radius-control: ' + tokens.control_radius + ';');
855
- if (tokens.pill_radius) rows.push(' --aura-radius-pill: ' + tokens.pill_radius + ';');
856
- const css = rows.length ? ':root{' + '\n' + rows.join('\n') + '\n}' : '';
857
- const mode = previewMode(previewHtml);
858
- const warn = (mode === 'dark' && tokens.background && !/000|0f1115|111827|1a1a1a|09090b/i.test(tokens.background))
859
- ? 'tokens say light background ' + tokens.background + ' but preview_html renders dark — follow the preview body, not the tokens, or ask which mode is wanted'
860
- : ((mode === 'light' && tokens.background && /000000|0f1115/i.test(tokens.background))
861
- ? 'tokens say dark background but preview renders light — follow the preview body'
862
- : null);
863
- return { tokens, css, preview_mode: mode, mode_warning: warn };
864
- };
865
-
866
- // ---- tools.mjs ----
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 ----
867
1098
  // Module: tools — thin adapters. Each tool validates args at the seam,
868
- // calls one catalog/guide method, wraps textResult. No retries/caching here.
1099
+ // calls one catalog/guide method, wraps textResult. No retries/caching here
1100
+ // (except aura_recommend's single bounded auto-retry + editorial fill).
869
1101
  const TOOL_KINDS = ['components', 'skills', 'assets', 'design_systems'];
870
1102
  const TOOL_SORTS = ['popular', 'recent', 'trending', 'updated'];
871
1103
  const TOOL_CATS = ['hero','section','button','card','background','header','logo','feature','pricing','testimonial','footer','form','heading'];
@@ -876,28 +1108,48 @@ function toolFn_bool(v, name) { if (v !== undefined && typeof v !== 'boolean') t
876
1108
  function toolFn_sort(v) { if (v !== undefined && !TOOL_SORTS.includes(v)) toolFn_bad('sort must be one of ' + TOOL_SORTS.join('|')); return v; }
877
1109
  function theme(v) { if (v !== undefined && v !== 'dark' && v !== 'light') toolFn_bad('theme must be dark|light'); return v; }
878
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
+ }
879
1131
  function toolFn_cat(v) { if (v !== undefined && !TOOL_CATS.includes(v)) toolFn_bad('category must be one of ' + TOOL_CATS.join('|')); return v; }
880
1132
  const TOOL_DEFS = [
881
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 } },
882
- { 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. Items carry facets (theme/weight/needs).', 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' } }, additionalProperties: false } },
883
- { 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).', inputSchema: { type: 'object', properties: { id: {}, slug: { type: 'string' }, chunk: { type: 'number' } }, additionalProperties: false } },
884
- { name: 'aura_search_skills', description: 'Search free Aura agent skills (187). Metadata only; use aura_get_skill for the full SKILL.md content.', inputSchema: { type: 'object', properties: { query: { type: 'string' }, sort: { type: 'string', enum: TOOL_SORTS }, limit: { type: 'number' }, offset: { type: 'number' } }, 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 } },
885
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 } },
886
- { name: 'aura_search_assets', description: 'Search free Aura assets (images and video). Keywords, media_type image or video.', 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' } }, additionalProperties: false } },
887
- { name: 'aura_search_design_systems', description: 'Search free Aura DESIGN.md systems (725). Metadata only; use aura_get_design_system for content.', inputSchema: { type: 'object', properties: { query: { type: 'string' }, sort: { type: 'string', enum: TOOL_SORTS }, limit: { type: 'number' }, offset: { type: 'number' } }, 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 } },
888
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 } },
889
- { name: 'aura_search_all', description: 'One call across components, skills, assets, and design systems in parallel. Free only.', inputSchema: { type: 'object', properties: { query: { type: 'string' }, limit: { type: 'number' } }, additionalProperties: false } },
890
- { name: 'aura_recommend', description: 'Starter kit for a goal: top free components, skills, design systems, and assets with page URLs and rationale.', inputSchema: { type: 'object', properties: { goal: { type: 'string' } }, required: ['goal'], 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 } },
891
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 } },
892
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 } },
893
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 } },
894
- { name: 'aura_trending', description: 'What is new and popular across the free catalogue: top components, skills, assets, design systems in one call.', inputSchema: { type: 'object', properties: { limit: { type: 'number' } }, additionalProperties: false } },
895
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 } },
896
- { 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.', inputSchema: { type: 'object', properties: { goal: { type: 'string' }, system: { type: 'string' }, components: { type: 'array', items: {} } }, required: ['goal'], 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 } },
897
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 } },
898
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 } },
899
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 } },
900
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 } },
901
1153
  ];
902
1154
  function createHandlers(catalog) {
903
1155
  const free = { freeOnly: true };
@@ -908,38 +1160,49 @@ function createHandlers(catalog) {
908
1160
  negative_theme: a.negative_theme === undefined ? undefined : theme(a.negative_theme),
909
1161
  min_views: toolFn_num(a.min_views, 'min_views'),
910
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'),
911
1165
  })); },
912
1166
  aura_get_component: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
913
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 }));
914
1170
  const chunk = a.chunk === undefined ? 0 : toolFn_num(a.chunk, 'chunk');
915
1171
  return textResult(await catalog.getItem('components', id, { chunk })); },
916
1172
  aura_search_skills: async (a) => { a = a || {}; return textResult(await catalog.searchCatalog('skills', {
917
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'),
918
1176
  })); },
919
1177
  aura_get_skill: async (a) => { a = a || {}; if (typeof a.id !== 'string' || !a.id) toolFn_bad('id (string) is required');
920
1178
  const chunk = a.chunk === undefined ? 0 : toolFn_num(a.chunk, 'chunk');
921
1179
  return textResult(await catalog.getItem('skills', a.id, { chunk })); },
922
1180
  aura_search_assets: async (a) => { a = a || {}; const co = a.commercial_ok === undefined ? undefined : toolFn_bool(a.commercial_ok, 'commercial_ok');
923
- 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') });
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') });
924
1182
  // Gap 5: commercial_ok filter (all-rights-reserved per Aura Terms S4).
925
1183
  // commercial_ok:true returns [] with explanation, never silence.
926
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.' });
927
- if (co === true) return textResult({ ...r, items: [], commercial_ok: false, commercial_note: 'x' });
928
- if (co === true) return textResult({ ...r, items: [], commercial_ok: false, commercial_note: 'All Aura catalogue assets are all-rights-reserved per Aura Terms S4.' });
929
1185
  return textResult(r); },
930
1186
  aura_search_design_systems: async (a) => { a = a || {}; return textResult(await catalog.searchCatalog('design_systems', {
931
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'),
932
1190
  })); },
933
1191
  aura_get_design_system: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
934
1192
  if (typeof id !== 'string' || !id) toolFn_bad('provide id or slug (string)');
935
1193
  const chunk = a.chunk === undefined ? 0 : toolFn_num(a.chunk, 'chunk');
936
1194
  return textResult(await catalog.getItem('design_systems', id, { chunk })); },
937
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');
938
1201
  const r = await Promise.all([
939
- catalog.searchCatalog('components', { query, freeOnly: true, limit }),
940
- catalog.searchCatalog('skills', { query, limit }),
941
- catalog.searchCatalog('assets', { query, freeOnly: true, limit }),
942
- catalog.searchCatalog('design_systems', { query, limit }),
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 }),
943
1206
  ]);
944
1207
  return textResult({ query: query || null, components: r[0], skills: r[1], assets: r[2], design_systems: r[3] }); },
945
1208
  aura_recommend: async (a) => { a = a || {}; if (typeof a.goal !== 'string' || !a.goal.trim()) toolFn_bad('goal (string) is required');
@@ -948,16 +1211,44 @@ function createHandlers(catalog) {
948
1211
  const themeV = a.theme === undefined ? undefined : theme(a.theme);
949
1212
  const negative_themeV = a.negative_theme === undefined ? undefined : theme(a.negative_theme);
950
1213
  const min_views = toolFn_num(a.min_views, 'min_views');
1214
+ const slim = a.slim === undefined ? undefined : toolFn_bool(a.slim, 'slim');
951
1215
  const r = await Promise.all([
952
- catalog.searchCatalog('components', { query: a.goal, freeOnly: true, limit: 8, theme: themeV, negative_theme: negative_themeV, min_views }),
953
- catalog.searchCatalog('skills', { query: a.goal, limit: 5 }),
954
- catalog.searchCatalog('assets', { query: a.goal, freeOnly: true, limit: 3 }),
955
- catalog.searchCatalog('design_systems', { query: a.goal, limit: 3 }),
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 }),
956
1220
  ]);
957
- r[0].items = r[0].items.slice(0, 5);
958
- const pick = (x) => (x.items || []).slice(0, 3).map((i) => ({ title: i.title, page_url: i.page_url }));
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 }));
959
1249
  return textResult({ goal: a.goal, freeOnly: true,
960
- rationale: 'Top free catalogue hits per surface, ranked by Aura views. Open the page_url previews, then fetch full source or content for finalists.',
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.',
961
1252
  components: r[0], skills: r[1], assets: r[2], design_systems: r[3],
962
1253
  starter_kit: { components: pick(r[0]), skills: pick(r[1]), assets: pick(r[2]), design_systems: pick(r[3]) } }); },
963
1254
  aura_install_component: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
@@ -972,20 +1263,11 @@ function createHandlers(catalog) {
972
1263
  if (typeof id !== 'string' || !id) toolFn_bad('provide id or slug (string)');
973
1264
  const got = await catalog.getItem('design_systems', id);
974
1265
  return textResult({ item: got.item, install: installGuide('design_systems', got.item), tokens: tokenHints(got.item.content || '', got.item.preview_html || '') }); },
975
- aura_trending: async (a) => { a = a || {}; const limit = toolFn_num(a.limit, 'limit') || 5;
976
- // Gap 2: 30d rolling window + editorial picks. Components at 90d/views are noise
977
- // (total 2, views 0-5); skills/assets already work and keep their own signal.
978
- const windowDays = a.window_days === undefined ? 30 : toolFn_num(a.window_days, 'window_days');
979
- const r = await Promise.all([
980
- catalog.searchCatalog('components', { freeOnly: true, sort: 'trending', limit, window_days: windowDays }),
981
- catalog.searchCatalog('skills', { sort: 'trending', limit }),
982
- catalog.searchCatalog('assets', { freeOnly: true, sort: 'trending', limit }),
983
- catalog.searchCatalog('design_systems', { sort: 'trending', limit }),
984
- ]);
985
- const picks = await catalog.editorialPicks(Math.min(limit, 5)).catch(() => []);
986
- return textResult({ window: 'last ' + windowDays + ' days by views', window_note: 'Components at 90d/views are low-signal (total 2, views 0-5): see editorial_picks for human-grade component leads; skills/assets trending is organic.', editorial_picks: picks, components: r[0], skills: r[1], assets: r[2], design_systems: r[3] });},
987
- aura_categories: async () => textResult({ categories: await catalog.categoryCounts() }),
988
- aura_bundle: async (a) => { a = a || {}; const ids = idList(a.ids, 'ids') || idList(a.slugs, 'slugs');
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');
989
1271
  if (!ids || !ids.length) toolFn_bad('provide ids (array of 1-8 numbers/strings) or slugs (array of strings)');
990
1272
  return textResult({ kind: 'components', results: await catalog.bundleItems('components', ids) }); },
991
1273
  aura_related: async (a) => { a = a || {}; const k = a.kind;
@@ -998,15 +1280,17 @@ function createHandlers(catalog) {
998
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.'] } }); },
999
1281
  aura_bulk_fetch: async (a) => { a = a || {}; const kind = a.kind;
1000
1282
  if (kind !== 'components' && kind !== 'design_systems' && kind !== 'assets') toolFn_bad('kind must be components|design_systems|assets');
1001
- const ids = idList(a.ids, 'ids');
1283
+ const ids = normRefs(a.ids, 'ids');
1002
1284
  if (!ids || !ids.length) toolFn_bad('provide ids (array of 1-8)');
1003
1285
  return textResult({ kind, results: await catalog.bundleItems(kind, ids) }); },
1004
1286
  aura_scaffold_page: async (a) => { a = a || {}; if (typeof a.goal !== 'string' || !a.goal.trim()) toolFn_bad('goal (string) is required');
1005
1287
  const sysRef = (typeof a.system === 'string' && a.system) ? a.system : null;
1006
- const compRefs = idList(a.components, 'components') || [];
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 };
1007
1291
  const [sys, comps] = await Promise.all([
1008
- sysRef ? catalog.getItem('design_systems', sysRef).catch(() => null) : catalog.searchCatalog('design_systems', { query: a.goal, limit: 1 }).then((r) => (r.items[0] ? { item: r.items[0] } : null)),
1009
- compRefs.length ? catalog.bundleItems('components', compRefs).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),
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),
1010
1294
  ]);
1011
1295
  const sysItem = sys && sys.item ? sys.item : null;
1012
1296
  const toks = sysItem ? tokenHints(sysItem.content || '') : { tokens: {}, css: '' };
@@ -1016,75 +1300,75 @@ function createHandlers(catalog) {
1016
1300
  const files = [{ path: 'styles/tokens.css', contains: 'tokens.css from use_design_system' }];
1017
1301
  if (sysItem) files.push({ path: 'DESIGN.md', contains: 'system rules' });
1018
1302
  for (const g of guides) for (const f of (g.files || [])) files.push(f);
1019
- 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 }); },
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.' }); },
1020
1304
  };
1021
1305
  };
1022
-
1023
- // ---- server.mjs (main) ----
1024
- // Module: server — composition root. Wires config -> fetcher -> catalog ->
1025
- // tools -> protocol session over stdio. Only framing + wiring lives here.
1026
-
1027
-
1028
-
1029
-
1030
-
1031
-
1032
-
1033
- const config = loadConfig();
1034
- const fetcher = createFetcher({ fetchImpl: globalThis.fetch, timeoutMs: config.timeoutMs, retries: config.retries, userAgent: config.userAgent });
1035
- const catalog = createCatalog({ fetcher, config, facetsFn: facets });
1036
- const handlers = createHandlers(catalog);
1037
-
1038
- let buffer = "";
1039
- const send = (msg) => process.stdout.write(JSON.stringify(msg) + "\n");
1040
- const { dispatch } = createSession(send, { tools: TOOL_DEFS, handlers });
1041
-
1042
- process.stdin.setEncoding("utf8");
1043
- process.stdin.on("data", (chunk) => {
1044
- buffer += chunk;
1045
- let i;
1046
- while ((i = buffer.indexOf("\n")) >= 0) {
1047
- const line = buffer.slice(0, i).trim();
1048
- buffer = buffer.slice(i + 1);
1049
- if (!line) continue;
1050
- let msg;
1051
- try { msg = JSON.parse(line); }
1052
- catch { send({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } }); continue; }
1053
- dispatch(msg);
1054
- }
1055
- });
1056
- process.stdin.on("end", () => process.exit(0));
1057
- process.stdin.resume();
1058
-
1059
- // Startup cinematics live on stderr only: stdout is reserved for JSON-RPC.
1060
- // Sequence: aura moment -> catalogue credit -> builder credit -> running.
1061
- const BANNER = [
1062
- " <<< aura-components-mcp >>>",
1063
- " free Aura catalogue, every AI agent",
1064
- ];
1065
- const sleepMs = (ms) => new Promise((r) => setTimeout(r, ms));
1066
- const paint = (s) => process.stderr.write(s + "\n");
1067
- async function typeLine(text, cps = 90) {
1068
- for (const ch of text) { process.stderr.write(ch); await sleepMs(1000 / cps); }
1069
- process.stderr.write("\n");
1070
- }
1071
- async function dots(label, n = 3) {
1072
- process.stderr.write(label);
1073
- for (let i = 0; i < n; i++) { await sleepMs(160); process.stderr.write("."); }
1074
- process.stderr.write("\n");
1075
- }
1076
- async function boot() {
1077
- if (process.env.AURA_QUIET === "1" || process.argv.includes("--quiet")) {
1078
- paint(SERVER_INFO.name + " v" + SERVER_INFO.version + " listening on stdio");
1079
- return;
1080
- }
1081
- for (const l of BANNER) paint(l);
1082
- await typeLine("starting your aura for designing...");
1083
- await dots("warming catalogue cache");
1084
- paint("catalogue credit: Aura (aura.build) — components, skills, assets, systems");
1085
- await sleepMs(180);
1086
- paint("built by swastiksingh-dev — free to use, MIT, no paywall");
1087
- await sleepMs(180);
1088
- paint(SERVER_INFO.name + " v" + SERVER_INFO.version + " running — 15 tools on stdio");
1089
- }
1090
- boot();
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();