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/LICENSE +21 -21
- package/README.md +216 -53
- package/SETUP.md +17 -8
- package/dist/server.js +989 -192
- package/package.json +52 -53
- package/flow/app.js +0 -178
- package/flow/compare.html +0 -62
- package/flow/docs.html +0 -73
- package/flow/how.html +0 -63
- package/flow/index.html +0 -89
- package/flow/setup.html +0 -77
- package/flow/styles.css +0 -378
- package/flow/tools.html +0 -79
package/dist/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// aura-components-mcp v1.
|
|
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.
|
|
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
|
-
|
|
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
|
|
229
|
-
//
|
|
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,
|
|
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
|
|
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 { /*
|
|
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
|
-
//
|
|
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
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
868
|
-
// calls one catalog/guide method, wraps textResult. No retries/caching here
|
|
869
|
-
|
|
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
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
{
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
function
|
|
903
|
-
|
|
904
|
-
return
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
const
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
//
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
const
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
const
|
|
1006
|
-
const
|
|
1007
|
-
const
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
if (
|
|
1018
|
-
|
|
1019
|
-
|
|
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
|
|
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 —
|
|
1885
|
+
paint(SERVER_INFO.name + " v" + SERVER_INFO.version + " running — " + TOOL_DEFS.length + " tools on stdio");
|
|
1089
1886
|
}
|
|
1090
1887
|
boot();
|