aura-components-mcp 1.6.2 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // aura-components-mcp v1.6.1 — bundled (zero deps). Built by swastiksingh-dev.
2
+ // aura-components-mcp v1.7.0 — bundled (zero deps). Built by swastiksingh-dev.
3
3
 
4
4
  // ---- config.mjs ----
5
5
  // Module: config — one small interface (loadConfig) over all env parsing.
@@ -30,7 +30,7 @@ function loadConfig(env = process.env) {
30
30
  codeChars: num(env.AURA_CODE_CHARS, 12_000, { min: 1_000, max: 60_000 }),
31
31
  contentChars: num(env.AURA_CONTENT_CHARS, 12_000, { min: 1_000, max: 60_000 }),
32
32
  freeOnlyDefault: bool(env.AURA_FREE_ONLY_DEFAULT, true),
33
- userAgent: env.AURA_USER_AGENT ?? "aura-components-mcp/1.0.0",
33
+ userAgent: env.AURA_USER_AGENT ?? "aura-components-mcp/1.7.0",
34
34
  };
35
35
  }
36
36
 
@@ -84,7 +84,34 @@ function createFetcher({ fetchImpl, timeoutMs, retries, userAgent }) {
84
84
  throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
85
85
  }
86
86
 
87
- return { getJson };
87
+ // GET raw text (icons). 404 returns null (fallback chain keeps walking);
88
+ // 429/5xx retry like getJson; other non-2xx also return null (a missing
89
+ // icon is normal, not an upstream failure).
90
+ async function getText(url, headers) {
91
+ let lastErr;
92
+ for (let attempt = 0; attempt <= retries; attempt++) {
93
+ try {
94
+ const res = await once(url, { headers });
95
+ if (res.status === 429 || (res.status >= 500 && res.status < 600)) {
96
+ lastErr = new Error("transient upstream status " + res.status);
97
+ } else if (res.status === 404) {
98
+ return null;
99
+ } else if (!res.ok) {
100
+ return null;
101
+ } else {
102
+ return await res.text();
103
+ }
104
+ } catch (err) {
105
+ lastErr = err;
106
+ if (err && err.name === "AbortError") break;
107
+ }
108
+ if (attempt < retries) await sleep(120 * 2 ** attempt + Math.floor(Math.random() * 60));
109
+ }
110
+ if (lastErr) throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
111
+ return null;
112
+ }
113
+
114
+ return { getJson, getText };
88
115
  }
89
116
 
90
117
  // ---- catalog.mjs ----
@@ -155,12 +182,40 @@ function scoreRows(kind, rows, toks, themeHint) {
155
182
  return scored;
156
183
  }
157
184
 
185
+ // Kind-aware token overlap for the zero-overlap refusal gate (pure, no fetch).
186
+ // Components/assets: title-only (strict visual surfaces; tags/descriptions don't count).
187
+ // Skills/design_systems: wider net — title + description + tags + any snippet present.
188
+ function matchOverlap(kind, toks, item) {
189
+ const list = Array.isArray(toks) ? toks : [];
190
+ if (!list.length || !item) return 0;
191
+ if (kind === "components" || kind === "assets") {
192
+ const title = String(item.title || "").toLowerCase();
193
+ let n = 0;
194
+ for (const t of list) {
195
+ const tok = String(t ?? "").toLowerCase();
196
+ if (tok && title.includes(tok)) n++;
197
+ }
198
+ return n;
199
+ }
200
+ const parts = [String(item.title || ""), String(item.description || "")];
201
+ if (Array.isArray(item.tags)) parts.push(item.tags.map((x) => String(x)).join(" "));
202
+ for (const k of ["content_snippet", "content_excerpt", "preview_snippet", "tokens_snippet"]) {
203
+ if (typeof item[k] === "string" && item[k]) parts.push(item[k]);
204
+ }
205
+ const hay = parts.join("\n").toLowerCase();
206
+ let n = 0;
207
+ for (const t of list) {
208
+ const tok = String(t ?? "").toLowerCase();
209
+ if (tok && hay.includes(tok)) n++;
210
+ }
211
+ return n;
212
+ }
213
+
158
214
  function windowAgoIso(days) {
159
215
  const d = new Date();
160
216
  d.setDate(d.getDate() - days);
161
217
  return d.toISOString();
162
218
  }
163
- function weekAgoIso() { return windowAgoIso(7); }
164
219
  function trendingIso(days = 90) { return windowAgoIso(days); } // 7d seed is empty; callers pass window_days (default 30 for components now)
165
220
 
166
221
  function buildSearchParams(kind, q = {}) {
@@ -187,7 +242,7 @@ function buildSearchParams(kind, q = {}) {
187
242
  if (kind === "components") ors.push("title.ilike." + v + ",description.ilike." + v);
188
243
  else if (kind === "skills") ors.push("title.ilike." + v + ",description.ilike." + v);
189
244
  else if (kind === "assets") ors.push("title.ilike." + v + ",description.ilike." + v);
190
- else ors.push("title.ilike." + v + ",description.ilike." + v);
245
+ else ors.push("title.ilike." + v + ",description.ilike." + v + ",content.ilike." + v + ",preview_html.ilike." + v);
191
246
  }
192
247
  if (q.sort === "trending") filters.push("created_at=gte." + trendingIso(q.window_days ?? 90));
193
248
  return { params: p, filters, ors };
@@ -202,7 +257,6 @@ function buildUrl(base, table, params, filters, ors) {
202
257
 
203
258
  // Truncation with cursors: agents see full_length + chunk info instead of a silent
204
259
  // 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
260
  function chunkText(s, n, chunk = 0) {
207
261
  const full = String(s || "");
208
262
  const total = Math.max(1, Math.ceil(full.length / n));
@@ -210,6 +264,22 @@ function chunkText(s, n, chunk = 0) {
210
264
  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
265
  }
212
266
 
267
+ // Slim lists: drop heavy blobs (full code/content/preview_html) from list rows,
268
+ // keep excerpts + chunk pointers + full_fetch so agents fetch detail on purpose.
269
+ // getItem/bundleItems stay full — detail is their job. Default on (q.slim ?? true).
270
+ function slimRows(kind, items) {
271
+ return (items || []).map((it) => {
272
+ const o = { ...it };
273
+ if (kind === "components" && typeof o.code === "string") delete o.code; // keep code_excerpt + code_info + full_fetch
274
+ if (kind === "skills" && typeof o.content === "string") { o.content_excerpt = String(o.content).slice(0, 800); delete o.content; }
275
+ if (kind === "design_systems") {
276
+ if (typeof o.content === "string") { o.content_excerpt = String(o.content).slice(0, 800); delete o.content; }
277
+ if (typeof o.preview_html === "string") { o.preview_excerpt = String(o.preview_html).slice(0, 800); delete o.preview_html; }
278
+ }
279
+ return o;
280
+ });
281
+ }
282
+
213
283
  function pageUrl(kind, row) {
214
284
  if (kind === "components") return SITE + "/component/" + row.slug;
215
285
  if (kind === "skills") return SITE + "/skills/" + row.id;
@@ -225,11 +295,12 @@ function assetKey(row) {
225
295
 
226
296
  function shapeRow(kind, row, cfg, withFacets) {
227
297
  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).
298
+ // Facets probe the FULL available code (pre-truncation) so search facets equal
299
+ // detail facets and install-time needs: a short prefix missed late markers
300
+ // like data-lucide (id 1572 drift). Weight uses the true length (ISS1/2/3).
230
301
  if (kind === "components" && typeof base.code === "string") {
231
302
  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 */ } }
303
+ if (withFacets) { try { base.facets = withFacets(kind, base, { fullLength: base.code.length }); } catch { /* never break rows */ } }
233
304
  // Gap 3: code_excerpt (first 1200 chars, readable) + full_fetch flag. Agents skim
234
305
  // the excerpt, then get chunk N — no more blind 18k-70k dumps or silent cuts.
235
306
  base.code_excerpt = String(base.code).slice(0, 1200);
@@ -316,7 +387,9 @@ function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = nul
316
387
  // the cache key so filtered/unfiltered variants never collide.
317
388
  const negTheme = q.negative_theme ? String(q.negative_theme).toLowerCase() : "";
318
389
  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("|");
390
+ const slim = q.slim ?? true;
391
+ const exactFlag = q.exact_match === true ? "exact" : "fuzzy";
392
+ const key = ["search", kind, q.query ?? "", q.tag ?? "", q.mediaType ?? "", sort, limit, offset, freeOnly, negTheme, minViews, slim ? "slim" : "full", exactFlag].join("|");
320
393
  const hit = cacheGet(key);
321
394
  if (hit) return { ...hit, cached: true };
322
395
  if (inflight.has(key)) return inflight.get(key);
@@ -340,12 +413,13 @@ function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = nul
340
413
  for (const x of t.slice(0, 3)) s.push(x);
341
414
  return [...new Set(s)].slice(0, 4);
342
415
  };
343
- if ((!rows || !rows.length) && toks.length) {
416
+ if ((!rows || !rows.length) && toks.length && q.exact_match !== true) {
344
417
  // Pass 1: OR over tokens, same filters (drops the AND-phrase requirement).
345
418
  const orParts = [];
346
419
  for (const t of toks) {
347
420
  const v = ilike(t);
348
- orParts.push("title.ilike." + v + ",description.ilike." + v);
421
+ if (kind === "design_systems") orParts.push("title.ilike." + v + ",description.ilike." + v + ",content.ilike." + v + ",preview_html.ilike." + v);
422
+ else orParts.push("title.ilike." + v + ",description.ilike." + v);
349
423
  }
350
424
  const b1 = buildSearchParams(kind, { ...q, query: "", limit, offset, sort, freeOnly });
351
425
  try {
@@ -359,7 +433,17 @@ function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = nul
359
433
  try {
360
434
  const fb2 = await fetcher.getJson(buildUrl(base, kind, b2.params, b2.filters, orParts), headers);
361
435
  if (fb2.rows && fb2.rows.length) { rows = fb2.rows; total = fb2.total; fallback = "or-tokens-unwindowed:" + toks.join(","); }
362
- } catch { /* keep original empty result */ }
436
+ } catch { /* try safety net */ }
437
+ }
438
+ // Safety net: total miss on a text query still returns usable items.
439
+ // Same freeOnly/tag/mediaType filters, popular order, labeled honestly —
440
+ // true never-empty (unless upstream itself is down).
441
+ if ((!rows || !rows.length)) {
442
+ const b3 = buildSearchParams(kind, { ...q, query: "", limit, offset, sort: "popular", freeOnly });
443
+ try {
444
+ const fb3 = await fetcher.getJson(buildUrl(base, kind, b3.params, b3.filters, []), headers);
445
+ if (fb3.rows && fb3.rows.length) { rows = fb3.rows; total = fb3.total; fallback = "popular-safety-net"; }
446
+ } catch { /* keep empty: upstream truth */ }
363
447
  }
364
448
  }
365
449
  // ISS7: dedupe skills by source_url (same SKILL.md listed twice). First (most-viewed)
@@ -387,6 +471,8 @@ function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = nul
387
471
  // Gap 4: inline snippets — one extra batched fetch fills preview/tokens glimpses
388
472
  // into search rows (top N only) so agents decide without a second call per row.
389
473
  // Bounded: top 5 rows, client-side sliced to 800 chars. No extra call when empty.
474
+ // Same fetch also resolves facets.has_preview to a real boolean for those rows.
475
+ let presence = null;
390
476
  if ((kind === "design_systems" || kind === "skills") && rows.length) {
391
477
  const topIds = rows.slice(0, 5).map((r) => r.id);
392
478
  try {
@@ -394,6 +480,7 @@ function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = nul
394
480
  const sel = kind === "design_systems" ? "id,content,preview_html" : "id,content,source_url";
395
481
  const { rows: full } = await fetcher.getJson(base + "/rest/v1/" + kind + "?select=" + encodeURIComponent(sel) + "&id=in.(" + idList + ")", headers);
396
482
  const byId = new Map(full.map((r) => [String(r.id), r]));
483
+ if (kind === "design_systems") presence = new Map(full.map((r) => [String(r.id), Boolean(r.preview_html)]));
397
484
  rows = rows.map((r) => {
398
485
  const f = byId.get(String(r.id));
399
486
  if (!f) return r;
@@ -415,29 +502,59 @@ function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = nul
415
502
  }
416
503
  // Theme-aware re-rank (components): theme hint from query (dark/light/cinematic)
417
504
  // 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.
505
+ // and always reports checked/kept counts so the re-rank is auditable, not opaque.
419
506
  if (kind === "components" && effTheme) {
420
507
  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;
508
+ const themeOkCount = scored.filter((s) => s.themeOk).length;
509
+ out.theme_hint = effTheme;
510
+ out.theme_checked = scored.length;
511
+ out.theme_kept = out.items.length;
512
+ if (themeOkCount === 0) {
429
513
  out.theme_warning = "no " + effTheme + "-theme rows in top results; showing unfiltered, check facets.theme per row";
514
+ } else {
515
+ out.items = scored.map((s) => s.r);
516
+ const dep = scored.length - themeOkCount;
517
+ if (dep > 0) out.theme_deprioritized = dep;
518
+ }
519
+ }
520
+ // Real has_preview for snippet-enriched rows (top 5); the rest stay unknown-list.
521
+ if (presence) {
522
+ for (const it of out.items) {
523
+ if (presence.has(String(it.id)) && it.facets) it.facets = { ...it.facets, has_preview: presence.get(String(it.id)) };
430
524
  }
431
525
  }
432
526
  // ISS15: transparent fallback — agents see what happened, not a magic string.
527
+ // Safety net additionally carries suggested_queries so agents can retry sharp.
528
+ 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"]; };
433
529
  if (fallback) {
434
530
  out.fallback = fallback; out.isFallback = true;
435
531
  const scoredTop = out.items[0];
436
- const topTitle = scoredTop ? String(scoredTop.title || "").toLowerCase() : "";
437
- const hits = toks.filter((t) => topTitle.includes(t)).length;
532
+ const hits = matchOverlap(kind, toks, scoredTop);
438
533
  out.fallback_score = "token-overlap:" + hits + "/" + toks.length + "-terms" + (effTheme ? ";theme:" + effTheme : "");
534
+ if (fallback === "popular-safety-net") out.suggested_queries = suggestList();
535
+ }
536
+ else if ((!rows || !rows.length) && toks.length) { out.isFallback = false; out.suggested_queries = suggestList(); }
537
+ // Zero-overlap refusal: fallback engaged but top row matches 0/N tokens —
538
+ // honest empty instead of irrelevant popular rows. Direct hits never refused.
539
+ // Kind-aware: components/assets stay title-only strict; skills/systems use the
540
+ // wider net (title+description+tags+snippets) via matchOverlap.
541
+ if (fallback != null && toks.length > 0) {
542
+ const top = out.items[0];
543
+ const hits2 = matchOverlap(kind, toks, top);
544
+ if (hits2 === 0) {
545
+ const refused = fallback;
546
+ out.items = [];
547
+ out.total = 0;
548
+ out.isFallback = false;
549
+ delete out.fallback;
550
+ delete out.fallback_score;
551
+ out.no_match = true;
552
+ out.refused_fallback = refused;
553
+ out.suggested_queries = suggestList();
554
+ out.note = "Top fallback result matched 0/" + toks.length + " title tokens — empty instead of irrelevant popular rows. Try a suggested query.";
555
+ }
439
556
  }
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"]; }
557
+ if (slim) out.items = slimRows(kind, out.items);
441
558
  cacheSet(key, out);
442
559
  return out;
443
560
  })();
@@ -447,7 +564,8 @@ function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = nul
447
564
 
448
565
  async function getItem(kind, idOrSlug, opts = {}) {
449
566
  const chunk = Math.max(0, opts.chunk ?? 0);
450
- const key = ["get", kind, String(idOrSlug), "c" + chunk].join("|");
567
+ const wantFull = !!opts.full;
568
+ const key = ["get", kind, String(idOrSlug), "c" + chunk, wantFull ? "full" : "trunc"].join("|");
451
569
  const hit = cacheGet(key);
452
570
  if (hit) return { ...hit, cached: true };
453
571
  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));
@@ -467,7 +585,7 @@ function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = nul
467
585
  // Chunked fetch: re-slice full blobs per requested chunk so agents can page
468
586
  // through 18k-70k payloads instead of hitting one silent truncation wall.
469
587
  const item = shapeRow(kind, full, config, facetsFn);
470
- if (chunk > 0) {
588
+ if (chunk > 0 && !wantFull) {
471
589
  if (kind === "components" && typeof full.code === "string") {
472
590
  const ch = chunkText(full.code, config.codeChars, chunk);
473
591
  item.code = ch.text;
@@ -491,6 +609,26 @@ function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = nul
491
609
  }
492
610
  }
493
611
  }
612
+ if (wantFull) {
613
+ if (kind === "components" && typeof full.code === "string") {
614
+ item.code = full.code;
615
+ item.code_info = { full_length: full.code.length, chunks: 1, chunk: 0, truncated: false };
616
+ }
617
+ if (kind === "skills" && typeof full.content === "string") {
618
+ item.content = full.content;
619
+ item.content_info = { full_length: full.content.length, chunks: 1, chunk: 0, truncated: false };
620
+ }
621
+ if (kind === "design_systems") {
622
+ if (typeof full.content === "string") {
623
+ item.content = full.content;
624
+ item.content_info = { full_length: full.content.length, chunks: 1, chunk: 0, truncated: false };
625
+ }
626
+ if (typeof full.preview_html === "string") {
627
+ item.preview_html = full.preview_html;
628
+ item.preview_info = { full_length: full.preview_html.length, chunks: 1, chunk: 0, truncated: false };
629
+ }
630
+ }
631
+ }
494
632
  const out = { kind, item, cached: false };
495
633
  cacheSet(key, out);
496
634
  return out;
@@ -512,7 +650,17 @@ function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = nul
512
650
  try { const { total } = await fetcher.getJson(base + "/rest/v1/" + path, headers); counts[k] = total; }
513
651
  catch { counts[k] = null; }
514
652
  }));
515
- const out = { site: SITE, catalogue: "aura.build public catalogue (PostgREST, anon)", counts, cached: false };
653
+ // Freshness: best-effort MAX(updated_at) per table so agents see staleness.
654
+ // Never throws; null on failure; cached with the status entry.
655
+ const freshness = { components: null, skills: null, assets: null, design_systems: null };
656
+ await Promise.all(Object.keys(freshness).map(async (table) => {
657
+ try {
658
+ const { rows } = await fetcher.getJson(base + "/rest/v1/" + table + "?select=updated_at&order=updated_at.desc&limit=1", headers);
659
+ const v = rows && rows[0] ? rows[0].updated_at : null;
660
+ freshness[table] = v ? String(v) : null;
661
+ } catch { freshness[table] = null; }
662
+ }));
663
+ const out = { site: SITE, catalogue: "aura.build public catalogue (PostgREST, anon)", counts, freshness, cached: false };
516
664
  cacheSet(key, out);
517
665
  return out;
518
666
  }
@@ -542,16 +690,16 @@ function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = nul
542
690
  try {
543
691
  const { rows } = await fetcher.getJson(url, headers);
544
692
  const enriched = await enrichAuthors(rows);
545
- return enriched.map((r) => shapeRow("components", r, config, facetsFn));
693
+ return enriched.map((r) => slimRows("components", [shapeRow("components", r, config, facetsFn)])[0]);
546
694
  } catch { return []; }
547
695
  }
548
696
 
549
697
  // Bulk fetch: N details in one parallel round (backs aura_bundle + aura_scaffold_page).
550
698
  // Per-id errors are captured, never thrown: { ok:true, item } or { ok:false, id, error }.
551
- async function bundleItems(kind, ids, max = 5) {
699
+ async function bundleItems(kind, ids, max = 5, opts = {}) {
552
700
  const list = [...new Set((ids || []).map(String))].slice(0, Math.max(1, Math.min(max, 8)));
553
701
  const out = await Promise.all(list.map(async (id) => {
554
- try { const got = await getItem(kind, id); return { ok: true, id, item: got.item }; }
702
+ try { const got = await getItem(kind, id, { full: !!opts.full }); return { ok: true, id, item: got.item }; }
555
703
  catch (e) { return { ok: false, id, error: (e && e.code === "NOT_FOUND") ? "not found" : String((e && e.message) || e).slice(0, 160) }; }
556
704
  }));
557
705
  return out;
@@ -574,7 +722,7 @@ function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = nul
574
722
  try {
575
723
  const { rows } = await fetcher.getJson(url, headers);
576
724
  const enriched = await enrichAuthors(rows);
577
- return { item: shapeRow(kind, item, config), related: enriched.map((r) => shapeRow(kind, r, config)) };
725
+ return { item: shapeRow(kind, item, config), related: slimRows(kind, enriched.map((r) => shapeRow(kind, r, config))) };
578
726
  } catch { return { item: shapeRow(kind, item, config), related: [] }; }
579
727
  }
580
728
 
@@ -582,7 +730,7 @@ function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = nul
582
730
  }
583
731
 
584
732
  // ---- protocol.mjs ----
585
- const SERVER_INFO = { name: "aura-components-mcp", version: "1.6.1" };
733
+ const SERVER_INFO = { name: "aura-components-mcp", version: "1.7.0" };
586
734
  // Module: protocol — stdio JSON-RPC framing + MCP handshake + error codes.
587
735
  // One interface: createSession(send) -> { dispatch(msg) }. No business logic.
588
736
 
@@ -710,6 +858,7 @@ function facets(kind, row, opts) {
710
858
  f.needsTailwind = needs.needsTailwind;
711
859
  f.needsIcons = needs.needsIcons;
712
860
  f.fonts = needs.fonts;
861
+ f.license = 'all-rights-reserved (Aura Terms §4) — check page_url before commercial reuse';
713
862
  }
714
863
  // ISS14 lives here too: license is top-level on every asset row, not buried.
715
864
  // ISS13: rewrite hotlinked demo images to stable asset references where possible.
@@ -770,7 +919,17 @@ function installGuide(kind, row) {
770
919
  if (url) steps.push('Preview first: ' + url);
771
920
  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
921
  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 };
922
+ // Chunk concat helper: truncated installs carry the exact re-fetch plan so no
923
+ // agent is left guessing how part files map to chunks.
924
+ const ci = row.code_info || null;
925
+ const concat = isTrunc ? {
926
+ tool: 'aura_get_component',
927
+ id: row.slug || row.id,
928
+ chunks: (ci && ci.chunks) || 'unknown — call once with chunk:0 and read code_info.chunks',
929
+ how: 'call aura_get_component with chunk:0..chunks-1 and concatenate code in order; part files above map 1:1 to chunks',
930
+ } : null;
931
+ 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';
932
+ 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 } : {}) };
774
933
  }
775
934
  if (kind === 'skills') {
776
935
  steps.push('Read the SKILL.md content from aura_get_skill first: it names its own triggers and pitfalls.');
@@ -799,6 +958,19 @@ function installGuide(kind, row) {
799
958
  return { kind, title: row.title, page_url: url, steps, deps, files };
800
959
  };
801
960
 
961
+ // Scaffold merge: one ordered page from a system + component rows. Owns the
962
+ // dep-dedupe and file-collection rules so the scaffold handler stays a thin
963
+ // adapter (fetch + freeOnly policy) and tests cross this seam, not the wire.
964
+ function combineScaffold(sysItem, comps) {
965
+ const guides = (comps || []).map((c) => installGuide('components', c));
966
+ const deps = []; const seen = new Set();
967
+ for (const g of guides) for (const d of (g.deps || [])) if (!seen.has(d.name)) { seen.add(d.name); deps.push(d); }
968
+ const files = [{ path: 'styles/tokens.css', contains: 'tokens.css from use_design_system' }];
969
+ if (sysItem) files.push({ path: 'DESIGN.md', contains: 'system rules' });
970
+ for (const g of guides) for (const f of (g.files || [])) files.push(f);
971
+ return { guides, combined_deps: deps, files };
972
+ }
973
+
802
974
  // ISS5: full token surface — colors + typography + spacing + radius — so scaffolded
803
975
  // pages don't lose the system. Also detects preview dark/light mode so agents don't
804
976
  // apply light-cream tokens to a dark preview body.
@@ -860,165 +1032,786 @@ function tokenHints(content, previewHtml) {
860
1032
  : ((mode === 'light' && tokens.background && /000000|0f1115/i.test(tokens.background))
861
1033
  ? 'tokens say dark background but preview renders light — follow the preview body'
862
1034
  : null);
863
- return { tokens, css, preview_mode: mode, mode_warning: warn };
1035
+ return { tokens, css, ...(mode === 'unknown' ? {} : { preview_mode: mode }), mode_warning: warn };
1036
+ }
1037
+
1038
+ // Module: prompt — deep module, small interface (buildPrompt) over all
1039
+ // need-based prompt rules. Pure, no fetch: same seam for tool + tests + docs.
1040
+ // Pass 1 (no answers): draft + questions[<=4] + next_call. Agent MUST ask the
1041
+ // user via native question UI before Pass 2. Pass 2 (with answers): final.
1042
+ const PROMPT_SOURCES = [
1043
+ 'https://www.aura.build/learn/tips-for-prompting',
1044
+ 'https://www.aura.build/learn/introduction',
1045
+ 'https://www.aura.build/design-systems',
1046
+ ];
1047
+ const PROMPT_CREDIT = 'Built by swastiksingh-dev — catalogue by Aura (aura.build)';
1048
+ const PROMPT_ORDER = ['1 tokens.css', '2 system preview_html', '3 component markup in listed order'];
1049
+
1050
+ function promptThemeHint(goal, explicit) {
1051
+ if (explicit === 'dark' || explicit === 'light') return explicit;
1052
+ const t = String(goal || '').toLowerCase();
1053
+ if (/\bdark\b|\bmidnight\b|\bnoir\b|\bcinematic\b/.test(t) && !/\blight\b|\bbright\b|\bairy\b/.test(t)) return 'dark';
1054
+ if (/\blight\b|\bbright\b|\bairy\b/.test(t)) return 'light';
1055
+ return null;
1056
+ }
1057
+
1058
+ function draftPromptText(goal, theme) {
1059
+ const th = theme ? theme + ' theme' : 'theme to confirm with user';
1060
+ 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.';
1061
+ }
1062
+
1063
+ function finalPromptText(goal, a) {
1064
+ const theme = a.theme || 'confirm with user';
1065
+ const sections = (a.sections && a.sections.length ? a.sections.join(', ') : 'hero + 2 sections');
1066
+ const system = a.system || 'auto-pick via aura_search_design_systems';
1067
+ const assets = a.assets || 'only if needed via aura_search_assets';
1068
+ return 'GOAL: ' + goal + '\n'
1069
+ + 'THEME: ' + theme + ' (tokens first, follow preview body on mismatch)\n'
1070
+ + 'SECTIONS: ' + sections + '\n'
1071
+ + 'DESIGN SYSTEM: ' + system + ' — copy DESIGN.md as source of truth, apply color/type/spacing before markup\n'
1072
+ + 'ASSETS: ' + assets + '\n'
1073
+ + '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'
1074
+ + 'ORDER: 1 tokens.css, 2 system preview_html, 3 component markup\n'
1075
+ + 'CALLS: aura_recommend -> aura_get_* finalists -> aura_use_design_system -> aura_scaffold_page';
1076
+ }
1077
+
1078
+ function buildPrompt(input = {}) {
1079
+ const goal = typeof input.goal === 'string' ? input.goal.trim() : '';
1080
+ if (!goal) { const e = new Error('goal (string) is required'); e.code = 'BAD_ARGS'; throw e; }
1081
+ const themeIn = input.theme === 'dark' || input.theme === 'light' ? input.theme : undefined;
1082
+ if (input.theme !== undefined && themeIn === undefined) { const e = new Error('theme must be dark|light'); e.code = 'BAD_ARGS'; throw e; }
1083
+ const theme = promptThemeHint(goal, themeIn);
1084
+ const system = typeof input.system === 'string' && input.system.trim() ? input.system.trim() : null;
1085
+ const surfaces = Array.isArray(input.surfaces) ? input.surfaces.filter((s) => typeof s === 'string').slice(0, 4) : [];
1086
+ const answers = input.answers && typeof input.answers === 'object' && !Array.isArray(input.answers) ? input.answers : null;
1087
+
1088
+ if (!answers) {
1089
+ const questions = [
1090
+ { key: 'theme', question: 'Which theme — dark or light?', options: ['dark', 'light'] },
1091
+ { key: 'sections', question: 'Which sections (e.g. hero, pricing, testimonial, footer)?', options: [] },
1092
+ { key: 'system', question: 'Which DESIGN system, or auto-pick?', options: system ? [system, 'auto-pick'] : ['auto-pick'] },
1093
+ { key: 'assets', question: 'Need images/video?', options: ['no', 'images', 'video'] },
1094
+ { key: 'macrostructure', question: 'Which page macrostructure (e.g. bento-grid, marquee-hero, long-document), or auto-pick from Inspo evidence?', options: ['auto-pick', 'bento-grid', 'marquee-hero', 'long-document', 'photographic', 'stat-led'] },
1095
+ { key: 'vibe', question: 'Which mood — calm, serious, luxe, playful, technical?', options: ['calm', 'serious', 'luxe', 'playful', 'technical', 'warm'] },
1096
+ ];
1097
+ return {
1098
+ phase: 'draft',
1099
+ goal,
1100
+ theme_hint: theme,
1101
+ draft_prompt: draftPromptText(goal, theme),
1102
+ questions,
1103
+ missing: ['theme', 'sections', 'system', 'assets', 'macrostructure', 'vibe'],
1104
+ next_call: { tool: 'aura_prompt', arguments: { goal, answers: { theme: theme || 'dark', sections: [], system: system || 'auto-pick', assets: 'no', macrostructure: 'auto-pick', vibe: 'calm' } } },
1105
+ plan_order: PROMPT_ORDER,
1106
+ sources: PROMPT_SOURCES,
1107
+ credit: PROMPT_CREDIT,
1108
+ inspo_axes: { mode_maps_to: 'theme', macrostructures: INSPO_MACROS.slice(0, 6), note: 'Run inspo.recommend first for evidence, then aura_inspo_recommend to map it onto Aura calls. Mode folds into theme.' },
1109
+ note: 'Ask the user these questions via native question UI, then call aura_prompt again with goal + answers. Never skip confirmation.',
1110
+ };
1111
+ }
1112
+
1113
+ const a = {
1114
+ theme: answers.theme === 'dark' || answers.theme === 'light' ? answers.theme : (theme || 'dark'),
1115
+ sections: Array.isArray(answers.sections) ? answers.sections.filter((s) => typeof s === 'string').slice(0, 6) : [],
1116
+ system: typeof answers.system === 'string' && answers.system.trim() ? answers.system.trim() : (system || 'auto-pick'),
1117
+ assets: typeof answers.assets === 'string' ? answers.assets : 'no',
1118
+ macrostructure: typeof answers.macrostructure === 'string' && answers.macrostructure.trim() ? answers.macrostructure.trim() : 'auto-pick',
1119
+ vibe: typeof answers.vibe === 'string' && answers.vibe.trim() ? answers.vibe.trim() : 'calm',
1120
+ };
1121
+ return {
1122
+ phase: 'final',
1123
+ goal,
1124
+ answers: a,
1125
+ final_prompt: finalPromptText(goal, a),
1126
+ plan_order: PROMPT_ORDER,
1127
+ suggested_calls: [
1128
+ { tool: 'inspo.recommend', arguments: { brief: goal, mode: a.theme, vibe: a.vibe, ...(a.macrostructure !== 'auto-pick' ? { macrostructure: a.macrostructure } : {}) } },
1129
+ { tool: 'aura_inspo_recommend', arguments: { brief: goal, mode: a.theme, vibe: a.vibe, ...(a.macrostructure !== 'auto-pick' ? { macrostructure: a.macrostructure } : {}) } },
1130
+ { tool: 'aura_recommend', arguments: { goal } },
1131
+ { tool: 'aura_search_all', arguments: { query: goal, limit: 3 } },
1132
+ { tool: 'aura_use_design_system', arguments: a.system !== 'auto-pick' ? { slug: a.system } : { slug: 'auto-pick: search first' } },
1133
+ { tool: 'aura_scaffold_page', arguments: { goal, system: a.system } },
1134
+ ],
1135
+ page_urls: ['https://www.aura.build/design-systems', 'https://www.aura.build/components'],
1136
+ sources: PROMPT_SOURCES,
1137
+ surfaces: surfaces.length ? surfaces : ['components', 'design_systems', 'assets', 'skills'],
1138
+ credit: PROMPT_CREDIT,
1139
+ };
1140
+ };
1141
+
1142
+ // Module: inspo-mappers — pure zero-dep translators between Inspo evidence and
1143
+ // Aura calls. No fetch: the agent pastes slim Inspo evidence in, gets Aura call
1144
+ // plans out. Same seam for tools + tests + docs (mirrors buildPrompt).
1145
+ const INSPO_MACROS = ['bento-grid', 'long-document', 'marquee-hero', 'stat-led', 'conversational-faq', 'manifesto', 'photographic', 'specimen', 'catalogue', 'letter', 'index-first', 'narrative-workflow', 'split-studio', 'feature-stack', 'type-specimen', 'portfolio-grid', 'map-diagram', 'ecosystem-index', 'component-playground'];
1146
+ const INSPO_VIBES = ['calm', 'loud', 'playful', 'serious', 'luxe', 'raw', 'soft', 'technical', 'warm', 'cold'];
1147
+ const INSPO_MODES = ['light', 'dark'];
1148
+ const INSPO_COMP_TYPES = ['hero', 'pricing', 'features', 'cta', 'nav', 'footer', 'testimonial', 'logo-cloud', 'faq', 'stat'];
1149
+ const INSPO_STYLES = ['minimalism', 'editorial', 'brutalism', 'glassmorphism', 'swiss', 'maximalism', 'playful', 'dark-mode', 'monochrome', 'vintage', 'futurist'];
1150
+ const INSPO_MACRO_TO_CATEGORY = {
1151
+ 'bento-grid': 'feature', 'long-document': 'section', 'marquee-hero': 'hero',
1152
+ 'stat-led': 'feature', 'conversational-faq': 'form', 'manifesto': 'heading',
1153
+ 'photographic': 'background', 'specimen': 'card', 'catalogue': 'card',
1154
+ 'letter': 'heading', 'index-first': 'header', 'narrative-workflow': 'section',
1155
+ 'split-studio': 'hero', 'feature-stack': 'feature', 'type-specimen': 'heading',
1156
+ 'portfolio-grid': 'card', 'map-diagram': 'feature', 'ecosystem-index': 'section',
1157
+ 'component-playground': 'section',
1158
+ };
1159
+ const INSPO_COMP_TO_CATEGORY = {
1160
+ hero: 'hero', pricing: 'pricing', features: 'feature', cta: 'section',
1161
+ nav: 'header', footer: 'footer', testimonial: 'testimonial',
1162
+ 'logo-cloud': 'logo', faq: 'form', stat: 'feature',
864
1163
  };
1164
+ const INSPO_LICENSE = 'Reference JSX is Inspo-canonical (copy-pasteable, tweak tokens to the brand). Aura markup/assets are all-rights-reserved per Aura Terms §4 — fine to prototype, check each page_url before commercial ship.';
1165
+ const INSPO_SLIM_KEYS = ['slug', 'palette', 'fonts', 'macro', 'macrostructure', 'mode', 'vibe', 'style', 'type', 'id', 'brief', 'brand_hex', 'background', 'text'];
1166
+ // Palette priority (locked): brand hex > Inspo evidence > Aura tokens. Aura keeps structure/spacing.
1167
+ const INSPO_ORDER = ['1 tokens.css', '2 system preview_html', '3 component markup in listed order'];
1168
+ const INSPO_IMAGE_PLAN = { draft: 'image_800w', prod: 'image_original', srcset: [320, 800, 1600], alt_from: 'title', swap_hotlinked: ['pravatar', 'placehold', 'dummyimage', 'example.com'], weight_cap: 's|m only — full:true on the winner alone' };
1169
+
1170
+ function inspoBad(msg) { const e = new Error(msg); e.code = 'BAD_ARGS'; throw e; }
1171
+ function inspoHex(v, name) {
1172
+ if (v === undefined) return undefined;
1173
+ if (typeof v !== 'string' || !/^#?[0-9a-f]{3}([0-9a-f]{3})?$|^#?[0-9a-f]{6}$/i.test(v.trim())) inspoBad(name + ' must be a hex color like "#c7402f"');
1174
+ const h = v.trim().replace(/^#/, '');
1175
+ return '#' + (h.length === 3 ? h.split('').map((c) => c + c).join('') : h).toLowerCase();
1176
+ }
1177
+ function inspoLum(hex) {
1178
+ const n = parseInt(hex.slice(1), 16);
1179
+ const f = (c) => { const s = c / 255; return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4); };
1180
+ return 0.2126 * f((n >> 16) & 255) + 0.7152 * f((n >> 8) & 255) + 0.0722 * f(n & 255);
1181
+ }
1182
+ function inspoContrastWarn(bg, text) {
1183
+ if (!bg || !text) return null;
1184
+ const l1 = inspoLum(bg), l2 = inspoLum(text);
1185
+ const ratio = (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
1186
+ return ratio < 4.5 ? 'contrast ' + ratio.toFixed(2) + ':1 (' + bg + ' on ' + text + ') is below 4.5 — ask the user which side moves' : null;
1187
+ }
1188
+ function inspoSlimCheck(ev, name) {
1189
+ if (!ev || typeof ev !== 'object' || Array.isArray(ev)) inspoBad(name + ' must be an object with slim keys only: ' + INSPO_SLIM_KEYS.join(','));
1190
+ for (const k of Object.keys(ev)) {
1191
+ if (!INSPO_SLIM_KEYS.includes(k)) inspoBad(name + '.' + k + ' is not slim — pass slugs+palette+fonts+macro+mode+vibe only, no autopsy/screenshots');
1192
+ }
1193
+ return ev;
1194
+ }
1195
+
1196
+ function buildInspoRecommend(input = {}) {
1197
+ const brief = typeof input.brief === 'string' ? input.brief.trim() : '';
1198
+ if (!brief) inspoBad('brief (string) is required');
1199
+ const mode = input.mode === undefined ? undefined : (INSPO_MODES.includes(input.mode) ? input.mode : inspoBad('mode must be light|dark'));
1200
+ const vibe = input.vibe === undefined ? undefined : (INSPO_VIBES.includes(input.vibe) ? input.vibe : inspoBad('vibe must be one of ' + INSPO_VIBES.join('|')));
1201
+ const macro = input.macrostructure === undefined ? undefined : (INSPO_MACROS.includes(input.macrostructure) ? input.macrostructure : inspoBad('macrostructure must be one of ' + INSPO_MACROS.join('|')));
1202
+ const brand = inspoHex(input.brand_hex, 'brand_hex');
1203
+ // Strict macro gate (locked): no macro on file means run Inspo first — never scaffold generic.
1204
+ if (!macro) {
1205
+ return {
1206
+ need_macro: true,
1207
+ brief, mode: mode || null, vibe: vibe || null, brand_hex: brand || null,
1208
+ inspo_call: { tool: 'inspo.recommend', arguments: { brief, ...(mode ? { mode } : {}), ...(vibe ? { vibe } : {}) } },
1209
+ note: 'No macrostructure yet — call inspo.recommend first, then call aura_inspo_recommend again with its macrostructure. Never scaffold without a macro.',
1210
+ palette_priority: ['brand_hex', 'inspo_evidence', 'aura_tokens'],
1211
+ order: INSPO_ORDER,
1212
+ license_note: INSPO_LICENSE,
1213
+ };
1214
+ }
1215
+ const category = INSPO_MACRO_TO_CATEGORY[macro];
1216
+ return {
1217
+ need_macro: false,
1218
+ brief, mode: mode || null, vibe: vibe || null, brand_hex: brand || null,
1219
+ macrostructure: macro, aura_category: category,
1220
+ inspo_call: { tool: 'inspo.recommend', arguments: { brief, macrostructure: macro, ...(mode ? { mode } : {}), ...(vibe ? { vibe } : {}) } },
1221
+ aura_calls: [
1222
+ { tool: 'aura_search_all', arguments: { query: brief, limit: 3, ...(mode ? { theme: mode } : {}) } },
1223
+ { tool: 'aura_recommend', arguments: { goal: brief, ...(mode ? { theme: mode } : {}) } },
1224
+ { tool: 'aura_search_components', arguments: { query: brief, category, ...(mode ? { theme: mode } : {}) } },
1225
+ ],
1226
+ palette_priority: ['brand_hex', 'inspo_evidence', 'aura_tokens'],
1227
+ image_plan: INSPO_IMAGE_PLAN,
1228
+ order: INSPO_ORDER,
1229
+ next: { tool: 'aura_inspo_components', arguments: { evidence: { type: 'hero', macrostructure: macro, ...(mode ? { mode } : {}) }, brief } },
1230
+ license_note: INSPO_LICENSE,
1231
+ };
1232
+ }
1233
+
1234
+ function buildInspoComponents(input = {}) {
1235
+ const ev = input.evidence && typeof input.evidence === 'object' && !Array.isArray(input.evidence) ? input.evidence : inspoBad('evidence {type, ...} is required');
1236
+ if (!INSPO_COMP_TYPES.includes(ev.type)) inspoBad('evidence.type must be one of ' + INSPO_COMP_TYPES.join('|'));
1237
+ if (ev.style !== undefined && !INSPO_STYLES.includes(ev.style)) inspoBad('evidence.style must be one of ' + INSPO_STYLES.join('|'));
1238
+ if (ev.macrostructure !== undefined && !INSPO_MACROS.includes(ev.macrostructure)) inspoBad('evidence.macrostructure must be one of ' + INSPO_MACROS.join('|'));
1239
+ if (ev.mode !== undefined && !INSPO_MODES.includes(ev.mode)) inspoBad('evidence.mode must be light|dark');
1240
+ for (const k of Object.keys(ev)) {
1241
+ if (!['type', 'style', 'macrostructure', 'mode', 'macro', 'vibe'].includes(k)) inspoBad('evidence.' + k + ' is not slim — type/style/macrostructure/mode only');
1242
+ }
1243
+ const brief = typeof input.brief === 'string' && input.brief.trim() ? input.brief.trim() : ev.type;
1244
+ const category = INSPO_COMP_TO_CATEGORY[ev.type];
1245
+ return {
1246
+ evidence: { ...ev },
1247
+ aura_category: category,
1248
+ inspo_call: { tool: 'inspo.find_components', arguments: { ...ev } },
1249
+ aura_search: { tool: 'aura_search_components', arguments: { query: brief, category, ...(ev.mode ? { theme: ev.mode } : {}) } },
1250
+ scaffold_hint: { order: INSPO_ORDER, weight_cap: 's|m only', tokens_first: true },
1251
+ image_plan: INSPO_IMAGE_PLAN,
1252
+ license_note: INSPO_LICENSE,
1253
+ };
1254
+ }
1255
+
1256
+ function buildInspoReference(input = {}) {
1257
+ const ev = input.evidence && typeof input.evidence === 'object' && !Array.isArray(input.evidence) ? input.evidence : inspoBad('evidence {type, id} is required');
1258
+ if (!INSPO_COMP_TYPES.includes(ev.type)) inspoBad('evidence.type must be one of ' + INSPO_COMP_TYPES.join('|'));
1259
+ if (typeof ev.id !== 'string' || !ev.id.trim()) inspoBad('evidence.id (string) is required');
1260
+ const ac = input.aura_component && typeof input.aura_component === 'object' && !Array.isArray(input.aura_component) ? input.aura_component : inspoBad('aura_component {id|slug} is required');
1261
+ const ref = typeof ac.id === 'string' && ac.id ? ac.id : (typeof ac.slug === 'string' && ac.slug ? ac.slug : inspoBad('aura_component needs id or slug'));
1262
+ const scope = 'aura-' + String(ref).toLowerCase().replace(/[^a-z0-9]+/g, '');
1263
+ return {
1264
+ evidence: { type: ev.type, id: ev.id.trim() },
1265
+ aura_component: ref,
1266
+ inspo_call: { tool: 'inspo.get_reference_jsx', arguments: { type: ev.type, id: ev.id.trim() } },
1267
+ aura_call: { tool: 'aura_get_component', arguments: { id: ref } },
1268
+ adapt_plan: {
1269
+ scope, css_scope_how: 'rewrite bare selectors as .' + scope + ' <selector>; leave Tailwind utilities, :root vars, @keyframes untouched',
1270
+ token_swaps: ['--inspo-* -> --aura-* (brand hex wins ties)'],
1271
+ files: [{ path: 'styles/tokens.css', contains: 'merged tokens first' }, { path: 'components/' + ref + '.html', contains: 'adapted markup' }],
1272
+ steps: ['Fetch both sources', 'Swap Inspo tokens to brand/Aura tokens', 'Scope CSS with .' + scope, 'Replace hotlinked demo images (see image_plan)'],
1273
+ },
1274
+ image_plan: INSPO_IMAGE_PLAN,
1275
+ license_note: INSPO_LICENSE,
1276
+ };
1277
+ }
1278
+
1279
+ function buildInspoSystem(input = {}) {
1280
+ const toks = input.inspo_tokens && typeof input.inspo_tokens === 'object' && !Array.isArray(input.inspo_tokens) ? inspoSlimCheck(input.inspo_tokens, 'inspo_tokens') : inspoBad('inspo_tokens {palette, fonts, ...} is required');
1281
+ const slug = typeof input.aura_system === 'string' && input.aura_system.trim() ? input.aura_system.trim()
1282
+ : (input.aura_system && typeof input.aura_system.slug === 'string' && input.aura_system.slug.trim() ? input.aura_system.slug.trim() : inspoBad('aura_system slug (string) is required'));
1283
+ const palette = Array.isArray(toks.palette) ? toks.palette.map((c) => inspoHex(c, 'inspo_tokens.palette[]')) : [];
1284
+ const brand = inspoHex(toks.brand_hex, 'inspo_tokens.brand_hex');
1285
+ const bg = toks.background ? inspoHex(toks.background, 'inspo_tokens.background') : (brand || palette[0]);
1286
+ const tx = toks.text ? inspoHex(toks.text, 'inspo_tokens.text') : null;
1287
+ const contrast_warning = bg && tx ? inspoContrastWarn(bg, tx) : null;
1288
+ return {
1289
+ aura_system: slug,
1290
+ palette_priority: ['brand_hex', 'inspo_evidence', 'aura_tokens'],
1291
+ optionA_inspo: { palette, fonts: Array.isArray(toks.fonts) ? toks.fonts.slice(0, 4) : [] },
1292
+ optionB_aura: { fetch: { tool: 'aura_get_design_system', arguments: { slug } }, note: 'Fetch Aura tokens, then compare side by side.' },
1293
+ optionC_blend: { colors_from: brand ? 'brand_hex' : 'inspo palette', structure_from: 'aura DESIGN.md', note: 'Brand/Inspo colors on Aura spacing/type skeleton.' },
1294
+ contrast_warning,
1295
+ question: { key: 'system_choice', question: 'Inspo palette and Aura tokens disagree — which wins?', options: ['A-inspo', 'B-aura', 'C-blend'] },
1296
+ files: [{ path: 'styles/tokens.css', contains: 'merged winning tokens — paste first' }, { path: 'DESIGN.md', contains: 'winning system rules' }],
1297
+ order: INSPO_ORDER,
1298
+ tokens_first: true,
1299
+ license_note: INSPO_LICENSE,
1300
+ };
1301
+ }
1302
+
1303
+ function buildInspoCompare(input = {}) { const list = Array.isArray(input.evidence) ? input.evidence : inspoBad('evidence must be an array of 2-4 slim site objects');
1304
+ if (list.length < 2 || list.length > 4) inspoBad('evidence must hold 2-4 sites — got ' + list.length);
1305
+ const sites = list.map((s, i) => {
1306
+ const o = inspoSlimCheck(s, 'evidence[' + i + ']');
1307
+ if (typeof o.slug !== 'string' || !o.slug.trim()) inspoBad('evidence[' + i + '].slug (string) is required');
1308
+ if (o.mode !== undefined && !INSPO_MODES.includes(o.mode)) inspoBad('evidence[' + i + '].mode must be light|dark');
1309
+ if (o.macrostructure !== undefined && !INSPO_MACROS.includes(o.macrostructure)) inspoBad('evidence[' + i + '].macrostructure must be one of ' + INSPO_MACROS.join('|'));
1310
+ return o;
1311
+ });
1312
+ const votes = {};
1313
+ for (const s of sites) for (const k of ['macrostructure', 'macro', 'mode']) {
1314
+ if (s[k]) votes[s[k]] = (votes[s[k]] || 0) + 1;
1315
+ }
1316
+ const top = Object.entries(votes).sort((a, b) => b[1] - a[1])[0];
1317
+ const house = { macrostructure: (sites.map((s) => s.macrostructure || s.macro).find(Boolean)) || null, mode: sites.map((s) => s.mode).find(Boolean) || null, agreement: top ? top[0] + ' x' + top[1] + '/' + sites.length : 'none' };
1318
+ const macro = house.macrostructure;
1319
+ const category = macro && INSPO_MACRO_TO_CATEGORY[macro] ? INSPO_MACRO_TO_CATEGORY[macro] : null;
1320
+ return {
1321
+ sites: sites.map((s) => s.slug),
1322
+ house_style: house,
1323
+ inspo_call: { tool: 'inspo.compare', arguments: { slugs: sites.map((s) => s.slug) } },
1324
+ closest_aura_systems_query: { tool: 'aura_search_design_systems', arguments: { query: macro || house.agreement, limit: 3 } },
1325
+ ranked_components_query: category ? { tool: 'aura_search_components', arguments: { query: macro, category } } : null,
1326
+ order: INSPO_ORDER,
1327
+ license_note: INSPO_LICENSE,
1328
+ };
1329
+ }
1330
+
1331
+ // Module: docs-helpers — pure planners linking Aura output to fresh Context7
1332
+ // docs. No fetch: the agent calls Context7, Aura shapes the lookup and applies
1333
+ // the answer. Same seam for tools + tests + docs.
1334
+ const DOCS_TRUSTED = {
1335
+ tailwind: { label: 'Tailwind CSS', resolve: 'tailwindcss', id: null, cdn: 'https://cdn.tailwindcss.com', aura: ['installGuide tailwindcss dep', 'styles/tokens.css', 'scaffold order'] },
1336
+ gsap: { label: 'GSAP + ScrollTrigger', resolve: 'gsap', id: null, cdn: 'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js', aura: ['installGuide gsap dep', 'preview_html motion'] },
1337
+ lenis: { label: 'Lenis smooth scroll', resolve: 'lenis', id: '/darkroomengineering/lenis', cdn: 'unpkg lenis@1 (confirm exact file via lookup)', aura: ['aura_motion_smooth wiring'] },
1338
+ iconify: { label: 'Iconify icons', resolve: 'iconify', id: null, cdn: 'https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js', aura: ['installGuide iconify-icon dep'] },
1339
+ };
1340
+ // Markers verified against renames we know happened; version pins below are
1341
+ // never declared stale — they go to needs_check for a live lookup instead.
1342
+ const DOCS_STALE_MARKERS = [
1343
+ { lib: 'lenis', re: /studio-freight/i, fix: 'Package renamed studio-freight -> lenis. Swap the import/CDN path, keep option names; look unchanged.' },
1344
+ ];
1345
+ const DOCS_PIN_HINTS = [
1346
+ { lib: 'tailwind', re: /cdn\.tailwindcss\.com/i, check: 'Confirm v3 Play CDN vs v4 @tailwindcss/browser build via lookup.' },
1347
+ { lib: 'gsap', re: /cdnjs\.cloudflare\.com\/ajax\/libs\/gsap\/[\d.]+/i, check: 'Confirm latest 3.x pin via lookup.' },
1348
+ { lib: 'lenis', re: /unpkg\.com\/lenis/i, check: 'Confirm latest 1.x file via lookup.' },
1349
+ { lib: 'iconify', re: /iconify-icon\/[\d.]+/i, check: 'Confirm latest iconify-icon pin via lookup.' },
1350
+ ];
1351
+ const DOCS_DEP_TO_LIB = { tailwindcss: 'tailwind', gsap: 'gsap', 'gsap-ScrollTrigger': 'gsap', 'iconify-icon': 'iconify' };
1352
+
1353
+ function docsBad(msg) { const e = new Error(msg); e.code = 'BAD_ARGS'; throw e; }
1354
+
1355
+ function docsStampFor(deps) {
1356
+ const libs = [];
1357
+ for (const d of (deps || [])) {
1358
+ const lib = DOCS_DEP_TO_LIB[d && d.name];
1359
+ if (lib && !libs.includes(lib)) libs.push(lib);
1360
+ }
1361
+ return {
1362
+ status: 'unchecked',
1363
+ needs_check: libs.map((l) => l + ': confirm CDN/version via aura_docs_plan + Context7'),
1364
+ note: 'Vendored Aura code is a snapshot — verify pins before shipping.',
1365
+ };
1366
+ }
1367
+
1368
+ function buildDocsPlan(input = {}) {
1369
+ const raw = typeof input.library === 'string' ? input.library.trim().toLowerCase() : '';
1370
+ if (!raw) docsBad('library (string) is required');
1371
+ const need = typeof input.need === 'string' ? input.need.trim() : '';
1372
+ if (!need) docsBad('need (string) is required — what question should the docs answer?');
1373
+ const t = DOCS_TRUSTED[raw] || null;
1374
+ return {
1375
+ library: raw,
1376
+ trusted: !!t,
1377
+ context7: t
1378
+ ? { resolve: t.resolve, ...(t.id ? { id: t.id } : {}), query: need, note: t.id ? 'ID verified — query it directly.' : 'Resolve the id first via Context7, then query.' }
1379
+ : { resolve: raw, query: need, note: 'Unlisted library — resolve the id first via Context7, then query. Apply guidance is generic.' },
1380
+ apply: t
1381
+ ? { aura_touchpoints: t.aura, steps: ['Look up via Context7', 'Adapt Aura code to the fresh API (keep the look identical)', 'Update the docs_stamp from unchecked to checked'] }
1382
+ : { aura_touchpoints: [], steps: ['Look up via Context7', 'Apply carefully — no Aura mapping exists for this library'] },
1383
+ stamp: { status: 'planned', needs_check: [raw + ': ' + need] },
1384
+ };
1385
+ }
1386
+
1387
+ function buildDocsCheck(input = {}) {
1388
+ const raw = typeof input.library === 'string' ? input.library.trim().toLowerCase() : '';
1389
+ if (!raw) docsBad('library (string) is required');
1390
+ const code = typeof input.code_excerpt === 'string' ? input.code_excerpt : docsBad('code_excerpt (string) is required');
1391
+ if (!code.trim()) docsBad('code_excerpt must not be empty');
1392
+ if (code.length > 20000) docsBad('code_excerpt is ' + code.length + ' chars — paste excerpts under 20000 chars, not full pages');
1393
+ const note = typeof input.docs_note === 'string' && input.docs_note.trim() ? input.docs_note.trim() : null;
1394
+ const fixes = [];
1395
+ for (const m of DOCS_STALE_MARKERS) {
1396
+ if (m.re.test(code)) fixes.push({ lib: m.lib, fix: m.fix });
1397
+ }
1398
+ const needs = [];
1399
+ for (const h of DOCS_PIN_HINTS) {
1400
+ if (h.re.test(code) && !needs.includes(h.lib + ': ' + h.check)) needs.push(h.lib + ': ' + h.check);
1401
+ }
1402
+ if (note) needs.push('agent-reported docs change: ' + note.slice(0, 200));
1403
+ return {
1404
+ library: raw,
1405
+ verdict: fixes.length ? 'needs-update' : 'current',
1406
+ fixes,
1407
+ needs_check: needs,
1408
+ rule: 'Fresh docs win on API, but edits must preserve the look — adapt Aura code, never restyle.',
1409
+ stamp: { status: fixes.length || note ? 'checked-with-findings' : 'checked', findings: fixes.length + (note ? 1 : 0) },
1410
+ };
1411
+ }
1412
+
1413
+ // ---- icons.mjs ----
1414
+ // Module: icons — live icon fetch (Font Awesome Free + HackerNoon pixel
1415
+ // library) over jsDelivr. One interface: getIcon({name, source, style}).
1416
+ // URLs verified live 2026-10-10; licenses verified against the repos'
1417
+ // own license pages (FA Free: CC BY 4.0 icons; pixel .svg/.png: CC BY 4.0).
1418
+
1419
+ const ICON_TTL_MS = 24 * 60 * 60 * 1000; // icons are static — remember 24h
1420
+ const ICON_MAX_KEYS = 500;
1421
+ const ICON_MAX_BYTES = 50 * 1024;
1422
+ const ICON_FA_BASE = "https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6/svgs";
1423
+ const ICON_FA_CSS = "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css";
1424
+ const ICON_PX_BASE = "https://cdn.jsdelivr.net/gh/hackernoon/pixel-icon-library@main/icons/SVG";
1425
+ const ICON_PX_CSS = "https://cdn.jsdelivr.net/npm/@hackernoon/pixel-icon-library@latest/fonts/iconfont.css";
1426
+ const ICON_FA_STYLES = ["solid", "regular", "brands"];
1427
+ const ICON_SOURCES = ["auto", "fontawesome", "pixel"];
1428
+ const ICON_STYLES = ["auto", "solid", "regular", "brands"];
1429
+
1430
+ function iconBad(msg) { const e = new Error(msg); e.code = "BAD_ARGS"; throw e; }
1431
+ function iconMissing(msg) { const e = new Error(msg); e.code = "NOT_FOUND"; throw e; }
1432
+
1433
+ const ICON_CREDITS = {
1434
+ fontawesome: {
1435
+ license: "CC BY 4.0 (icons) — https://fontawesome.com/license/free",
1436
+ credit: "Icon by Font Awesome Free (CC BY 4.0) — https://fontawesome.com",
1437
+ },
1438
+ pixel: {
1439
+ license: "CC BY 4.0 — https://github.com/hackernoon/pixel-icon-library#license-free--starter--pro",
1440
+ credit: "Icon by HackerNoon Pixel Icon Library (CC BY 4.0) — https://pixeliconlibrary.com (give credit + link, note changes)",
1441
+ },
1442
+ };
1443
+
1444
+ function iconCredit(source) {
1445
+ const c = ICON_CREDITS[source];
1446
+ if (!c) iconBad("source must be fontawesome|pixel");
1447
+ return { ...c };
1448
+ }
1449
+
1450
+ // Candidate URLs in fallback order. Pixel solid variants use a -solid suffix
1451
+ // (see iconfont.css hn-*-solid classes); FA uses per-style directories.
1452
+ function iconCandidates(name, source, style) {
1453
+ const out = [];
1454
+ const faStyles = style === "auto" ? ICON_FA_STYLES : [style];
1455
+ const pxCats = style === "auto" ? ["regular", "solid", "brands"] : [style];
1456
+ if (source === "auto" || source === "fontawesome") {
1457
+ for (const s of faStyles) out.push({ source: "fontawesome", style: s, url: ICON_FA_BASE + "/" + s + "/" + name + ".svg" });
1458
+ }
1459
+ if (source === "auto" || source === "pixel") {
1460
+ for (const c of pxCats) {
1461
+ out.push({ source: "pixel", style: c, url: ICON_PX_BASE + "/" + c + "/" + name + ".svg" });
1462
+ out.push({ source: "pixel", style: c, url: ICON_PX_BASE + "/" + c + "/" + name + "-solid.svg" });
1463
+ }
1464
+ }
1465
+ return out;
1466
+ }
1467
+
1468
+ function createIcons({ fetcher, now = () => Date.now() }) {
1469
+ if (!fetcher || typeof fetcher.getText !== "function") throw new Error("fetcher.getText is required");
1470
+ const cache = new Map();
1471
+
1472
+ async function getIcon(q = {}) {
1473
+ const raw = typeof q.name === "string" ? q.name.trim().toLowerCase() : "";
1474
+ if (!raw) iconBad("name (string) is required — e.g. {name:'bolt'}");
1475
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(raw)) iconBad("name must be kebab-case like 'bolt' or 'arrow-down'");
1476
+ const source = q.source === undefined ? "auto" : q.source;
1477
+ const style = q.style === undefined ? "auto" : q.style;
1478
+ if (!ICON_SOURCES.includes(source)) iconBad("source must be one of " + ICON_SOURCES.join("|"));
1479
+ if (!ICON_STYLES.includes(style)) iconBad("style must be one of " + ICON_STYLES.join("|"));
1480
+
1481
+ const key = [source, style, raw].join("|");
1482
+ const hit = cache.get(key);
1483
+ if (hit && now() - hit.t < ICON_TTL_MS) return { ...hit.v, cached: true };
1484
+ if (hit) cache.delete(key);
1485
+
1486
+ const tried = [];
1487
+ for (const c of iconCandidates(raw, source, style)) {
1488
+ let body = null;
1489
+ try {
1490
+ body = await fetcher.getText(c.url, {});
1491
+ } catch {
1492
+ tried.push({ url: c.url, ok: false, why: "fetch-error" });
1493
+ continue;
1494
+ }
1495
+ if (body === null || body === undefined) { tried.push({ url: c.url, ok: false, why: "404" }); continue; }
1496
+ const text = String(body).replace(/^\s*<\?xml[^?]*\?>\s*/, "");
1497
+ if (!text.trim().startsWith("<svg")) { tried.push({ url: c.url, ok: false, why: "not-svg" }); continue; }
1498
+ if (text.length > ICON_MAX_BYTES) { tried.push({ url: c.url, ok: false, why: "oversize" }); continue; }
1499
+ const lic = ICON_CREDITS[c.source];
1500
+ const out = {
1501
+ name: raw, source: c.source, style: c.style, svg: text, bytes: text.length,
1502
+ source_url: c.url, license: lic.license, credit: lic.credit,
1503
+ alt: c.source === "pixel"
1504
+ ? { iconfont_css: ICON_PX_CSS, class: "hn hn-" + raw, use: '<link rel="stylesheet" href="' + ICON_PX_CSS + '"> + <i class="hn hn-' + raw + '"></i>' }
1505
+ : { css: ICON_FA_CSS, class: "fa-" + c.style + " fa-" + raw, use: '<link rel="stylesheet" href="' + ICON_FA_CSS + '"> + <i class="fa-' + c.style + " fa-" + raw + '"></i>' },
1506
+ tried: tried.map((t) => t.url),
1507
+ cached: false,
1508
+ };
1509
+ cache.set(key, { t: now(), v: out });
1510
+ while (cache.size > ICON_MAX_KEYS) cache.delete(cache.keys().next().value);
1511
+ return out;
1512
+ }
1513
+ iconMissing("icon not found: '" + raw + "' (" + source + "/" + style + ") — checked " + tried.length + " URLs; check spelling (FA names use dashes, e.g. 'arrow-down')");
1514
+ }
1515
+
1516
+ return { getIcon };
1517
+ }
1518
+
1519
+ // ---- version.mjs ----
1520
+ // Module: version — running vs npm-published version check. One small
1521
+ // interface (getVersion) over a registry fetch with a long cache. Never
1522
+ // throws: offline or unpublished registries return latest:null and a note.
1523
+
1524
+ const VERSION_TTL_MS = 60 * 60 * 1000; // re-check npm at most hourly
1525
+ const VERSION_URL = "https://registry.npmjs.org/aura-components-mcp/latest";
1526
+ const VERSION_INSTALL = "npm i -g aura-components-mcp@latest (or npx -y aura-components-mcp@latest), then restart the MCP client";
1527
+
1528
+ function createVersion({ fetcher, running, now = () => Date.now() }) {
1529
+ if (!fetcher || typeof fetcher.getText !== "function") throw new Error("fetcher.getText is required");
1530
+ const run = typeof running === "string" && running ? running : "0.0.0-dev";
1531
+ let cached = null;
1532
+
1533
+ function cmp(a, b) {
1534
+ const pa = String(a).split(".").map((x) => parseInt(x, 10) || 0);
1535
+ const pb = String(b).split(".").map((x) => parseInt(x, 10) || 0);
1536
+ for (let i = 0; i < 3; i++) {
1537
+ if (pa[i] !== pb[i]) return pa[i] < pb[i] ? -1 : 1;
1538
+ }
1539
+ return 0;
1540
+ }
1541
+
1542
+ async function getVersion() {
1543
+ if (cached && now() - cached.t < VERSION_TTL_MS) return { ...cached.v, cached: true };
1544
+ let latest = null;
1545
+ try {
1546
+ const body = await fetcher.getText(VERSION_URL, { accept: "application/json" });
1547
+ const j = body ? JSON.parse(body) : null;
1548
+ if (j && typeof j.version === "string" && /^\d+\.\d+\.\d+/.test(j.version)) latest = j.version;
1549
+ } catch { latest = null; }
1550
+ const behind = latest !== null && cmp(run, latest) < 0;
1551
+ const v = latest === null
1552
+ ? { running: run, latest: null, update_available: false, cached: false, checked_at: new Date(now()).toISOString(), notice: "Could not reach the npm registry — running v" + run + ". Prefer the latest published version when you can check." }
1553
+ : behind
1554
+ ? { running: run, latest, update_available: true, cached: false, checked_at: new Date(now()).toISOString(), notice: "Update available: running v" + run + ", npm publishes v" + latest + ". Prefer latest — " + VERSION_INSTALL + "." }
1555
+ : { running: run, latest, update_available: false, cached: false, checked_at: new Date(now()).toISOString(), notice: "Running v" + run + " — latest published. Prefer latest on every new session." };
1556
+ cached = { t: now(), v };
1557
+ return { ...v };
1558
+ }
1559
+
1560
+ return { getVersion };
1561
+ }
865
1562
 
866
1563
  // ---- tools.mjs ----
867
- // Module: tools — thin adapters. Each tool validates args at the seam,
868
- // calls one catalog/guide method, wraps textResult. No retries/caching here.
869
- const TOOL_KINDS = ['components', 'skills', 'assets', 'design_systems'];
870
- const TOOL_SORTS = ['popular', 'recent', 'trending', 'updated'];
871
- const TOOL_CATS = ['hero','section','button','card','background','header','logo','feature','pricing','testimonial','footer','form','heading'];
872
- function toolFn_bad(msg) { const e = new Error(msg); e.code = 'BAD_ARGS'; throw e; }
873
- function toolFn_str(v, name) { if (v !== undefined && typeof v !== 'string') toolFn_bad(name + ' must be a string'); return v; }
874
- function toolFn_num(v, name) { if (v !== undefined && typeof v !== 'number') toolFn_bad(name + ' must be a number'); return v; }
875
- function toolFn_bool(v, name) { if (v !== undefined && typeof v !== 'boolean') toolFn_bad(name + ' must be a boolean'); return v; }
876
- function toolFn_sort(v) { if (v !== undefined && !TOOL_SORTS.includes(v)) toolFn_bad('sort must be one of ' + TOOL_SORTS.join('|')); return v; }
877
- function theme(v) { if (v !== undefined && v !== 'dark' && v !== 'light') toolFn_bad('theme must be dark|light'); return v; }
878
- 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; }
879
- function toolFn_cat(v) { if (v !== undefined && !TOOL_CATS.includes(v)) toolFn_bad('category must be one of ' + TOOL_CATS.join('|')); return v; }
880
- const TOOL_DEFS = [
881
- { 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 } },
885
- { 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 } },
888
- { 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 } },
891
- { 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
- { 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
- { 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
- { 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 } },
897
- { 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
- { 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
- { 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
- { name: 'aura_categories', description: 'The 13 component categories with live free counts. Pick one, then search within it.', inputSchema: { type: 'object', properties: {}, additionalProperties: false } },
901
- ];
902
- function createHandlers(catalog) {
903
- const free = { freeOnly: true };
904
- return {
905
- aura_status: async () => textResult(await catalog.getStatus()),
906
- aura_search_components: async (a) => { a = a || {}; return textResult(await catalog.searchCatalog('components', {
907
- query: toolFn_str(a.query, 'query'), tag: toolFn_cat(a.category), freeOnly: true, theme: theme(a.theme),
908
- negative_theme: a.negative_theme === undefined ? undefined : theme(a.negative_theme),
909
- min_views: toolFn_num(a.min_views, 'min_views'),
910
- sort: toolFn_sort(a.sort), limit: toolFn_num(a.limit, 'limit'), offset: toolFn_num(a.offset, 'offset'),
911
- })); },
912
- aura_get_component: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
913
- if (id === undefined || (typeof id !== 'string' && typeof id !== 'number')) toolFn_bad('provide id (number) or slug (string)');
914
- const chunk = a.chunk === undefined ? 0 : toolFn_num(a.chunk, 'chunk');
915
- return textResult(await catalog.getItem('components', id, { chunk })); },
916
- aura_search_skills: async (a) => { a = a || {}; return textResult(await catalog.searchCatalog('skills', {
917
- query: toolFn_str(a.query, 'query'), sort: toolFn_sort(a.sort), limit: toolFn_num(a.limit, 'limit'), offset: toolFn_num(a.offset, 'offset'),
918
- })); },
919
- aura_get_skill: async (a) => { a = a || {}; if (typeof a.id !== 'string' || !a.id) toolFn_bad('id (string) is required');
920
- const chunk = a.chunk === undefined ? 0 : toolFn_num(a.chunk, 'chunk');
921
- return textResult(await catalog.getItem('skills', a.id, { chunk })); },
922
- 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') });
924
- // Gap 5: commercial_ok filter (all-rights-reserved per Aura Terms S4).
925
- // commercial_ok:true returns [] with explanation, never silence.
926
- 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
- return textResult(r); },
930
- aura_search_design_systems: async (a) => { a = a || {}; return textResult(await catalog.searchCatalog('design_systems', {
931
- query: toolFn_str(a.query, 'query'), sort: toolFn_sort(a.sort), limit: toolFn_num(a.limit, 'limit'), offset: toolFn_num(a.offset, 'offset'),
932
- })); },
933
- aura_get_design_system: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
934
- if (typeof id !== 'string' || !id) toolFn_bad('provide id or slug (string)');
935
- const chunk = a.chunk === undefined ? 0 : toolFn_num(a.chunk, 'chunk');
936
- return textResult(await catalog.getItem('design_systems', id, { chunk })); },
937
- aura_search_all: async (a) => { a = a || {}; const query = toolFn_str(a.query, 'query'), limit = toolFn_num(a.limit, 'limit') || 5;
938
- 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 }),
943
- ]);
944
- return textResult({ query: query || null, components: r[0], skills: r[1], assets: r[2], design_systems: r[3] }); },
945
- aura_recommend: async (a) => { a = a || {}; if (typeof a.goal !== 'string' || !a.goal.trim()) toolFn_bad('goal (string) is required');
946
- // Gap 1: recommend hard-filters theme (not just reranks) — starter kits must not
947
- // mix light rows into a dark ask. min_views gates noise; both are explicit params.
948
- const themeV = a.theme === undefined ? undefined : theme(a.theme);
949
- const negative_themeV = a.negative_theme === undefined ? undefined : theme(a.negative_theme);
950
- const min_views = toolFn_num(a.min_views, 'min_views');
951
- 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 }),
956
- ]);
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 }));
959
- 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.',
961
- components: r[0], skills: r[1], assets: r[2], design_systems: r[3],
962
- starter_kit: { components: pick(r[0]), skills: pick(r[1]), assets: pick(r[2]), design_systems: pick(r[3]) } }); },
963
- aura_install_component: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
964
- if (id === undefined || (typeof id !== 'string' && typeof id !== 'number')) toolFn_bad('provide id (number) or slug (string)');
965
- const got = await catalog.getItem('components', id);
966
- if (got.item && got.item.premium !== false) toolFn_bad('that component is Pro (paid). Search with freeOnly or pick a free one.');
967
- return textResult({ item: got.item, install: installGuide('components', got.item) }); },
968
- aura_install_skill: async (a) => { a = a || {}; if (typeof a.id !== 'string' || !a.id) toolFn_bad('id (string) is required');
969
- const got = await catalog.getItem('skills', a.id);
970
- return textResult({ item: got.item, install: installGuide('skills', got.item) }); },
971
- aura_use_design_system: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
972
- if (typeof id !== 'string' || !id) toolFn_bad('provide id or slug (string)');
973
- const got = await catalog.getItem('design_systems', id);
974
- 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');
989
- if (!ids || !ids.length) toolFn_bad('provide ids (array of 1-8 numbers/strings) or slugs (array of strings)');
990
- return textResult({ kind: 'components', results: await catalog.bundleItems('components', ids) }); },
991
- aura_related: async (a) => { a = a || {}; const k = a.kind;
992
- if (k !== 'components' && k !== 'skills' && k !== 'design_systems') toolFn_bad('kind must be components|skills|design_systems');
993
- const id = a.id; if (typeof id !== 'string' && typeof id !== 'number') toolFn_bad('id is required');
994
- return textResult(await catalog.relatedItems(k, id, 3)); },
995
- aura_install_asset: async (a) => { a = a || {}; if (typeof a.id !== 'number') toolFn_bad('id (number) is required');
996
- const got = await catalog.getItem('assets', a.id);
997
- const it = got.item; const fx = (it.facets || {});
998
- 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
- aura_bulk_fetch: async (a) => { a = a || {}; const kind = a.kind;
1000
- if (kind !== 'components' && kind !== 'design_systems' && kind !== 'assets') toolFn_bad('kind must be components|design_systems|assets');
1001
- const ids = idList(a.ids, 'ids');
1002
- if (!ids || !ids.length) toolFn_bad('provide ids (array of 1-8)');
1003
- return textResult({ kind, results: await catalog.bundleItems(kind, ids) }); },
1004
- aura_scaffold_page: async (a) => { a = a || {}; if (typeof a.goal !== 'string' || !a.goal.trim()) toolFn_bad('goal (string) is required');
1005
- const sysRef = (typeof a.system === 'string' && a.system) ? a.system : null;
1006
- const compRefs = idList(a.components, 'components') || [];
1007
- 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),
1010
- ]);
1011
- const sysItem = sys && sys.item ? sys.item : null;
1012
- const toks = sysItem ? tokenHints(sysItem.content || '') : { tokens: {}, css: '' };
1013
- const guides = (comps || []).map((c) => installGuide('components', c));
1014
- const deps = []; const seen = new Set();
1015
- for (const g of guides) for (const d of (g.deps || [])) if (!seen.has(d.name)) { seen.add(d.name); deps.push(d); }
1016
- const files = [{ path: 'styles/tokens.css', contains: 'tokens.css from use_design_system' }];
1017
- if (sysItem) files.push({ path: 'DESIGN.md', contains: 'system rules' });
1018
- 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 }); },
1020
- };
1021
- };
1564
+ // Module: tools — thin adapters. Each tool validates args at the seam,
1565
+ // calls one catalog/guide method, wraps textResult. No retries/caching here
1566
+ // (except aura_recommend's single bounded auto-retry + editorial fill).
1567
+ const TOOL_SORTS = ['popular', 'recent', 'trending', 'updated'];
1568
+ const TOOL_CATS = ['hero','section','button','card','background','header','logo','feature','pricing','testimonial','footer','form','heading'];
1569
+ function toolFn_bad(msg) { const e = new Error(msg); e.code = 'BAD_ARGS'; throw e; }
1570
+ function toolFn_str(v, name) { if (v !== undefined && typeof v !== 'string') toolFn_bad(name + ' must be a string'); return v; }
1571
+ function toolFn_num(v, name) { if (v !== undefined && typeof v !== 'number') toolFn_bad(name + ' must be a number'); return v; }
1572
+ function toolFn_bool(v, name) { if (v !== undefined && typeof v !== 'boolean') toolFn_bad(name + ' must be a boolean'); return v; }
1573
+ function toolFn_sort(v) { if (v !== undefined && !TOOL_SORTS.includes(v)) toolFn_bad('sort must be one of ' + TOOL_SORTS.join('|')); return v; }
1574
+ function theme(v) { if (v !== undefined && v !== 'dark' && v !== 'light') toolFn_bad('theme must be dark|light'); return v; }
1575
+ function normRefs(v, name) {
1576
+ if (v === undefined) return v;
1577
+ let preview = '';
1578
+ try { preview = JSON.stringify(v).slice(0, 120); } catch { preview = String(v).slice(0, 120); }
1579
+ const ex = name + ' must be an array like ["732","1364"] or [{id:"732"}] — got ' + preview;
1580
+ if (!Array.isArray(v) || !v.length || v.length > 8) toolFn_bad(ex);
1581
+ const out = [];
1582
+ for (const e of v) {
1583
+ if (typeof e === 'string' && e) out.push(e);
1584
+ else if (typeof e === 'number' && Number.isFinite(e)) out.push(String(e));
1585
+ else if (e && typeof e === 'object' && !Array.isArray(e)) {
1586
+ if (typeof e.id === 'string' && e.id) out.push(e.id);
1587
+ else if (typeof e.id === 'number' && Number.isFinite(e.id)) out.push(String(e.id));
1588
+ else if (typeof e.slug === 'string' && e.slug) out.push(e.slug);
1589
+ else toolFn_bad(ex);
1590
+ } else toolFn_bad(ex);
1591
+ }
1592
+ if (!out.length) toolFn_bad(ex);
1593
+ return out;
1594
+ }
1595
+ function toolFn_cat(v) { if (v !== undefined && !TOOL_CATS.includes(v)) toolFn_bad('category must be one of ' + TOOL_CATS.join('|')); return v; }
1596
+ function inspoMode(v) { if (v !== undefined && !INSPO_MODES.includes(v)) toolFn_bad('mode must be light|dark'); return v; }
1597
+ function inspoVibe(v) { if (v !== undefined && !INSPO_VIBES.includes(v)) toolFn_bad('vibe must be one of ' + INSPO_VIBES.join('|')); return v; }
1598
+ function inspoMacro(v) { if (v !== undefined && !INSPO_MACROS.includes(v)) toolFn_bad('macrostructure must be one of ' + INSPO_MACROS.join('|')); return v; }
1599
+ function inspoEvidence(v, name) {
1600
+ if (!v || typeof v !== 'object' || Array.isArray(v)) toolFn_bad((name || 'evidence') + ' must be an object (slim keys only)');
1601
+ return v;
1602
+ }
1603
+ const TOOL_DEFS = [
1604
+ { 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 } },
1605
+ { 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 } },
1606
+ { 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 } },
1607
+ { name: 'aura_search_skills', description: 'Search free Aura agent skills (192). Metadata only, slim by default; use aura_get_skill for the full SKILL.md content. exact_match:true skips fallbacks.', inputSchema: { type: 'object', properties: { query: { type: 'string' }, sort: { type: 'string', enum: TOOL_SORTS }, limit: { type: 'number' }, offset: { type: 'number' }, slim: { type: 'boolean' }, exact_match: { type: 'boolean' } }, additionalProperties: false } },
1608
+ { 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 } },
1609
+ { 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 } },
1610
+ { 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 } },
1611
+ { 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 } },
1612
+ { 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 } },
1613
+ { 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 } },
1614
+ { 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 } },
1615
+ { 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 } },
1616
+ { 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 } },
1617
+ { 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 } },
1618
+ { 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 } },
1619
+ { 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 } },
1620
+ { 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 } },
1621
+ { 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 } },
1622
+ { name: 'aura_categories', description: 'The 13 component categories with live free counts. Pick one, then search within it.', inputSchema: { type: 'object', properties: {}, additionalProperties: false } },
1623
+ { name: 'aura_prompt', description: 'Need-based design prompt in 2 passes: Pass 1 (goal only) returns draft + 6 questions (theme, sections, system, assets, macrostructure, vibe — mode folds into theme); ask the user via native question UI, then Pass 2 (goal + answers) returns copy-paste final_prompt + plan order + suggested calls starting at inspo.recommend. Always confirm with user before scaffolding.', inputSchema: { type: 'object', properties: { goal: { type: 'string' }, theme: { type: 'string', enum: ['dark', 'light'] }, surfaces: { type: 'array', items: { type: 'string' } }, system: { type: 'string' }, answers: { type: 'object' } }, required: ['goal'], additionalProperties: false } },
1624
+ { name: 'aura_inspo_recommend', description: 'Map a brief onto Inspo + Aura calls (pure, no fetch). Strict macro gate: without macrostructure returns need_macro:true — run inspo.recommend first, never scaffold generic. With macro returns inspo_call + 3 aura_calls + palette priority (brand > Inspo > Aura).', inputSchema: { type: 'object', properties: { brief: { type: 'string' }, mode: { type: 'string', enum: ['dark', 'light'] }, vibe: { type: 'string' }, macrostructure: { type: 'string' }, brand_hex: { type: 'string' } }, required: ['brief'], additionalProperties: false } },
1625
+ { name: 'aura_inspo_components', description: 'Map an Inspo component filter onto Aura search (pure). evidence.type hero|pricing|features|cta|nav|footer|testimonial|logo-cloud|faq|stat plus style/macrostructure/mode. Returns inspo.find_components call + aura_search + scaffold_hint (tokens first, weight s|m).', inputSchema: { type: 'object', properties: { evidence: { type: 'object' }, brief: { type: 'string' } }, required: ['evidence'], additionalProperties: false } },
1626
+ { name: 'aura_inspo_reference', description: 'Adapt one Inspo reference JSX onto one Aura component (pure). evidence {type, id} + aura_component {id|slug}. Returns inspo.get_reference_jsx + aura_get_component calls plus scoped adapt_plan (token swaps, css scope, files).', inputSchema: { type: 'object', properties: { evidence: { type: 'object' }, aura_component: { type: 'object' } }, required: ['evidence', 'aura_component'], additionalProperties: false } },
1627
+ { name: 'aura_inspo_system', description: 'Resolve Inspo tokens vs Aura DESIGN system (pure). inspo_tokens {palette, fonts, background?, text?, brand_hex?} + aura_system slug. Returns option A (Inspo) vs B (Aura fetch) vs C (blend) plus one system_choice question payload (A-inspo|B-aura|C-blend) and contrast warning. Tokens first.', inputSchema: { type: 'object', properties: { inspo_tokens: { type: 'object' }, aura_system: {} }, required: ['inspo_tokens', 'aura_system'], additionalProperties: false } },
1628
+ { name: 'aura_inspo_compare', description: 'Triangulate a house style from 2-4 slim Inspo sites (pure). evidence[] of {slug, palette?, fonts?, macrostructure?, mode?} — slim keys only, no autopsy. Returns inspo.compare call + closest Aura system/component queries.', inputSchema: { type: 'object', properties: { evidence: { type: 'array', items: { type: 'object' } } }, required: ['evidence'], additionalProperties: false } },
1629
+ { name: 'aura_docs_plan', description: 'Plan a fresh-docs lookup (pure, no fetch). library + need -> Context7 resolve/query plan (trusted: tailwind, gsap, lenis, iconify) + apply steps for Aura files + docs stamp slot. The agent calls Context7, Aura shapes the rest.', inputSchema: { type: 'object', properties: { library: { type: 'string' }, need: { type: 'string' } }, required: ['library', 'need'], additionalProperties: false } },
1630
+ { name: 'aura_docs_check', description: 'Check pasted code against fresh docs (pure). library + code_excerpt (under 20000 chars, excerpts not full pages) + optional docs_note -> verdict current|needs-update, look-preserving fixes, version-pin needs_check. Fresh API wins, look stays identical.', inputSchema: { type: 'object', properties: { library: { type: 'string' }, code_excerpt: { type: 'string' }, docs_note: { type: 'string' } }, required: ['library', 'code_excerpt'], additionalProperties: false } },
1631
+ { name: 'aura_icon', description: 'Fetch one free icon as ready-to-paste SVG (live). name in kebab-case (bolt, arrow-down). source auto|fontawesome|pixel, style auto|solid|regular|brands. Auto-fallback across styles then libraries; every hit carries license + credit (CC BY 4.0, credit always) plus a font/CSS alternative. 24h icon cache.', inputSchema: { type: 'object', properties: { name: { type: 'string' }, source: { type: 'string', enum: ['auto', 'fontawesome', 'pixel'] }, style: { type: 'string', enum: ['solid', 'regular', 'brands', 'auto'] } }, required: ['name'], additionalProperties: false } },
1632
+ { name: 'aura_version', description: 'Running vs npm-published version (live, hourly cache). Returns update_available + notice telling the agent to prefer latest (npm i -g aura-components-mcp@latest) when behind. Never fails — offline registries return latest:null. aura_status already carries this verdict on every call.', inputSchema: { type: 'object', properties: {}, additionalProperties: false } },
1633
+ ];
1634
+ function createHandlers(catalog, icons, version) {
1635
+ // Automatic update notice: every aura_status carries the cached version
1636
+ // verdict (hourly npm re-check), so agents learn about updates unprompted.
1637
+ const versionNote = async () => {
1638
+ try {
1639
+ if (version && typeof version.getVersion === "function") return await version.getVersion();
1640
+ } catch { /* status must never fail on version check */ }
1641
+ return null;
1642
+ };
1643
+ return {
1644
+ aura_status: async () => {
1645
+ const [status, update] = await Promise.all([catalog.getStatus(), versionNote()]);
1646
+ return textResult(update ? { ...status, update } : status);
1647
+ },
1648
+ aura_search_components: async (a) => { a = a || {}; return textResult(await catalog.searchCatalog('components', {
1649
+ query: toolFn_str(a.query, 'query'), tag: toolFn_cat(a.category), freeOnly: true, theme: theme(a.theme),
1650
+ negative_theme: a.negative_theme === undefined ? undefined : theme(a.negative_theme),
1651
+ min_views: toolFn_num(a.min_views, 'min_views'),
1652
+ sort: toolFn_sort(a.sort), limit: toolFn_num(a.limit, 'limit'), offset: toolFn_num(a.offset, 'offset'),
1653
+ slim: a.slim === undefined ? undefined : toolFn_bool(a.slim, 'slim'),
1654
+ exact_match: a.exact_match === undefined ? undefined : toolFn_bool(a.exact_match, 'exact_match'),
1655
+ })); },
1656
+ aura_get_component: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
1657
+ if (id === undefined || (typeof id !== 'string' && typeof id !== 'number')) toolFn_bad('provide id (number) or slug (string)');
1658
+ const full = a.full === undefined ? undefined : toolFn_bool(a.full, 'full');
1659
+ if (full === true) return textResult(await catalog.getItem('components', id, { full: true }));
1660
+ const chunk = a.chunk === undefined ? 0 : toolFn_num(a.chunk, 'chunk');
1661
+ return textResult(await catalog.getItem('components', id, { chunk })); },
1662
+ aura_search_skills: async (a) => { a = a || {}; return textResult(await catalog.searchCatalog('skills', {
1663
+ query: toolFn_str(a.query, 'query'), sort: toolFn_sort(a.sort), limit: toolFn_num(a.limit, 'limit'), offset: toolFn_num(a.offset, 'offset'),
1664
+ slim: a.slim === undefined ? undefined : toolFn_bool(a.slim, 'slim'),
1665
+ exact_match: a.exact_match === undefined ? undefined : toolFn_bool(a.exact_match, 'exact_match'),
1666
+ })); },
1667
+ aura_get_skill: async (a) => { a = a || {}; if (typeof a.id !== 'string' || !a.id) toolFn_bad('id (string) is required');
1668
+ const chunk = a.chunk === undefined ? 0 : toolFn_num(a.chunk, 'chunk');
1669
+ return textResult(await catalog.getItem('skills', a.id, { chunk })); },
1670
+ aura_search_assets: async (a) => { a = a || {}; const co = a.commercial_ok === undefined ? undefined : toolFn_bool(a.commercial_ok, 'commercial_ok');
1671
+ 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') });
1672
+ // Gap 5: commercial_ok filter (all-rights-reserved per Aura Terms S4).
1673
+ // commercial_ok:true returns [] with explanation, never silence.
1674
+ 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.' });
1675
+ return textResult(r); },
1676
+ aura_search_design_systems: async (a) => { a = a || {}; return textResult(await catalog.searchCatalog('design_systems', {
1677
+ query: toolFn_str(a.query, 'query'), sort: toolFn_sort(a.sort), limit: toolFn_num(a.limit, 'limit'), offset: toolFn_num(a.offset, 'offset'),
1678
+ slim: a.slim === undefined ? undefined : toolFn_bool(a.slim, 'slim'),
1679
+ exact_match: a.exact_match === undefined ? undefined : toolFn_bool(a.exact_match, 'exact_match'),
1680
+ })); },
1681
+ aura_get_design_system: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
1682
+ if (typeof id !== 'string' || !id) toolFn_bad('provide id or slug (string)');
1683
+ const chunk = a.chunk === undefined ? 0 : toolFn_num(a.chunk, 'chunk');
1684
+ return textResult(await catalog.getItem('design_systems', id, { chunk })); },
1685
+ aura_search_all: async (a) => { a = a || {}; const query = toolFn_str(a.query, 'query'), limit = toolFn_num(a.limit, 'limit') || 5;
1686
+ const slim = a.slim === undefined ? undefined : toolFn_bool(a.slim, 'slim');
1687
+ const themeV = a.theme === undefined ? undefined : theme(a.theme);
1688
+ const negV = a.negative_theme === undefined ? undefined : theme(a.negative_theme);
1689
+ const minV = toolFn_num(a.min_views, 'min_views');
1690
+ const exactV = a.exact_match === undefined ? undefined : toolFn_bool(a.exact_match, 'exact_match');
1691
+ const r = await Promise.all([
1692
+ catalog.searchCatalog('components', { query, freeOnly: true, limit, slim, theme: themeV, negative_theme: negV, min_views: minV, exact_match: exactV }),
1693
+ catalog.searchCatalog('skills', { query, limit, slim, min_views: minV, exact_match: exactV }),
1694
+ catalog.searchCatalog('assets', { query, freeOnly: true, limit, slim, min_views: minV, exact_match: exactV }),
1695
+ catalog.searchCatalog('design_systems', { query, limit, slim, min_views: minV, exact_match: exactV }),
1696
+ ]);
1697
+ return textResult({ query: query || null, components: r[0], skills: r[1], assets: r[2], design_systems: r[3] }); },
1698
+ aura_recommend: async (a) => { a = a || {}; if (typeof a.goal !== 'string' || !a.goal.trim()) toolFn_bad('goal (string) is required');
1699
+ // Gap 1: recommend hard-filters theme (not just reranks) — starter kits must not
1700
+ // mix light rows into a dark ask. min_views gates noise; both are explicit params.
1701
+ const themeV = a.theme === undefined ? undefined : theme(a.theme);
1702
+ const negative_themeV = a.negative_theme === undefined ? undefined : theme(a.negative_theme);
1703
+ const min_views = toolFn_num(a.min_views, 'min_views');
1704
+ const slim = a.slim === undefined ? undefined : toolFn_bool(a.slim, 'slim');
1705
+ const r = await Promise.all([
1706
+ catalog.searchCatalog('components', { query: a.goal, freeOnly: true, limit: 3, theme: themeV, negative_theme: negative_themeV, min_views, slim }),
1707
+ catalog.searchCatalog('skills', { query: a.goal, limit: 3, slim }),
1708
+ catalog.searchCatalog('assets', { query: a.goal, freeOnly: true, limit: 3, slim }),
1709
+ catalog.searchCatalog('design_systems', { query: a.goal, limit: 3, slim }),
1710
+ ]);
1711
+ // P0 auto-retry (bounded: max 1 retry + 1 picks call, no loops): an honest-empty
1712
+ // components surface (no_match) retries once via suggested_queries[0] with the
1713
+ // same filters, then falls back to human-grade editorial picks so the kit fills.
1714
+ if (r[0] && r[0].no_match === true && (!Array.isArray(r[0].items) || !r[0].items.length)) {
1715
+ const sq = Array.isArray(r[0].suggested_queries) ? r[0].suggested_queries.filter((q) => typeof q === 'string' && q.trim()) : [];
1716
+ let filled = false;
1717
+ if (sq.length) {
1718
+ try {
1719
+ const retry = await catalog.searchCatalog('components', { query: sq[0], freeOnly: true, limit: 3, theme: themeV, negative_theme: negative_themeV, min_views, slim });
1720
+ if (retry && Array.isArray(retry.items) && retry.items.length) {
1721
+ retry.retried = true;
1722
+ retry.retried_from = sq[0];
1723
+ r[0] = retry;
1724
+ filled = true;
1725
+ }
1726
+ } catch { /* keep the original honest-empty */ }
1727
+ }
1728
+ if (!filled && (!Array.isArray(r[0].items) || !r[0].items.length)) {
1729
+ try {
1730
+ const picks = typeof catalog.editorialPicks === 'function' ? await catalog.editorialPicks(3) : [];
1731
+ if (Array.isArray(picks) && picks.length) {
1732
+ r[0] = { kind: 'components', total: picks.length, items: picks, editorial_fallback: true, editorial_note: 'Goal matched nothing — showing human-grade free picks instead.' };
1733
+ }
1734
+ } catch { /* keep the original honest-empty */ }
1735
+ }
1736
+ }
1737
+ r[0].items = (r[0].items || []).slice(0, 3);
1738
+ const pick = (x) => ((x && x.items) || []).slice(0, 3).map((i) => ({ title: i.title, page_url: i.page_url }));
1739
+ return textResult({ goal: a.goal, freeOnly: true,
1740
+ 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.',
1741
+ commercial_note: 'Assets (and component art) are all-rights-reserved per Aura Terms §4: fine to prototype, get Aura permission before commercial ship.',
1742
+ components: r[0], skills: r[1], assets: r[2], design_systems: r[3],
1743
+ starter_kit: { components: pick(r[0]), skills: pick(r[1]), assets: pick(r[2]), design_systems: pick(r[3]) } }); },
1744
+ aura_install_component: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
1745
+ if (id === undefined || (typeof id !== 'string' && typeof id !== 'number')) toolFn_bad('provide id (number) or slug (string)');
1746
+ const got = await catalog.getItem('components', id);
1747
+ if (got.item && got.item.premium !== false) toolFn_bad('that component is Pro (paid). Search with freeOnly or pick a free one.');
1748
+ const compInstall = installGuide('components', got.item);
1749
+ return textResult({ item: got.item, install: compInstall, docs_stamp: docsStampFor(compInstall.deps) }); },
1750
+ aura_install_skill: async (a) => { a = a || {}; if (typeof a.id !== 'string' || !a.id) toolFn_bad('id (string) is required');
1751
+ const got = await catalog.getItem('skills', a.id);
1752
+ return textResult({ item: got.item, install: installGuide('skills', got.item), docs_stamp: { status: 'unchecked', needs_check: ['SKILL.md is a vendored snapshot — re-check source_url for updates'], note: 'No library pins in skill installs.' } }); },
1753
+ aura_use_design_system: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
1754
+ if (typeof id !== 'string' || !id) toolFn_bad('provide id or slug (string)');
1755
+ const got = await catalog.getItem('design_systems', id);
1756
+ const dsInstall = installGuide('design_systems', got.item);
1757
+ return textResult({ item: got.item, install: dsInstall, tokens: tokenHints(got.item.content || '', got.item.preview_html || ''), docs_stamp: docsStampFor(dsInstall.deps) }); },
1758
+ aura_categories: async () => { const cats = await catalog.categoryCounts();
1759
+ const s = (cats || []).reduce((n, c) => n + (typeof c.free === 'number' ? c.free : 0), 0);
1760
+ 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.' }); },
1761
+ 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 })); },
1762
+ aura_inspo_recommend: async (a) => { a = a || {}; return textResult(buildInspoRecommend({
1763
+ brief: typeof a.brief === 'string' ? a.brief : undefined, mode: inspoMode(a.mode), vibe: inspoVibe(a.vibe),
1764
+ macrostructure: inspoMacro(a.macrostructure), brand_hex: toolFn_str(a.brand_hex, 'brand_hex'),
1765
+ })); },
1766
+ aura_inspo_components: async (a) => { a = a || {}; return textResult(buildInspoComponents({ evidence: inspoEvidence(a.evidence, 'evidence'), brief: toolFn_str(a.brief, 'brief') })); },
1767
+ aura_inspo_reference: async (a) => { a = a || {}; return textResult(buildInspoReference({ evidence: inspoEvidence(a.evidence, 'evidence'), aura_component: inspoEvidence(a.aura_component, 'aura_component') })); },
1768
+ aura_inspo_system: async (a) => { a = a || {}; const sys = (typeof a.aura_system === 'string' || (a.aura_system && typeof a.aura_system === 'object')) ? a.aura_system : toolFn_bad('aura_system slug (string) is required');
1769
+ return textResult(buildInspoSystem({ inspo_tokens: inspoEvidence(a.inspo_tokens, 'inspo_tokens'), aura_system: sys })); },
1770
+ aura_inspo_compare: async (a) => { a = a || {}; if (!Array.isArray(a.evidence)) toolFn_bad('evidence must be an array of 2-4 slim site objects');
1771
+ return textResult(buildInspoCompare({ evidence: a.evidence })); },
1772
+ aura_docs_plan: async (a) => { a = a || {}; return textResult(buildDocsPlan({ library: typeof a.library === 'string' ? a.library : undefined, need: typeof a.need === 'string' ? a.need : undefined })); },
1773
+ aura_docs_check: async (a) => { a = a || {}; return textResult(buildDocsCheck({ library: typeof a.library === 'string' ? a.library : undefined, code_excerpt: typeof a.code_excerpt === 'string' ? a.code_excerpt : undefined, docs_note: toolFn_str(a.docs_note, 'docs_note') })); },
1774
+ aura_icon: async (a) => { a = a || {}; if (!icons || typeof icons.getIcon !== 'function') { const e = new Error('icons backend not wired'); throw e; }
1775
+ const name = typeof a.name === 'string' ? a.name : toolFn_bad('name (string) is required — e.g. {name:"bolt"}');
1776
+ const source = a.source === undefined ? undefined : (a.source === 'auto' || a.source === 'fontawesome' || a.source === 'pixel' ? a.source : toolFn_bad('source must be auto|fontawesome|pixel'));
1777
+ const style = a.style === undefined ? undefined : (a.style === 'auto' || a.style === 'solid' || a.style === 'regular' || a.style === 'brands' ? a.style : toolFn_bad('style must be auto|solid|regular|brands'));
1778
+ return textResult(await icons.getIcon({ name, source, style })); },
1779
+ aura_version: async () => { if (!version || typeof version.getVersion !== 'function') { const e = new Error('version backend not wired'); throw e; }
1780
+ return textResult(await version.getVersion()); },
1781
+ aura_bundle: async (a) => { a = a || {}; const ids = normRefs(a.ids, 'ids') || normRefs(a.slugs, 'slugs');
1782
+ if (!ids || !ids.length) toolFn_bad('provide ids (array of 1-8 numbers/strings) or slugs (array of strings)');
1783
+ return textResult({ kind: 'components', results: await catalog.bundleItems('components', ids) }); },
1784
+ aura_related: async (a) => { a = a || {}; const k = a.kind;
1785
+ if (k !== 'components' && k !== 'skills' && k !== 'design_systems') toolFn_bad('kind must be components|skills|design_systems');
1786
+ const id = a.id; if (typeof id !== 'string' && typeof id !== 'number') toolFn_bad('id is required');
1787
+ return textResult(await catalog.relatedItems(k, id, 3)); },
1788
+ aura_install_asset: async (a) => { a = a || {}; if (typeof a.id !== 'number') toolFn_bad('id (number) is required');
1789
+ const got = await catalog.getItem('assets', a.id);
1790
+ const it = got.item; const fx = (it.facets || {});
1791
+ return textResult({ item: it, install: { kind: 'assets', title: it.title, page_url: it.page_url, license: fx.license || 'all-rights-reserved (Aura Terms §4)', terms_url: 'https://www.aura.build/terms', download: fx.download || null, preview: fx.preview || null, files: [{ path: 'assets/' + it.id + '-' + String(it.title || 'asset').toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 40) + '.jpg', contains: 'downloaded original' }], steps: ['Check the license on the Aura asset page before commercial use — this server reports unknown, never assumes free-to-sell.', 'Download the download URL into the suggested path.', 'Use the preview URL for <img> srcset while drafting.'] }, docs_stamp: { status: 'unchecked', needs_check: ['license per page_url — terms can change'], note: 'No library pins in asset installs.' } }); },
1792
+ aura_bulk_fetch: async (a) => { a = a || {}; const kind = a.kind;
1793
+ if (kind !== 'components' && kind !== 'design_systems' && kind !== 'assets') toolFn_bad('kind must be components|design_systems|assets');
1794
+ const ids = normRefs(a.ids, 'ids');
1795
+ if (!ids || !ids.length) toolFn_bad('provide ids (array of 1-8)');
1796
+ return textResult({ kind, results: await catalog.bundleItems(kind, ids) }); },
1797
+ aura_scaffold_page: async (a) => { a = a || {}; if (typeof a.goal !== 'string' || !a.goal.trim()) toolFn_bad('goal (string) is required');
1798
+ const sysRef = (typeof a.system === 'string' && a.system) ? a.system : null;
1799
+ const compRefs = normRefs(a.components, 'components') || [];
1800
+ const full = a.full === undefined ? undefined : toolFn_bool(a.full, 'full');
1801
+ const fullOpt = full === undefined ? {} : { full };
1802
+ const [sys, comps] = await Promise.all([
1803
+ 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)),
1804
+ 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),
1805
+ ]);
1806
+ // Pro guard: explicit-id bundles can name Pro rows — free flows never ship them.
1807
+ const freeComps = (comps || []).filter((c) => c && c.premium === false);
1808
+ const proFiltered = (comps || []).length - freeComps.length;
1809
+ const sysItem = sys && sys.item ? sys.item : null;
1810
+ const toks = sysItem ? tokenHints(sysItem.content || '') : { tokens: {}, css: '' };
1811
+ const { guides, combined_deps: deps, files } = combineScaffold(sysItem, freeComps);
1812
+ return textResult({ goal: a.goal, order: ['1 tokens.css', '2 system preview_html', '3 component markup in listed order'], tokens_css: toks.css || '', system: sysItem, components: freeComps || [], guides, combined_deps: deps, files, docs_stamp: docsStampFor(deps), ...(proFiltered ? { pro_filtered_out: proFiltered, pro_note: 'Pro (paid) components were refused — free-only scaffold ships free rows.' } : {}), license_note: 'Markup and any pulled assets are all-rights-reserved per Aura Terms §4 — fine to prototype, check each page_url before commercial ship.' }); },
1813
+ };
1814
+ };
1022
1815
 
1023
1816
  // ---- server.mjs (main) ----
1024
1817
  // Module: server — composition root. Wires config -> fetcher -> catalog ->
@@ -1030,10 +1823,14 @@ function createHandlers(catalog) {
1030
1823
 
1031
1824
 
1032
1825
 
1826
+
1827
+
1033
1828
  const config = loadConfig();
1034
1829
  const fetcher = createFetcher({ fetchImpl: globalThis.fetch, timeoutMs: config.timeoutMs, retries: config.retries, userAgent: config.userAgent });
1035
1830
  const catalog = createCatalog({ fetcher, config, facetsFn: facets });
1036
- const handlers = createHandlers(catalog);
1831
+ const icons = createIcons({ fetcher });
1832
+ const version = createVersion({ fetcher, running: SERVER_INFO.version });
1833
+ const handlers = createHandlers(catalog, icons, version);
1037
1834
 
1038
1835
  let buffer = "";
1039
1836
  const send = (msg) => process.stdout.write(JSON.stringify(msg) + "\n");
@@ -1085,6 +1882,6 @@ async function boot() {
1085
1882
  await sleepMs(180);
1086
1883
  paint("built by swastiksingh-dev — free to use, MIT, no paywall");
1087
1884
  await sleepMs(180);
1088
- paint(SERVER_INFO.name + " v" + SERVER_INFO.version + " running — 15 tools on stdio");
1885
+ paint(SERVER_INFO.name + " v" + SERVER_INFO.version + " running — " + TOOL_DEFS.length + " tools on stdio");
1089
1886
  }
1090
1887
  boot();