aura-components-mcp 1.6.2 → 1.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -27
- package/SETUP.md +17 -8
- package/dist/server.js +1262 -978
- package/flow/compare.html +67 -62
- package/flow/docs.html +78 -73
- package/flow/how.html +68 -63
- package/flow/index.html +94 -89
- package/flow/setup.html +82 -77
- package/flow/tools.html +84 -79
- package/package.json +53 -53
package/dist/server.js
CHANGED
|
@@ -1,871 +1,1103 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// aura-components-mcp v1.6.
|
|
3
|
-
|
|
4
|
-
// ---- config.mjs ----
|
|
5
|
-
// Module: config — one small interface (loadConfig) over all env parsing.
|
|
6
|
-
// Callers learn one function; env names, defaults, and clamping live here.
|
|
7
|
-
|
|
8
|
-
const num = (raw, fallback, { min = 1, max = 100 } = {}) => {
|
|
9
|
-
const n = Number.parseInt(String(raw ?? ""), 10);
|
|
10
|
-
if (!Number.isFinite(n)) return fallback;
|
|
11
|
-
return Math.min(max, Math.max(min, n));
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const bool = (raw, fallback) => {
|
|
15
|
-
if (raw === undefined) return fallback;
|
|
16
|
-
return /^(1|true|yes|on)$/i.test(String(raw));
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
function loadConfig(env = process.env) {
|
|
20
|
-
const defaultKey =
|
|
21
|
-
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImhvaXJxcmtkZ2JtdnB3dXR3dXdqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDM2Nzc2NTAsImV4cCI6MjA1OTI1MzY1MH0._UsCSHsTELn7m54tOhX3ySm67WEhcyHAPbuxEQZsl3c";
|
|
22
|
-
return {
|
|
23
|
-
supabaseUrl: env.AURA_SUPABASE_URL ?? "https://hoirqrkdgbmvpwutwuwj.supabase.co",
|
|
24
|
-
anonKey: env.AURA_SUPABASE_ANON_KEY ?? defaultKey,
|
|
25
|
-
timeoutMs: num(env.AURA_TIMEOUT_MS, 12_000, { min: 1_000, max: 60_000 }),
|
|
26
|
-
retries: num(env.AURA_RETRIES, 2, { min: 0, max: 5 }),
|
|
27
|
-
cacheTtlMs: num(env.AURA_CACHE_TTL_MS, 60_000, { min: 0, max: 3_600_000 }),
|
|
28
|
-
defaultLimit: num(env.AURA_DEFAULT_LIMIT, 10, { min: 1, max: 50 }),
|
|
29
|
-
maxLimit: num(env.AURA_MAX_LIMIT, 25, { min: 1, max: 50 }),
|
|
30
|
-
codeChars: num(env.AURA_CODE_CHARS, 12_000, { min: 1_000, max: 60_000 }),
|
|
31
|
-
contentChars: num(env.AURA_CONTENT_CHARS, 12_000, { min: 1_000, max: 60_000 }),
|
|
32
|
-
freeOnlyDefault: bool(env.AURA_FREE_ONLY_DEFAULT, true),
|
|
33
|
-
userAgent: env.AURA_USER_AGENT ?? "aura-components-mcp/1.
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// ---- http.mjs ----
|
|
38
|
-
// Module: http — one small interface (createFetcher) hiding timeout,
|
|
39
|
-
// retry-with-backoff, error mapping, and the injectable fetch seam.
|
|
40
|
-
// Tests inject a stub fetch; production passes globalThis.fetch.
|
|
41
|
-
|
|
42
|
-
function createFetcher({ fetchImpl, timeoutMs, retries, userAgent }) {
|
|
43
|
-
if (typeof fetchImpl !== "function") throw new Error("fetchImpl must be a function");
|
|
44
|
-
|
|
45
|
-
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
46
|
-
|
|
47
|
-
async function once(url, init) {
|
|
48
|
-
const ctrl = new AbortController();
|
|
49
|
-
const t = setTimeout(() => ctrl.abort(new Error("timeout after " + timeoutMs + "ms")), timeoutMs);
|
|
50
|
-
try {
|
|
51
|
-
return await fetchImpl(url, {
|
|
52
|
-
...init,
|
|
53
|
-
signal: ctrl.signal,
|
|
54
|
-
headers: { "user-agent": userAgent, ...(init?.headers ?? {}) },
|
|
55
|
-
});
|
|
56
|
-
} finally {
|
|
57
|
-
clearTimeout(t);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// GET JSON over PostgREST. Returns { rows, total } (total from content-range).
|
|
62
|
-
async function getJson(url, headers) {
|
|
63
|
-
let lastErr;
|
|
64
|
-
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
65
|
-
try {
|
|
66
|
-
const res = await once(url, { headers });
|
|
67
|
-
if (res.status === 429 || (res.status >= 500 && res.status < 600)) {
|
|
68
|
-
lastErr = new Error("transient upstream status " + res.status);
|
|
69
|
-
} else if (!res.ok) {
|
|
70
|
-
const body = await res.text().catch(() => "");
|
|
71
|
-
throw new Error("upstream " + res.status + ": " + body.slice(0, 300));
|
|
72
|
-
} else {
|
|
73
|
-
const range = res.headers?.get?.("content-range") ?? null;
|
|
74
|
-
const total = range && range.includes("/") ? Number(range.split("/").pop()) : null;
|
|
75
|
-
const rows = await res.json();
|
|
76
|
-
return { rows: Array.isArray(rows) ? rows : [rows], total: Number.isFinite(total) ? total : null };
|
|
77
|
-
}
|
|
78
|
-
} catch (err) {
|
|
79
|
-
lastErr = err;
|
|
80
|
-
if (err && err.name === "AbortError") break; // timeout: retrying rarely helps
|
|
81
|
-
}
|
|
82
|
-
if (attempt < retries) await sleep(120 * 2 ** attempt + Math.floor(Math.random() * 60));
|
|
83
|
-
}
|
|
84
|
-
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return { getJson };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// ---- catalog.mjs ----
|
|
91
|
-
// Module: catalog — the deep module. Small interface (searchCatalog,
|
|
92
|
-
// getItem, getStatus) over query building, caching, truncation, author
|
|
93
|
-
// enrichment, and page-URL derivation. Adapters: PostgREST via fetcher.
|
|
94
|
-
|
|
95
|
-
const SITE = "https://www.aura.build";
|
|
96
|
-
|
|
97
|
-
const CATALOG_SORTS = {
|
|
98
|
-
components: { popular: "views.desc", recent: "created_at.desc", trending: "views.desc", updated: "updated_at.desc" },
|
|
99
|
-
skills: { popular: "views.desc", recent: "created_at.desc", trending: "views.desc", updated: "updated_at.desc" },
|
|
100
|
-
assets: { popular: "views.desc", recent: "created_at.desc", trending: "views.desc", updated: "updated_at.desc" },
|
|
101
|
-
design_systems: { popular: "views.desc", recent: "created_at.desc", trending: "views.desc", updated: "updated_at.desc" },
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
const LIST_COLS = {
|
|
105
|
-
// PostgREST can't substring server-side: lists fetch code but shapeRow truncates to
|
|
106
|
-
// a 600-char probe for facets BEFORE the truncation marker (see shapeRow). ~0.7KB/row.
|
|
107
|
-
components: "id,title,description,tags,code,premium,views,forks,slug,background,created_by,created_at,updated_at",
|
|
108
|
-
skills: "id,title,description,source_url,views,forks,featured,created_by,created_at,updated_at",
|
|
109
|
-
assets: "id,title,description,keywords,media_type,premium,views,forks,image_800w,video_url,video_poster_url,created_by,created_at",
|
|
110
|
-
design_systems: "id,slug,title,description,views,forks,featured,created_by,created_at,updated_at",
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
const DETAIL_COLS = {
|
|
114
|
-
components: "id,title,description,tags,code,premium,views,forks,slug,image_url,background,credit_name,credit_url,created_by,created_at,updated_at",
|
|
115
|
-
skills: "id,title,description,content,source_url,views,forks,featured,created_by,created_at,updated_at",
|
|
116
|
-
assets: "id,title,description,keywords,resolution,colors,media_type,premium,views,forks,image_320w,image_800w,image_1600w,image_3840w,image_original,image_url,video_url,video_poster_url,video_duration,slug,created_by,created_at,updated_at",
|
|
117
|
-
design_systems: "id,slug,title,description,content,preview_html,thumbnail_url,source_name,views,forks,featured,created_by,created_at,updated_at",
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const esc = (s) => String(s).replaceAll('"', '""');
|
|
121
|
-
const ilike = (v) => "*" + String(v).replaceAll("*", "").replaceAll(",", " ").trim() + "*";
|
|
122
|
-
|
|
123
|
-
// Split a goal sentence into significant tokens for OR fallback (stop-word filtered).
|
|
124
|
-
// Theme words (dark/light) are NOT search tokens — they become rank/filter signals
|
|
125
|
-
// (see detectThemeHint + scoreByTheme), so "dark cinematic" can't match light rows.
|
|
126
|
-
const STOP = new Set("a,an,the,for,with,and,or,of,to,in,on,my,new,free,also,that,this,from,into,plus,vs,top,best,up".split(","));
|
|
127
|
-
const THEME_WORDS = new Set(["dark", "light", "midnight", "noir", "black", "white", "bright", "airy", "minimal"]);
|
|
128
|
-
function queryTokens(q, max = 4) {
|
|
129
|
-
return String(q ?? "").toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).map((t) => t.trim()).filter((t) => t.length > 2 && !STOP.has(t) && !THEME_WORDS.has(t)).slice(0, max);
|
|
130
|
-
}
|
|
131
|
-
// Theme hint from the raw query: explicit signal, separate from match tokens.
|
|
132
|
-
function detectThemeHint(q) {
|
|
133
|
-
const t = String(q ?? "").toLowerCase();
|
|
134
|
-
const dark = /\bdark\b|\bmidnight\b|\bnoir\b|\bcinematic\b/.test(t);
|
|
135
|
-
const light = /\blight\b|\bbright\b|\bairy\b/.test(t);
|
|
136
|
-
if (dark && !light) return "dark";
|
|
137
|
-
if (light && !dark) return "light";
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
// Rank rows: +2 per matched token in title, +1 in description/tags, theme match first.
|
|
141
|
-
// Returns { rows, scored } where scored explains the top pick for fallback_score.
|
|
142
|
-
function scoreRows(kind, rows, toks, themeHint) {
|
|
143
|
-
const scored = rows.map((r) => {
|
|
144
|
-
const title = String(r.title || "").toLowerCase();
|
|
145
|
-
const desc = String(r.description || "").toLowerCase();
|
|
146
|
-
const tags = ((r.tags || []).map((x) => String(x).toLowerCase()).join(" "));
|
|
147
|
-
let s = 0;
|
|
148
|
-
for (const t of toks) { if (title.includes(t)) s += 2; else if (desc.includes(t) || tags.includes(t)) s += 1; }
|
|
149
|
-
const th = r.facets && r.facets.theme;
|
|
150
|
-
const themeOk = !themeHint || th === themeHint || th === "mixed" || th === "unknown";
|
|
151
|
-
if (themeHint && th === themeHint) s += 3;
|
|
152
|
-
return { r, s, themeOk };
|
|
153
|
-
});
|
|
154
|
-
scored.sort((a, b) => ((b.themeOk ? 1 : 0) - (a.themeOk ? 1 : 0)) || (b.s - a.s) || ((b.r.views || 0) - (a.r.views || 0)));
|
|
155
|
-
return scored;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (typeof
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if (kind === "
|
|
261
|
-
|
|
262
|
-
//
|
|
263
|
-
if (kind === "assets")
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
} catch { /*
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
const
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
if (kind === "
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
if (
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
})
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
};
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
const
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
const
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
//
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
//
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
const
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// aura-components-mcp v1.6.3 — bundled (zero deps). Built by swastiksingh-dev.
|
|
3
|
+
|
|
4
|
+
// ---- config.mjs ----
|
|
5
|
+
// Module: config — one small interface (loadConfig) over all env parsing.
|
|
6
|
+
// Callers learn one function; env names, defaults, and clamping live here.
|
|
7
|
+
|
|
8
|
+
const num = (raw, fallback, { min = 1, max = 100 } = {}) => {
|
|
9
|
+
const n = Number.parseInt(String(raw ?? ""), 10);
|
|
10
|
+
if (!Number.isFinite(n)) return fallback;
|
|
11
|
+
return Math.min(max, Math.max(min, n));
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const bool = (raw, fallback) => {
|
|
15
|
+
if (raw === undefined) return fallback;
|
|
16
|
+
return /^(1|true|yes|on)$/i.test(String(raw));
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function loadConfig(env = process.env) {
|
|
20
|
+
const defaultKey =
|
|
21
|
+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImhvaXJxcmtkZ2JtdnB3dXR3dXdqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDM2Nzc2NTAsImV4cCI6MjA1OTI1MzY1MH0._UsCSHsTELn7m54tOhX3ySm67WEhcyHAPbuxEQZsl3c";
|
|
22
|
+
return {
|
|
23
|
+
supabaseUrl: env.AURA_SUPABASE_URL ?? "https://hoirqrkdgbmvpwutwuwj.supabase.co",
|
|
24
|
+
anonKey: env.AURA_SUPABASE_ANON_KEY ?? defaultKey,
|
|
25
|
+
timeoutMs: num(env.AURA_TIMEOUT_MS, 12_000, { min: 1_000, max: 60_000 }),
|
|
26
|
+
retries: num(env.AURA_RETRIES, 2, { min: 0, max: 5 }),
|
|
27
|
+
cacheTtlMs: num(env.AURA_CACHE_TTL_MS, 60_000, { min: 0, max: 3_600_000 }),
|
|
28
|
+
defaultLimit: num(env.AURA_DEFAULT_LIMIT, 10, { min: 1, max: 50 }),
|
|
29
|
+
maxLimit: num(env.AURA_MAX_LIMIT, 25, { min: 1, max: 50 }),
|
|
30
|
+
codeChars: num(env.AURA_CODE_CHARS, 12_000, { min: 1_000, max: 60_000 }),
|
|
31
|
+
contentChars: num(env.AURA_CONTENT_CHARS, 12_000, { min: 1_000, max: 60_000 }),
|
|
32
|
+
freeOnlyDefault: bool(env.AURA_FREE_ONLY_DEFAULT, true),
|
|
33
|
+
userAgent: env.AURA_USER_AGENT ?? "aura-components-mcp/1.6.3",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---- http.mjs ----
|
|
38
|
+
// Module: http — one small interface (createFetcher) hiding timeout,
|
|
39
|
+
// retry-with-backoff, error mapping, and the injectable fetch seam.
|
|
40
|
+
// Tests inject a stub fetch; production passes globalThis.fetch.
|
|
41
|
+
|
|
42
|
+
function createFetcher({ fetchImpl, timeoutMs, retries, userAgent }) {
|
|
43
|
+
if (typeof fetchImpl !== "function") throw new Error("fetchImpl must be a function");
|
|
44
|
+
|
|
45
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
46
|
+
|
|
47
|
+
async function once(url, init) {
|
|
48
|
+
const ctrl = new AbortController();
|
|
49
|
+
const t = setTimeout(() => ctrl.abort(new Error("timeout after " + timeoutMs + "ms")), timeoutMs);
|
|
50
|
+
try {
|
|
51
|
+
return await fetchImpl(url, {
|
|
52
|
+
...init,
|
|
53
|
+
signal: ctrl.signal,
|
|
54
|
+
headers: { "user-agent": userAgent, ...(init?.headers ?? {}) },
|
|
55
|
+
});
|
|
56
|
+
} finally {
|
|
57
|
+
clearTimeout(t);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// GET JSON over PostgREST. Returns { rows, total } (total from content-range).
|
|
62
|
+
async function getJson(url, headers) {
|
|
63
|
+
let lastErr;
|
|
64
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
65
|
+
try {
|
|
66
|
+
const res = await once(url, { headers });
|
|
67
|
+
if (res.status === 429 || (res.status >= 500 && res.status < 600)) {
|
|
68
|
+
lastErr = new Error("transient upstream status " + res.status);
|
|
69
|
+
} else if (!res.ok) {
|
|
70
|
+
const body = await res.text().catch(() => "");
|
|
71
|
+
throw new Error("upstream " + res.status + ": " + body.slice(0, 300));
|
|
72
|
+
} else {
|
|
73
|
+
const range = res.headers?.get?.("content-range") ?? null;
|
|
74
|
+
const total = range && range.includes("/") ? Number(range.split("/").pop()) : null;
|
|
75
|
+
const rows = await res.json();
|
|
76
|
+
return { rows: Array.isArray(rows) ? rows : [rows], total: Number.isFinite(total) ? total : null };
|
|
77
|
+
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
lastErr = err;
|
|
80
|
+
if (err && err.name === "AbortError") break; // timeout: retrying rarely helps
|
|
81
|
+
}
|
|
82
|
+
if (attempt < retries) await sleep(120 * 2 ** attempt + Math.floor(Math.random() * 60));
|
|
83
|
+
}
|
|
84
|
+
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { getJson };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---- catalog.mjs ----
|
|
91
|
+
// Module: catalog — the deep module. Small interface (searchCatalog,
|
|
92
|
+
// getItem, getStatus) over query building, caching, truncation, author
|
|
93
|
+
// enrichment, and page-URL derivation. Adapters: PostgREST via fetcher.
|
|
94
|
+
|
|
95
|
+
const SITE = "https://www.aura.build";
|
|
96
|
+
|
|
97
|
+
const CATALOG_SORTS = {
|
|
98
|
+
components: { popular: "views.desc", recent: "created_at.desc", trending: "views.desc", updated: "updated_at.desc" },
|
|
99
|
+
skills: { popular: "views.desc", recent: "created_at.desc", trending: "views.desc", updated: "updated_at.desc" },
|
|
100
|
+
assets: { popular: "views.desc", recent: "created_at.desc", trending: "views.desc", updated: "updated_at.desc" },
|
|
101
|
+
design_systems: { popular: "views.desc", recent: "created_at.desc", trending: "views.desc", updated: "updated_at.desc" },
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const LIST_COLS = {
|
|
105
|
+
// PostgREST can't substring server-side: lists fetch code but shapeRow truncates to
|
|
106
|
+
// a 600-char probe for facets BEFORE the truncation marker (see shapeRow). ~0.7KB/row.
|
|
107
|
+
components: "id,title,description,tags,code,premium,views,forks,slug,background,created_by,created_at,updated_at",
|
|
108
|
+
skills: "id,title,description,source_url,views,forks,featured,created_by,created_at,updated_at",
|
|
109
|
+
assets: "id,title,description,keywords,media_type,premium,views,forks,image_800w,video_url,video_poster_url,created_by,created_at",
|
|
110
|
+
design_systems: "id,slug,title,description,views,forks,featured,created_by,created_at,updated_at",
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const DETAIL_COLS = {
|
|
114
|
+
components: "id,title,description,tags,code,premium,views,forks,slug,image_url,background,credit_name,credit_url,created_by,created_at,updated_at",
|
|
115
|
+
skills: "id,title,description,content,source_url,views,forks,featured,created_by,created_at,updated_at",
|
|
116
|
+
assets: "id,title,description,keywords,resolution,colors,media_type,premium,views,forks,image_320w,image_800w,image_1600w,image_3840w,image_original,image_url,video_url,video_poster_url,video_duration,slug,created_by,created_at,updated_at",
|
|
117
|
+
design_systems: "id,slug,title,description,content,preview_html,thumbnail_url,source_name,views,forks,featured,created_by,created_at,updated_at",
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const esc = (s) => String(s).replaceAll('"', '""');
|
|
121
|
+
const ilike = (v) => "*" + String(v).replaceAll("*", "").replaceAll(",", " ").trim() + "*";
|
|
122
|
+
|
|
123
|
+
// Split a goal sentence into significant tokens for OR fallback (stop-word filtered).
|
|
124
|
+
// Theme words (dark/light) are NOT search tokens — they become rank/filter signals
|
|
125
|
+
// (see detectThemeHint + scoreByTheme), so "dark cinematic" can't match light rows.
|
|
126
|
+
const STOP = new Set("a,an,the,for,with,and,or,of,to,in,on,my,new,free,also,that,this,from,into,plus,vs,top,best,up".split(","));
|
|
127
|
+
const THEME_WORDS = new Set(["dark", "light", "midnight", "noir", "black", "white", "bright", "airy", "minimal"]);
|
|
128
|
+
function queryTokens(q, max = 4) {
|
|
129
|
+
return String(q ?? "").toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).map((t) => t.trim()).filter((t) => t.length > 2 && !STOP.has(t) && !THEME_WORDS.has(t)).slice(0, max);
|
|
130
|
+
}
|
|
131
|
+
// Theme hint from the raw query: explicit signal, separate from match tokens.
|
|
132
|
+
function detectThemeHint(q) {
|
|
133
|
+
const t = String(q ?? "").toLowerCase();
|
|
134
|
+
const dark = /\bdark\b|\bmidnight\b|\bnoir\b|\bcinematic\b/.test(t);
|
|
135
|
+
const light = /\blight\b|\bbright\b|\bairy\b/.test(t);
|
|
136
|
+
if (dark && !light) return "dark";
|
|
137
|
+
if (light && !dark) return "light";
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
// Rank rows: +2 per matched token in title, +1 in description/tags, theme match first.
|
|
141
|
+
// Returns { rows, scored } where scored explains the top pick for fallback_score.
|
|
142
|
+
function scoreRows(kind, rows, toks, themeHint) {
|
|
143
|
+
const scored = rows.map((r) => {
|
|
144
|
+
const title = String(r.title || "").toLowerCase();
|
|
145
|
+
const desc = String(r.description || "").toLowerCase();
|
|
146
|
+
const tags = ((r.tags || []).map((x) => String(x).toLowerCase()).join(" "));
|
|
147
|
+
let s = 0;
|
|
148
|
+
for (const t of toks) { if (title.includes(t)) s += 2; else if (desc.includes(t) || tags.includes(t)) s += 1; }
|
|
149
|
+
const th = r.facets && r.facets.theme;
|
|
150
|
+
const themeOk = !themeHint || th === themeHint || th === "mixed" || th === "unknown";
|
|
151
|
+
if (themeHint && th === themeHint) s += 3;
|
|
152
|
+
return { r, s, themeOk };
|
|
153
|
+
});
|
|
154
|
+
scored.sort((a, b) => ((b.themeOk ? 1 : 0) - (a.themeOk ? 1 : 0)) || (b.s - a.s) || ((b.r.views || 0) - (a.r.views || 0)));
|
|
155
|
+
return scored;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Kind-aware token overlap for the zero-overlap refusal gate (pure, no fetch).
|
|
159
|
+
// Components/assets: title-only (strict visual surfaces; tags/descriptions don't count).
|
|
160
|
+
// Skills/design_systems: wider net — title + description + tags + any snippet present.
|
|
161
|
+
function matchOverlap(kind, toks, item) {
|
|
162
|
+
const list = Array.isArray(toks) ? toks : [];
|
|
163
|
+
if (!list.length || !item) return 0;
|
|
164
|
+
if (kind === "components" || kind === "assets") {
|
|
165
|
+
const title = String(item.title || "").toLowerCase();
|
|
166
|
+
let n = 0;
|
|
167
|
+
for (const t of list) {
|
|
168
|
+
const tok = String(t ?? "").toLowerCase();
|
|
169
|
+
if (tok && title.includes(tok)) n++;
|
|
170
|
+
}
|
|
171
|
+
return n;
|
|
172
|
+
}
|
|
173
|
+
const parts = [String(item.title || ""), String(item.description || "")];
|
|
174
|
+
if (Array.isArray(item.tags)) parts.push(item.tags.map((x) => String(x)).join(" "));
|
|
175
|
+
for (const k of ["content_snippet", "content_excerpt", "preview_snippet", "tokens_snippet"]) {
|
|
176
|
+
if (typeof item[k] === "string" && item[k]) parts.push(item[k]);
|
|
177
|
+
}
|
|
178
|
+
const hay = parts.join("\n").toLowerCase();
|
|
179
|
+
let n = 0;
|
|
180
|
+
for (const t of list) {
|
|
181
|
+
const tok = String(t ?? "").toLowerCase();
|
|
182
|
+
if (tok && hay.includes(tok)) n++;
|
|
183
|
+
}
|
|
184
|
+
return n;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function windowAgoIso(days) {
|
|
188
|
+
const d = new Date();
|
|
189
|
+
d.setDate(d.getDate() - days);
|
|
190
|
+
return d.toISOString();
|
|
191
|
+
}
|
|
192
|
+
function weekAgoIso() { return windowAgoIso(7); }
|
|
193
|
+
function trendingIso(days = 90) { return windowAgoIso(days); } // 7d seed is empty; callers pass window_days (default 30 for components now)
|
|
194
|
+
|
|
195
|
+
function buildSearchParams(kind, q = {}) {
|
|
196
|
+
const p = new URLSearchParams();
|
|
197
|
+
p.set("select", LIST_COLS[kind]);
|
|
198
|
+
const limit = q.limit;
|
|
199
|
+
const offset = q.offset ?? 0;
|
|
200
|
+
p.set("limit", String(limit));
|
|
201
|
+
p.set("offset", String(offset));
|
|
202
|
+
const sort = CATALOG_SORTS[kind][q.sort] ?? CATALOG_SORTS[kind].popular;
|
|
203
|
+
p.set("order", sort);
|
|
204
|
+
const filters = [];
|
|
205
|
+
if (kind === "components" || kind === "assets") {
|
|
206
|
+
filters.push("private=eq.false");
|
|
207
|
+
if (q.freeOnly) filters.push("premium=eq.false");
|
|
208
|
+
} else if (kind === "skills" || kind === "design_systems") {
|
|
209
|
+
filters.push("private=eq.false");
|
|
210
|
+
}
|
|
211
|
+
if (q.tag && kind === "components") filters.push("tags=cs.{" + q.tag.toLowerCase() + "}");
|
|
212
|
+
if (q.mediaType && kind === "assets") filters.push("media_type=eq." + q.mediaType);
|
|
213
|
+
const ors = [];
|
|
214
|
+
if (q.query && q.query.trim()) {
|
|
215
|
+
const v = ilike(q.query);
|
|
216
|
+
if (kind === "components") ors.push("title.ilike." + v + ",description.ilike." + v);
|
|
217
|
+
else if (kind === "skills") ors.push("title.ilike." + v + ",description.ilike." + v);
|
|
218
|
+
else if (kind === "assets") ors.push("title.ilike." + v + ",description.ilike." + v);
|
|
219
|
+
else ors.push("title.ilike." + v + ",description.ilike." + v + ",content.ilike." + v + ",preview_html.ilike." + v);
|
|
220
|
+
}
|
|
221
|
+
if (q.sort === "trending") filters.push("created_at=gte." + trendingIso(q.window_days ?? 90));
|
|
222
|
+
return { params: p, filters, ors };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function buildUrl(base, table, params, filters, ors) {
|
|
226
|
+
let qs = params.toString();
|
|
227
|
+
for (const f of filters) qs += "&" + f;
|
|
228
|
+
if (ors.length) qs += "&or=(" + ors.join(",") + ")";
|
|
229
|
+
return base + "/rest/v1/" + table + "?" + qs;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Truncation with cursors: agents see full_length + chunk info instead of a silent
|
|
233
|
+
// marker, and can fetch the rest via getItem with { chunk } (see getItem below).
|
|
234
|
+
const trunc = (s, n) => (s && s.length > n ? s.slice(0, n) + "…[truncated]" : s);
|
|
235
|
+
function chunkText(s, n, chunk = 0) {
|
|
236
|
+
const full = String(s || "");
|
|
237
|
+
const total = Math.max(1, Math.ceil(full.length / n));
|
|
238
|
+
const i = Math.max(0, Math.min(chunk, total - 1));
|
|
239
|
+
return { text: full.slice(i * n, (i + 1) * n) + (i < total - 1 ? "…[truncated]" : ""), chunk: i, chunks: total, full_length: full.length, truncated: total > 1 };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Slim lists: drop heavy blobs (full code/content/preview_html) from list rows,
|
|
243
|
+
// keep excerpts + chunk pointers + full_fetch so agents fetch detail on purpose.
|
|
244
|
+
// getItem/bundleItems stay full — detail is their job. Default on (q.slim ?? true).
|
|
245
|
+
function slimRows(kind, items) {
|
|
246
|
+
return (items || []).map((it) => {
|
|
247
|
+
const o = { ...it };
|
|
248
|
+
if (kind === "components" && typeof o.code === "string") delete o.code; // keep code_excerpt + code_info + full_fetch
|
|
249
|
+
if (kind === "skills" && typeof o.content === "string") { o.content_excerpt = String(o.content).slice(0, 800); delete o.content; }
|
|
250
|
+
if (kind === "design_systems") {
|
|
251
|
+
if (typeof o.content === "string") { o.content_excerpt = String(o.content).slice(0, 800); delete o.content; }
|
|
252
|
+
if (typeof o.preview_html === "string") { o.preview_excerpt = String(o.preview_html).slice(0, 800); delete o.preview_html; }
|
|
253
|
+
}
|
|
254
|
+
return o;
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function pageUrl(kind, row) {
|
|
259
|
+
if (kind === "components") return SITE + "/component/" + row.slug;
|
|
260
|
+
if (kind === "skills") return SITE + "/skills/" + row.id;
|
|
261
|
+
// ISS9: asset pages are query-routed (no /assets/ID route exists). Keep the q= form
|
|
262
|
+
// but ALSO expose a stable key so agents can cite: id + slug + title together.
|
|
263
|
+
if (kind === "assets") return SITE + "/assets?q=" + encodeURIComponent(row.title ?? "");
|
|
264
|
+
return SITE + "/design-systems/" + (row.slug ?? row.id);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function assetKey(row) {
|
|
268
|
+
return { id: row.id, slug: row.slug ?? null, title: row.title ?? null };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function shapeRow(kind, row, cfg, withFacets) {
|
|
272
|
+
const base = { ...row };
|
|
273
|
+
// Facets probe the FULL available code (pre-truncation) so search facets equal
|
|
274
|
+
// detail facets and install-time needs: a short prefix missed late markers
|
|
275
|
+
// like data-lucide (id 1572 drift). Weight uses the true length (ISS1/2/3).
|
|
276
|
+
if (kind === "components" && typeof base.code === "string") {
|
|
277
|
+
base.code_chars = base.code.length;
|
|
278
|
+
if (withFacets) { try { base.facets = withFacets(kind, base, { fullLength: base.code.length }); } catch { /* never break rows */ } }
|
|
279
|
+
// Gap 3: code_excerpt (first 1200 chars, readable) + full_fetch flag. Agents skim
|
|
280
|
+
// the excerpt, then get chunk N — no more blind 18k-70k dumps or silent cuts.
|
|
281
|
+
base.code_excerpt = String(base.code).slice(0, 1200);
|
|
282
|
+
base.full_fetch = { tool: "aura_get_component", id: base.id ?? base.slug, chunks: Math.max(1, Math.ceil(base.code.length / cfg.codeChars)) };
|
|
283
|
+
const ch = chunkText(base.code, cfg.codeChars, 0);
|
|
284
|
+
base.code = ch.text;
|
|
285
|
+
base.code_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: 0, truncated: ch.truncated };
|
|
286
|
+
if (base.facets) base.facets.deps_summary = base.facets.needsTailwind || base.facets.needsIcons ? [base.facets.needsTailwind && "tailwindcss", base.facets.needsIcons && "iconify-icon"].filter(Boolean) : [];
|
|
287
|
+
}
|
|
288
|
+
if (kind === "skills" && typeof base.content === "string") {
|
|
289
|
+
const ch = chunkText(base.content, cfg.contentChars, 0);
|
|
290
|
+
base.content = ch.text;
|
|
291
|
+
base.content_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: 0, truncated: ch.truncated };
|
|
292
|
+
}
|
|
293
|
+
if (kind === "design_systems") {
|
|
294
|
+
if (typeof base.content === "string") {
|
|
295
|
+
const ch = chunkText(base.content, cfg.contentChars, 0);
|
|
296
|
+
base.content = ch.text;
|
|
297
|
+
base.content_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: 0, truncated: ch.truncated };
|
|
298
|
+
}
|
|
299
|
+
if (typeof base.preview_html === "string") {
|
|
300
|
+
const ch = chunkText(base.preview_html, cfg.codeChars, 0);
|
|
301
|
+
base.preview_html = ch.text;
|
|
302
|
+
base.preview_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: 0, truncated: ch.truncated };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
base.page_url = pageUrl(kind, row);
|
|
306
|
+
if (kind === "assets") base.asset_key = assetKey(row);
|
|
307
|
+
if (withFacets && !base.facets) { try { base.facets = withFacets(kind, base); } catch { /* facets never break rows */ } }
|
|
308
|
+
// ISS8: force absolute Supabase URLs — relative variant paths break fetch.
|
|
309
|
+
if (kind === "assets") {
|
|
310
|
+
const abs = (u) => (!u ? u : (/^https?:\/\//i.test(u) ? u : ("https://hoirqrkdgbmvpwutwuwj-all.supabase.co/storage/v1/object/public/assets/" + String(u).replace(/^\/+/, ""))));
|
|
311
|
+
for (const k of ["image_320w", "image_800w", "image_1600w", "image_3840w", "image_original", "image_url", "video_url", "video_poster_url"]) if (typeof base[k] === "string") base[k] = abs(base[k]);
|
|
312
|
+
if (base.facets) {
|
|
313
|
+
if (base.facets.download) base.facets.download = abs(base.facets.download);
|
|
314
|
+
if (base.facets.preview) base.facets.preview = abs(base.facets.preview);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return base;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = null }) {
|
|
321
|
+
const base = config.supabaseUrl.replace(/\/$/, "");
|
|
322
|
+
const headers = { apikey: config.anonKey, Authorization: "Bearer " + config.anonKey, Prefer: "count=exact" };
|
|
323
|
+
const cache = new Map();
|
|
324
|
+
const MAX_KEYS = 300;
|
|
325
|
+
const cacheGet = (k) => {
|
|
326
|
+
if (!config.cacheTtlMs) return null;
|
|
327
|
+
const e = cache.get(k);
|
|
328
|
+
if (!e) return null;
|
|
329
|
+
if (now() - e.t > config.cacheTtlMs) { cache.delete(k); return null; }
|
|
330
|
+
cache.delete(k); cache.set(k, e); // LRU refresh
|
|
331
|
+
return e.v;
|
|
332
|
+
};
|
|
333
|
+
const cacheSet = (k, v) => {
|
|
334
|
+
if (!config.cacheTtlMs) return;
|
|
335
|
+
if (cache.has(k)) cache.delete(k);
|
|
336
|
+
cache.set(k, { t: now(), v });
|
|
337
|
+
while (cache.size > MAX_KEYS) cache.delete(cache.keys().next().value);
|
|
338
|
+
};
|
|
339
|
+
const authorCache = new Map(); // created_by -> profile (shared across searches)
|
|
340
|
+
const inflight = new Map(); // cacheKey -> Promise (coalesce concurrent identical calls)
|
|
341
|
+
|
|
342
|
+
const clampLimit = (l) => Math.min(config.maxLimit, Math.max(1, l ?? config.defaultLimit));
|
|
343
|
+
|
|
344
|
+
async function enrichAuthors(rows) {
|
|
345
|
+
const ids = [...new Set(rows.map((r) => r.created_by).filter(Boolean).filter((id) => !authorCache.has(id)))];
|
|
346
|
+
if (ids.length) {
|
|
347
|
+
try {
|
|
348
|
+
const url = base + "/rest/v1/public_author_profiles?select=id,full_name,avatar_url,slug&id=in.(" + ids.map(esc).join(",") + ")";
|
|
349
|
+
const { rows: authors } = await fetcher.getJson(url, headers);
|
|
350
|
+
for (const a of authors) { authorCache.set(a.id, a); if (authorCache.size > 500) break; }
|
|
351
|
+
} catch { /* authors stay null, rows still return */ }
|
|
352
|
+
}
|
|
353
|
+
return rows.map((r) => ({ ...r, author: (r.created_by && authorCache.get(r.created_by)) || null }));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function searchCatalog(kind, q = {}) {
|
|
357
|
+
const limit = clampLimit(q.limit);
|
|
358
|
+
const offset = Math.max(0, q.offset ?? 0);
|
|
359
|
+
const sort = CATALOG_SORTS[kind][q.sort] ? q.sort : "popular";
|
|
360
|
+
const freeOnly = q.freeOnly ?? config.freeOnlyDefault;
|
|
361
|
+
// Gap 1: negative_theme (exclude a theme) + min_views (quality floor). Both ride
|
|
362
|
+
// the cache key so filtered/unfiltered variants never collide.
|
|
363
|
+
const negTheme = q.negative_theme ? String(q.negative_theme).toLowerCase() : "";
|
|
364
|
+
const minViews = Math.max(0, q.min_views ?? 0);
|
|
365
|
+
const slim = q.slim ?? true;
|
|
366
|
+
const exactFlag = q.exact_match === true ? "exact" : "fuzzy";
|
|
367
|
+
const key = ["search", kind, q.query ?? "", q.tag ?? "", q.mediaType ?? "", sort, limit, offset, freeOnly, negTheme, minViews, slim ? "slim" : "full", exactFlag].join("|");
|
|
368
|
+
const hit = cacheGet(key);
|
|
369
|
+
if (hit) return { ...hit, cached: true };
|
|
370
|
+
if (inflight.has(key)) return inflight.get(key);
|
|
371
|
+
const p = (async () => {
|
|
372
|
+
const runOnce = async (qq) => {
|
|
373
|
+
const { params, filters, ors } = buildSearchParams(kind, { ...qq, limit, offset, sort, freeOnly });
|
|
374
|
+
const url = buildUrl(base, kind, params, filters, ors);
|
|
375
|
+
return fetcher.getJson(url, headers);
|
|
376
|
+
};
|
|
377
|
+
let { rows, total } = await runOnce(q);
|
|
378
|
+
let fallback = null;
|
|
379
|
+
// AND-phrase queries like "dark cinematic portfolio" match nothing literally:
|
|
380
|
+
// retry as OR over significant tokens so every surface degrades instead of emptying.
|
|
381
|
+
// Theme words (dark/light/cinematic) are rank signals, not match tokens.
|
|
382
|
+
const toks = (q.query && q.query.trim() && !q.tag) ? queryTokens(q.query) : [];
|
|
383
|
+
const themeHint = (q.query && q.query.trim()) ? detectThemeHint(q.query) : null;
|
|
384
|
+
const effTheme = q.theme || themeHint;
|
|
385
|
+
const suggest = (t) => {
|
|
386
|
+
const s = [];
|
|
387
|
+
if (t.length > 1) s.push(t.slice(0, Math.min(3, t.length)).join(" "));
|
|
388
|
+
for (const x of t.slice(0, 3)) s.push(x);
|
|
389
|
+
return [...new Set(s)].slice(0, 4);
|
|
390
|
+
};
|
|
391
|
+
if ((!rows || !rows.length) && toks.length && q.exact_match !== true) {
|
|
392
|
+
// Pass 1: OR over tokens, same filters (drops the AND-phrase requirement).
|
|
393
|
+
const orParts = [];
|
|
394
|
+
for (const t of toks) {
|
|
395
|
+
const v = ilike(t);
|
|
396
|
+
if (kind === "design_systems") orParts.push("title.ilike." + v + ",description.ilike." + v + ",content.ilike." + v + ",preview_html.ilike." + v);
|
|
397
|
+
else orParts.push("title.ilike." + v + ",description.ilike." + v);
|
|
398
|
+
}
|
|
399
|
+
const b1 = buildSearchParams(kind, { ...q, query: "", limit, offset, sort, freeOnly });
|
|
400
|
+
try {
|
|
401
|
+
const fb = await fetcher.getJson(buildUrl(base, kind, b1.params, b1.filters, orParts), headers);
|
|
402
|
+
if (fb.rows && fb.rows.length) { rows = fb.rows; total = fb.total; fallback = "or-tokens:" + toks.join(","); }
|
|
403
|
+
} catch { /* try pass 2 */ }
|
|
404
|
+
// Pass 2: still empty (e.g. trending window + rare tokens): drop sort window,
|
|
405
|
+
// keep OR tokens, rank by views. Guarantees aggregators return something useful.
|
|
406
|
+
if ((!rows || !rows.length)) {
|
|
407
|
+
const b2 = buildSearchParams(kind, { ...q, query: "", limit, offset, sort: "popular", freeOnly });
|
|
408
|
+
try {
|
|
409
|
+
const fb2 = await fetcher.getJson(buildUrl(base, kind, b2.params, b2.filters, orParts), headers);
|
|
410
|
+
if (fb2.rows && fb2.rows.length) { rows = fb2.rows; total = fb2.total; fallback = "or-tokens-unwindowed:" + toks.join(","); }
|
|
411
|
+
} catch { /* try safety net */ }
|
|
412
|
+
}
|
|
413
|
+
// Safety net: total miss on a text query still returns usable items.
|
|
414
|
+
// Same freeOnly/tag/mediaType filters, popular order, labeled honestly —
|
|
415
|
+
// true never-empty (unless upstream itself is down).
|
|
416
|
+
if ((!rows || !rows.length)) {
|
|
417
|
+
const b3 = buildSearchParams(kind, { ...q, query: "", limit, offset, sort: "popular", freeOnly });
|
|
418
|
+
try {
|
|
419
|
+
const fb3 = await fetcher.getJson(buildUrl(base, kind, b3.params, b3.filters, []), headers);
|
|
420
|
+
if (fb3.rows && fb3.rows.length) { rows = fb3.rows; total = fb3.total; fallback = "popular-safety-net"; }
|
|
421
|
+
} catch { /* keep empty: upstream truth */ }
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// ISS7: dedupe skills by source_url (same SKILL.md listed twice). First (most-viewed)
|
|
425
|
+
// wins, gets canonical:true; dupes are dropped and counted in deduped.
|
|
426
|
+
let deduped = 0;
|
|
427
|
+
if (kind === "skills") {
|
|
428
|
+
const seen = new Set();
|
|
429
|
+
const kept = [];
|
|
430
|
+
for (const r of rows) {
|
|
431
|
+
const u = (r.source_url || "").trim().toLowerCase();
|
|
432
|
+
if (u && seen.has(u)) { deduped++; continue; }
|
|
433
|
+
if (u) seen.add(u);
|
|
434
|
+
kept.push(r);
|
|
435
|
+
}
|
|
436
|
+
const byUrl = new Map();
|
|
437
|
+
for (const r of kept) {
|
|
438
|
+
const u = (r.source_url || "").trim().toLowerCase();
|
|
439
|
+
if (u && (!byUrl.has(u) || (r.views || 0) > (byUrl.get(u).views || 0))) byUrl.set(u, r);
|
|
440
|
+
}
|
|
441
|
+
rows = kept.map((r) => {
|
|
442
|
+
const u = (r.source_url || "").trim().toLowerCase();
|
|
443
|
+
return (u && byUrl.get(u) === r) ? { ...r, canonical: true } : r;
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
// Gap 4: inline snippets — one extra batched fetch fills preview/tokens glimpses
|
|
447
|
+
// into search rows (top N only) so agents decide without a second call per row.
|
|
448
|
+
// Bounded: top 5 rows, client-side sliced to 800 chars. No extra call when empty.
|
|
449
|
+
// Same fetch also resolves facets.has_preview to a real boolean for those rows.
|
|
450
|
+
let presence = null;
|
|
451
|
+
if ((kind === "design_systems" || kind === "skills") && rows.length) {
|
|
452
|
+
const topIds = rows.slice(0, 5).map((r) => r.id);
|
|
453
|
+
try {
|
|
454
|
+
const idList = topIds.map((id) => encodeURIComponent(String(id))).join(",");
|
|
455
|
+
const sel = kind === "design_systems" ? "id,content,preview_html" : "id,content,source_url";
|
|
456
|
+
const { rows: full } = await fetcher.getJson(base + "/rest/v1/" + kind + "?select=" + encodeURIComponent(sel) + "&id=in.(" + idList + ")", headers);
|
|
457
|
+
const byId = new Map(full.map((r) => [String(r.id), r]));
|
|
458
|
+
if (kind === "design_systems") presence = new Map(full.map((r) => [String(r.id), Boolean(r.preview_html)]));
|
|
459
|
+
rows = rows.map((r) => {
|
|
460
|
+
const f = byId.get(String(r.id));
|
|
461
|
+
if (!f) return r;
|
|
462
|
+
if (kind === "design_systems") return { ...r, preview_snippet: String(f.preview_html || "").slice(0, 800), tokens_snippet: String(f.content || "").slice(0, 800) };
|
|
463
|
+
return { ...r, content_snippet: String(f.content || "").slice(0, 800) };
|
|
464
|
+
});
|
|
465
|
+
} catch { /* snippets are best-effort; rows still return */ }
|
|
466
|
+
}
|
|
467
|
+
const enriched = await enrichAuthors(rows);
|
|
468
|
+
const out = { kind, total, limit, offset, items: enriched.map((r) => shapeRow(kind, r, config, facetsFn)), cached: false };
|
|
469
|
+
if (deduped) out.deduped = deduped;
|
|
470
|
+
// Gap 1: min_views quality floor (views-gated surfaces only) + negative_theme
|
|
471
|
+
// hard exclusion. Both apply before theme re-rank so counts stay honest.
|
|
472
|
+
if (minViews > 0) out.items = out.items.filter((it) => (it.views || 0) >= minViews);
|
|
473
|
+
if (negTheme && kind === "components") {
|
|
474
|
+
const before = out.items.length;
|
|
475
|
+
out.items = out.items.filter((it) => !it.facets || it.facets.theme !== negTheme);
|
|
476
|
+
if (out.items.length < before) out.negative_theme_filtered_out = before - out.items.length;
|
|
477
|
+
}
|
|
478
|
+
// Theme-aware re-rank (components): theme hint from query (dark/light/cinematic)
|
|
479
|
+
// or explicit q.theme reorders so matches come first — never silently drops rows,
|
|
480
|
+
// and always reports checked/kept counts so the re-rank is auditable, not opaque.
|
|
481
|
+
if (kind === "components" && effTheme) {
|
|
482
|
+
const scored = scoreRows(kind, out.items, toks, effTheme);
|
|
483
|
+
const themeOkCount = scored.filter((s) => s.themeOk).length;
|
|
484
|
+
out.theme_hint = effTheme;
|
|
485
|
+
out.theme_checked = scored.length;
|
|
486
|
+
out.theme_kept = out.items.length;
|
|
487
|
+
if (themeOkCount === 0) {
|
|
488
|
+
out.theme_warning = "no " + effTheme + "-theme rows in top results; showing unfiltered, check facets.theme per row";
|
|
489
|
+
} else {
|
|
490
|
+
out.items = scored.map((s) => s.r);
|
|
491
|
+
const dep = scored.length - themeOkCount;
|
|
492
|
+
if (dep > 0) out.theme_deprioritized = dep;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
// Real has_preview for snippet-enriched rows (top 5); the rest stay unknown-list.
|
|
496
|
+
if (presence) {
|
|
497
|
+
for (const it of out.items) {
|
|
498
|
+
if (presence.has(String(it.id)) && it.facets) it.facets = { ...it.facets, has_preview: presence.get(String(it.id)) };
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
// ISS15: transparent fallback — agents see what happened, not a magic string.
|
|
502
|
+
// Safety net additionally carries suggested_queries so agents can retry sharp.
|
|
503
|
+
const suggestList = () => { const sq = suggest(toks).filter((s) => !/^[a-z0-9]{8,}$/i.test(s.replace(/\s/g, ""))); return sq.length ? sq : ["hero", "pricing", "landing page"]; };
|
|
504
|
+
if (fallback) {
|
|
505
|
+
out.fallback = fallback; out.isFallback = true;
|
|
506
|
+
const scoredTop = out.items[0];
|
|
507
|
+
const hits = matchOverlap(kind, toks, scoredTop);
|
|
508
|
+
out.fallback_score = "token-overlap:" + hits + "/" + toks.length + "-terms" + (effTheme ? ";theme:" + effTheme : "");
|
|
509
|
+
if (fallback === "popular-safety-net") out.suggested_queries = suggestList();
|
|
510
|
+
}
|
|
511
|
+
else if ((!rows || !rows.length) && toks.length) { out.isFallback = false; out.suggested_queries = suggestList(); }
|
|
512
|
+
// Zero-overlap refusal: fallback engaged but top row matches 0/N tokens —
|
|
513
|
+
// honest empty instead of irrelevant popular rows. Direct hits never refused.
|
|
514
|
+
// Kind-aware: components/assets stay title-only strict; skills/systems use the
|
|
515
|
+
// wider net (title+description+tags+snippets) via matchOverlap.
|
|
516
|
+
if (fallback != null && toks.length > 0) {
|
|
517
|
+
const top = out.items[0];
|
|
518
|
+
const hits2 = matchOverlap(kind, toks, top);
|
|
519
|
+
if (hits2 === 0) {
|
|
520
|
+
const refused = fallback;
|
|
521
|
+
out.items = [];
|
|
522
|
+
out.total = 0;
|
|
523
|
+
out.isFallback = false;
|
|
524
|
+
delete out.fallback;
|
|
525
|
+
delete out.fallback_score;
|
|
526
|
+
out.no_match = true;
|
|
527
|
+
out.refused_fallback = refused;
|
|
528
|
+
out.suggested_queries = suggestList();
|
|
529
|
+
out.note = "Top fallback result matched 0/" + toks.length + " title tokens — empty instead of irrelevant popular rows. Try a suggested query.";
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
if (slim) out.items = slimRows(kind, out.items);
|
|
533
|
+
cacheSet(key, out);
|
|
534
|
+
return out;
|
|
535
|
+
})();
|
|
536
|
+
inflight.set(key, p);
|
|
537
|
+
try { return await p; } finally { inflight.delete(key); }
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async function getItem(kind, idOrSlug, opts = {}) {
|
|
541
|
+
const chunk = Math.max(0, opts.chunk ?? 0);
|
|
542
|
+
const wantFull = !!opts.full;
|
|
543
|
+
const key = ["get", kind, String(idOrSlug), "c" + chunk, wantFull ? "full" : "trunc"].join("|");
|
|
544
|
+
const hit = cacheGet(key);
|
|
545
|
+
if (hit) return { ...hit, cached: true };
|
|
546
|
+
const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(String(idOrSlug));
|
|
547
|
+
const isNum = /^\d+$/.test(String(idOrSlug));
|
|
548
|
+
const sel = DETAIL_COLS[kind];
|
|
549
|
+
// design_systems + skills: uuid id vs slug; components: numeric id vs slug
|
|
550
|
+
const filt = (kind === "design_systems" || kind === "skills")
|
|
551
|
+
? (isUuid ? "id=eq." + encodeURIComponent(String(idOrSlug)) : "slug=eq." + encodeURIComponent(String(idOrSlug)))
|
|
552
|
+
: (kind === "components" && !isNum
|
|
553
|
+
? "slug=eq." + encodeURIComponent(String(idOrSlug))
|
|
554
|
+
: "id=eq." + encodeURIComponent(String(idOrSlug)));
|
|
555
|
+
const url = base + "/rest/v1/" + kind + "?select=" + encodeURIComponent(sel) + "&" + filt + "&limit=1";
|
|
556
|
+
const { rows } = await fetcher.getJson(url, headers);
|
|
557
|
+
if (!rows.length) { const e = new Error("not found: " + kind + " " + idOrSlug); e.code = "NOT_FOUND"; throw e; }
|
|
558
|
+
const enriched = await enrichAuthors(rows);
|
|
559
|
+
const full = enriched[0];
|
|
560
|
+
// Chunked fetch: re-slice full blobs per requested chunk so agents can page
|
|
561
|
+
// through 18k-70k payloads instead of hitting one silent truncation wall.
|
|
562
|
+
const item = shapeRow(kind, full, config, facetsFn);
|
|
563
|
+
if (chunk > 0 && !wantFull) {
|
|
564
|
+
if (kind === "components" && typeof full.code === "string") {
|
|
565
|
+
const ch = chunkText(full.code, config.codeChars, chunk);
|
|
566
|
+
item.code = ch.text;
|
|
567
|
+
item.code_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: ch.chunk, truncated: ch.truncated };
|
|
568
|
+
}
|
|
569
|
+
if ((kind === "skills") && typeof full.content === "string") {
|
|
570
|
+
const ch = chunkText(full.content, config.contentChars, chunk);
|
|
571
|
+
item.content = ch.text;
|
|
572
|
+
item.content_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: ch.chunk, truncated: ch.truncated };
|
|
573
|
+
}
|
|
574
|
+
if (kind === "design_systems") {
|
|
575
|
+
if (typeof full.content === "string") {
|
|
576
|
+
const ch = chunkText(full.content, config.contentChars, chunk);
|
|
577
|
+
item.content = ch.text;
|
|
578
|
+
item.content_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: ch.chunk, truncated: ch.truncated };
|
|
579
|
+
}
|
|
580
|
+
if (typeof full.preview_html === "string") {
|
|
581
|
+
const ch = chunkText(full.preview_html, config.codeChars, chunk);
|
|
582
|
+
item.preview_html = ch.text;
|
|
583
|
+
item.preview_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: ch.chunk, truncated: ch.truncated };
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (wantFull) {
|
|
588
|
+
if (kind === "components" && typeof full.code === "string") {
|
|
589
|
+
item.code = full.code;
|
|
590
|
+
item.code_info = { full_length: full.code.length, chunks: 1, chunk: 0, truncated: false };
|
|
591
|
+
}
|
|
592
|
+
if (kind === "skills" && typeof full.content === "string") {
|
|
593
|
+
item.content = full.content;
|
|
594
|
+
item.content_info = { full_length: full.content.length, chunks: 1, chunk: 0, truncated: false };
|
|
595
|
+
}
|
|
596
|
+
if (kind === "design_systems") {
|
|
597
|
+
if (typeof full.content === "string") {
|
|
598
|
+
item.content = full.content;
|
|
599
|
+
item.content_info = { full_length: full.content.length, chunks: 1, chunk: 0, truncated: false };
|
|
600
|
+
}
|
|
601
|
+
if (typeof full.preview_html === "string") {
|
|
602
|
+
item.preview_html = full.preview_html;
|
|
603
|
+
item.preview_info = { full_length: full.preview_html.length, chunks: 1, chunk: 0, truncated: false };
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
const out = { kind, item, cached: false };
|
|
608
|
+
cacheSet(key, out);
|
|
609
|
+
return out;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
async function getStatus() {
|
|
613
|
+
const key = "status";
|
|
614
|
+
const hit = cacheGet(key);
|
|
615
|
+
if (hit) return { ...hit, cached: true };
|
|
616
|
+
const counts = {};
|
|
617
|
+
const jobs = [
|
|
618
|
+
["components_free", "components?select=id&private=eq.false&premium=eq.false&limit=1"],
|
|
619
|
+
["components_pro", "components?select=id&private=eq.false&premium=eq.true&limit=1"],
|
|
620
|
+
["skills", "skills?select=id&private=eq.false&limit=1"],
|
|
621
|
+
["assets", "assets?select=id&private=eq.false&limit=1"],
|
|
622
|
+
["design_systems", "design_systems?select=id&private=eq.false&limit=1"],
|
|
623
|
+
];
|
|
624
|
+
await Promise.all(jobs.map(async ([k, path]) => {
|
|
625
|
+
try { const { total } = await fetcher.getJson(base + "/rest/v1/" + path, headers); counts[k] = total; }
|
|
626
|
+
catch { counts[k] = null; }
|
|
627
|
+
}));
|
|
628
|
+
// Freshness: best-effort MAX(updated_at) per table so agents see staleness.
|
|
629
|
+
// Never throws; null on failure; cached with the status entry.
|
|
630
|
+
const freshness = { components: null, skills: null, assets: null, design_systems: null };
|
|
631
|
+
await Promise.all(Object.keys(freshness).map(async (table) => {
|
|
632
|
+
try {
|
|
633
|
+
const { rows } = await fetcher.getJson(base + "/rest/v1/" + table + "?select=updated_at&order=updated_at.desc&limit=1", headers);
|
|
634
|
+
const v = rows && rows[0] ? rows[0].updated_at : null;
|
|
635
|
+
freshness[table] = v ? String(v) : null;
|
|
636
|
+
} catch { freshness[table] = null; }
|
|
637
|
+
}));
|
|
638
|
+
const out = { site: SITE, catalogue: "aura.build public catalogue (PostgREST, anon)", counts, freshness, cached: false };
|
|
639
|
+
cacheSet(key, out);
|
|
640
|
+
return out;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
async function categoryCounts() {
|
|
644
|
+
const key = 'categories';
|
|
645
|
+
const hit = cacheGet(key);
|
|
646
|
+
if (hit) return hit.categories;
|
|
647
|
+
const cats = ['hero','section','button','card','background','header','logo','feature','pricing','testimonial','footer','form','heading'];
|
|
648
|
+
const out = await Promise.all(cats.map(async (c) => {
|
|
649
|
+
try {
|
|
650
|
+
const url = base + '/rest/v1/components?select=id&private=eq.false&premium=eq.false&tags=cs.{' + c + '}&limit=1';
|
|
651
|
+
const { total } = await fetcher.getJson(url, headers);
|
|
652
|
+
return { category: c, free: total };
|
|
653
|
+
} catch { return { category: c, free: null }; }
|
|
654
|
+
}));
|
|
655
|
+
cacheSet(key, { categories: out });
|
|
656
|
+
return out;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Gap 2: editorial picks — human-grade component leads for when views-signal is
|
|
660
|
+
// noise (free trending total=2). Top free by forks (remix = real reuse), then views.
|
|
661
|
+
async function editorialPicks(limit = 5) {
|
|
662
|
+
const lim = Math.max(1, Math.min(limit || 5, 8));
|
|
663
|
+
const sel = encodeURIComponent(LIST_COLS.components);
|
|
664
|
+
const url = base + "/rest/v1/components?select=" + sel + "&private=eq.false&premium=eq.false&order=forks.desc,views.desc&limit=" + lim;
|
|
665
|
+
try {
|
|
666
|
+
const { rows } = await fetcher.getJson(url, headers);
|
|
667
|
+
const enriched = await enrichAuthors(rows);
|
|
668
|
+
return enriched.map((r) => slimRows("components", [shapeRow("components", r, config, facetsFn)])[0]);
|
|
669
|
+
} catch { return []; }
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Bulk fetch: N details in one parallel round (backs aura_bundle + aura_scaffold_page).
|
|
673
|
+
// Per-id errors are captured, never thrown: { ok:true, item } or { ok:false, id, error }.
|
|
674
|
+
async function bundleItems(kind, ids, max = 5, opts = {}) {
|
|
675
|
+
const list = [...new Set((ids || []).map(String))].slice(0, Math.max(1, Math.min(max, 8)));
|
|
676
|
+
const out = await Promise.all(list.map(async (id) => {
|
|
677
|
+
try { const got = await getItem(kind, id, { full: !!opts.full }); return { ok: true, id, item: got.item }; }
|
|
678
|
+
catch (e) { return { ok: false, id, error: (e && e.code === "NOT_FOUND") ? "not found" : String((e && e.message) || e).slice(0, 160) }; }
|
|
679
|
+
}));
|
|
680
|
+
return out;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Related: same-tag / same-text overlap, views-ranked, excluding self.
|
|
684
|
+
// ISS10: freeOnly default true — free flows never get Pro recommendations.
|
|
685
|
+
async function relatedItems(kind, idOrSlug, limit = 3, freeOnly = true) {
|
|
686
|
+
const got = await getItem(kind, idOrSlug);
|
|
687
|
+
const item = got.item;
|
|
688
|
+
const toks = queryTokens(item.title + " " + (item.description || "")).slice(0, 3);
|
|
689
|
+
const orParts = toks.map((t) => { const v = ilike(t); return "title.ilike." + v + ",description.ilike." + v; });
|
|
690
|
+
const tagF = (kind === "components" && item.tags && item.tags.length) ? "&tags=cs.{" + String(item.tags[0]).toLowerCase() + "}" : "";
|
|
691
|
+
const lim = Math.max(1, Math.min(limit || 3, 8));
|
|
692
|
+
const sel = LIST_COLS[kind];
|
|
693
|
+
const idCol = (kind === "components") ? "id" : "id";
|
|
694
|
+
const selfId = encodeURIComponent(String(item.id));
|
|
695
|
+
const premF = (freeOnly && (kind === "components" || kind === "assets")) ? "&premium=eq.false" : "";
|
|
696
|
+
const url = base + "/rest/v1/" + kind + "?select=" + encodeURIComponent(sel) + "&private=eq.false" + premF + tagF + "&" + idCol + "=neq." + selfId + (orParts.length ? "&or=(" + orParts.join(",") + ")" : "") + "&order=views.desc&limit=" + lim;
|
|
697
|
+
try {
|
|
698
|
+
const { rows } = await fetcher.getJson(url, headers);
|
|
699
|
+
const enriched = await enrichAuthors(rows);
|
|
700
|
+
return { item: shapeRow(kind, item, config), related: slimRows(kind, enriched.map((r) => shapeRow(kind, r, config))) };
|
|
701
|
+
} catch { return { item: shapeRow(kind, item, config), related: [] }; }
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
return { searchCatalog, getItem, getStatus, categoryCounts, bundleItems, relatedItems, editorialPicks };
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// ---- protocol.mjs ----
|
|
708
|
+
const SERVER_INFO = { name: "aura-components-mcp", version: "1.6.3" };
|
|
709
|
+
// Module: protocol — stdio JSON-RPC framing + MCP handshake + error codes.
|
|
710
|
+
// One interface: createSession(send) -> { dispatch(msg) }. No business logic.
|
|
711
|
+
|
|
712
|
+
const PROTOCOL_VERSION = "2024-11-05";
|
|
713
|
+
|
|
714
|
+
const protocolFn_err = (code, message, data) => ({ code, message, ...(data === undefined ? {} : { data }) });
|
|
715
|
+
const Errors = {
|
|
716
|
+
parse: (m) => protocolFn_err(-32700, "Parse error" + (m ? ": " + m : "")),
|
|
717
|
+
invalidRequest: (m) => protocolFn_err(-32600, "Invalid Request" + (m ? ": " + m : "")),
|
|
718
|
+
methodNotFound: (m) => protocolFn_err(-32601, "Method not found: " + m),
|
|
719
|
+
invalidParams: (m) => protocolFn_err(-32602, "Invalid params" + (m ? ": " + m : "")),
|
|
720
|
+
upstream: (m) => protocolFn_err(-32000, "Upstream error" + (m ? ": " + m : "")),
|
|
721
|
+
notFound: (m) => protocolFn_err(-32004, m || "Not found"),
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
function textResult(obj) {
|
|
725
|
+
return { content: [{ type: "text", text: JSON.stringify(obj, null, 2) }] };
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function createSession(send, { tools, handlers }) {
|
|
729
|
+
let initialized = false;
|
|
730
|
+
const ok = (id, result) => send({ jsonrpc: "2.0", id, result });
|
|
731
|
+
const fail = (id, e) => send({ jsonrpc: "2.0", id, error: e });
|
|
732
|
+
|
|
733
|
+
async function dispatch(msg) {
|
|
734
|
+
if (!msg || typeof msg !== "object" || msg.jsonrpc !== "2.0") {
|
|
735
|
+
if (msg && "id" in msg && msg.id !== undefined && msg.id !== null) fail(msg.id, Errors.invalidRequest("bad envelope"));
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
const { id, method, params } = msg;
|
|
739
|
+
const isCall = id !== undefined && id !== null;
|
|
740
|
+
try {
|
|
741
|
+
if (method === "initialize") {
|
|
742
|
+
initialized = true;
|
|
743
|
+
if (!isCall) return;
|
|
744
|
+
return ok(id, { protocolVersion: PROTOCOL_VERSION, capabilities: { tools: {} }, serverInfo: SERVER_INFO });
|
|
745
|
+
}
|
|
746
|
+
if (method === "notifications/initialized") return;
|
|
747
|
+
if (method === "ping") { if (isCall) return ok(id, {}); return; }
|
|
748
|
+
if (!initialized) { if (isCall) fail(id, Errors.invalidRequest("not initialized")); return; }
|
|
749
|
+
if (method === "tools/list") { if (isCall) return ok(id, { tools }); return; }
|
|
750
|
+
if (method === "tools/call") {
|
|
751
|
+
if (!isCall) return;
|
|
752
|
+
const name = params?.name;
|
|
753
|
+
const args = params?.arguments ?? {};
|
|
754
|
+
const h = handlers[name];
|
|
755
|
+
if (!h) return fail(id, Errors.methodNotFound("tool " + name));
|
|
756
|
+
try {
|
|
757
|
+
const result = await h(args);
|
|
758
|
+
return ok(id, result);
|
|
759
|
+
} catch (e) {
|
|
760
|
+
if (e && e.code === "NOT_FOUND") return fail(id, Errors.notFound(e.message));
|
|
761
|
+
if (e && e.code === "BAD_ARGS") return fail(id, Errors.invalidParams(e.message));
|
|
762
|
+
return fail(id, Errors.upstream(e?.message ?? String(e)));
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
if (isCall) return fail(id, Errors.methodNotFound(method));
|
|
766
|
+
} catch (e) {
|
|
767
|
+
if (isCall) fail(id, Errors.upstream(e?.message ?? String(e)));
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
return { dispatch };
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// ---- guide.mjs ----
|
|
775
|
+
// Module: guide — small interface (installGuide, tokenHints, detectNeeds)
|
|
776
|
+
// turning raw catalogue rows into paste-ready help: setup steps, file map,
|
|
777
|
+
// dependency list, token starter. Derived from row fields, no extra fetches.
|
|
778
|
+
|
|
779
|
+
const GUIDE_CDN = {
|
|
780
|
+
tailwind: 'https://cdn.tailwindcss.com',
|
|
781
|
+
iconify: 'https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js',
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
// Font allowlist: real families only. Tailwind weight/size utilities (medium, semibold,
|
|
785
|
+
// normal, light-as-weight, size-*) are NOT families — old regex leaked them into fonts[].
|
|
786
|
+
const KNOWN_FONTS = ['inter', 'geist', 'manrope', 'sora', 'space-grotesk', 'space-mono', 'poppins', 'montserrat', 'playfair-display', 'instrument-serif', 'dm-sans', 'dm-serif-display', 'jetbrains-mono', 'ibm-plex-mono', 'ibm-plex-serif', 'plus-jakarta-sans', 'bricolage-grotesque', 'newsreader', 'fraunces', 'lora', 'merriweather', 'libre-baskerville', 'cormorant-garamond', 'gfs-didot', 'bodoni-moda', 'cardo', 'marcellus', 'gloock', 'aboreto', 'syne', 'urbanist', 'sora', 'anton', 'bebas-neue', 'archivo-black', 'league-gothic', 'fredoka', 'chewy', 'foldit', 'oi', 'honk', 'nabla', 'quicksand', 'nunito', 'work-sans', 'oswald', 'lora', 'roboto', 'open-sans', 'figtree', 'outfit', 'lexend', 'gloria-hallelujah', 'caveat'];
|
|
787
|
+
const FONT_RE = /font-(?:family-)?\[?['"]?([a-z][a-z0-9-]*)/g;
|
|
788
|
+
|
|
789
|
+
function detectNeeds(code, tags) {
|
|
790
|
+
const c = String(code || '').toLowerCase();
|
|
791
|
+
const t = new Set((tags || []).map((x) => String(x).toLowerCase()));
|
|
792
|
+
const needsTailwind = c.includes('class=') || t.has('tailwind');
|
|
793
|
+
// Probes must work on a 2k prefix too: icon svgs usually appear in the first screen of markup.
|
|
794
|
+
// svg alone under-matches in lists (no code fetched) — but must not over-claim either:
|
|
795
|
+
// 'svg' counts only with an icon-ish neighbor (iconify, <svg, lucide, heroicons, feather).
|
|
796
|
+
const needsIcons = c.includes('iconify') || c.includes('<svg') || c.includes('lucide') || c.includes('heroicon') || c.includes('data-lucide');
|
|
797
|
+
const needsKeyframes = c.includes('keyframes') || c.includes('animation:');
|
|
798
|
+
const fonts = new Set();
|
|
799
|
+
for (const m of c.matchAll(FONT_RE)) {
|
|
800
|
+
const fam = m[1].replace(/['"\]]/g, '');
|
|
801
|
+
if (KNOWN_FONTS.includes(fam)) fonts.add(fam);
|
|
802
|
+
}
|
|
803
|
+
// font-family: 'X', Y declarations (not utility classes)
|
|
804
|
+
for (const m of c.matchAll(/font-family\s*:\s*([^;}]{1,80})/g)) {
|
|
805
|
+
for (const part of m[1].split(',')) {
|
|
806
|
+
const fam = part.trim().replace(/['"]/g, '').toLowerCase().replace(/\s+/g, '-');
|
|
807
|
+
if (KNOWN_FONTS.includes(fam)) fonts.add(fam);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return { needsTailwind, needsIcons, needsKeyframes, fonts: [...fonts].slice(0, 6) };
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
// Facets: cheap derived signals so search lists answer "dark? heavy? pro?" without a get.
|
|
814
|
+
// theme: dark|light|mixed|unknown from background field + code palette probes.
|
|
815
|
+
// weight: code_chars bucket (s/m/l) so agents can prefer light embeds.
|
|
816
|
+
function facets(kind, row, opts) {
|
|
817
|
+
const f = {};
|
|
818
|
+
if (kind === 'components') {
|
|
819
|
+
const bg = String(row.background || '').toLowerCase();
|
|
820
|
+
const tags = (row.tags || []).map((x) => String(x).toLowerCase());
|
|
821
|
+
const code = String(row.code || '').toLowerCase();
|
|
822
|
+
const tagDark = tags.includes('dark');
|
|
823
|
+
const tagLight = tags.includes('light');
|
|
824
|
+
const darkHits = (code.match(/#0{3,6}\b|#1[0-9a-f]{5}\b|bg-black|bg-neutral-9|bg-zinc-9|bg-slate-9|text-white|slate-300/g) || []).length;
|
|
825
|
+
const lightHits = (code.match(/bg-white|bg-neutral-50|bg-slate-50|bg-gray-50|text-black|text-neutral-9/g) || []).length;
|
|
826
|
+
const bgDark = bg.includes('000') && !bg.includes('fff');
|
|
827
|
+
const bgLight = bg.includes('fff') && !bg.includes('000');
|
|
828
|
+
f.theme = bgDark || tagDark ? 'dark' : (bgLight || tagLight ? 'light' : (code ? (darkHits > lightHits * 2 ? 'dark' : (lightHits > darkHits * 2 ? 'light' : (darkHits || lightHits ? 'mixed' : 'unknown'))) : (tags.includes('saas') || tags.includes('minimal') ? 'light' : 'unknown')));
|
|
829
|
+
const n = (opts && opts.fullLength) || String(row.code || '').length;
|
|
830
|
+
f.weight = n > 20000 ? 'l' : (n > 8000 ? 'm' : 's');
|
|
831
|
+
f.code_chars = n;
|
|
832
|
+
const needs = detectNeeds(row.code || '', row.tags || []);
|
|
833
|
+
f.needsTailwind = needs.needsTailwind;
|
|
834
|
+
f.needsIcons = needs.needsIcons;
|
|
835
|
+
f.fonts = needs.fonts;
|
|
836
|
+
f.license = 'all-rights-reserved (Aura Terms §4) — check page_url before commercial reuse';
|
|
837
|
+
}
|
|
838
|
+
// ISS14 lives here too: license is top-level on every asset row, not buried.
|
|
839
|
+
// ISS13: rewrite hotlinked demo images to stable asset references where possible.
|
|
840
|
+
// i.pravatar.cc + bare demo URLs 404 in production; flag them in the install plan.
|
|
841
|
+
if (kind === 'components' && typeof row.code === 'string') {
|
|
842
|
+
const hot = [...row.code.matchAll(/<img[^>]+src=["']([^"']+)["']/gi)].map((m) => m[1]).filter((u) => /pravatar|placehold|dummyimage|unsplash\/photos\/..\/download|example\.com/i.test(u));
|
|
843
|
+
if (hot.length) row = { ...row, _hotlinked: [...new Set(hot)].slice(0, 5) };
|
|
844
|
+
}
|
|
845
|
+
if (kind === 'assets' || row.image_800w || row.image_original || row.video_url) {
|
|
846
|
+
// Resolved 2026-09-13 from primary source https://www.aura.build/terms §4-5:
|
|
847
|
+
// catalogue content is the exclusive property of DESIGNCODE IO PTE. LTD.;
|
|
848
|
+
// no per-asset license column exists, so commercial reuse needs Aura's permission.
|
|
849
|
+
f.license = 'all-rights-reserved (Aura Terms §4: DESIGNCODE IO PTE. LTD.) — personal/preview use via page_url; commercial reuse needs Aura permission (support@designcode.io)';
|
|
850
|
+
// Gap 5: machine-readable license gate. commercial_ok:false always (no per-asset
|
|
851
|
+
// column exists to prove otherwise) — agents filter without parsing prose.
|
|
852
|
+
f.commercial_ok = false;
|
|
853
|
+
f.terms_url = 'https://www.aura.build/terms';
|
|
854
|
+
f.download = row.image_original || row.image_1600w || row.image_800w || row.video_url || null;
|
|
855
|
+
f.preview = row.image_800w || row.video_poster_url || null;
|
|
856
|
+
}
|
|
857
|
+
if (kind === 'design_systems') {
|
|
858
|
+
// ISS4: lists never fetch preview_html (24KB/row). has_preview:true is authoritative
|
|
859
|
+
// only on detail; on lists report 'unknown-list' so agents fetch instead of trusting false.
|
|
860
|
+
f.has_preview = row.preview_html !== undefined ? Boolean(row.preview_html) : 'unknown-list';
|
|
861
|
+
}
|
|
862
|
+
return f;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function installGuide(kind, row) {
|
|
866
|
+
const steps = [];
|
|
867
|
+
const files = [];
|
|
868
|
+
const deps = [];
|
|
869
|
+
const url = row.page_url || '';
|
|
870
|
+
if (kind === 'components') {
|
|
871
|
+
const needs = detectNeeds(row.code || '', row.tags || []);
|
|
872
|
+
if (needs.needsTailwind) {
|
|
873
|
+
deps.push({ name: 'tailwindcss', via: 'CDN or project setup', cdn: GUIDE_CDN.tailwind });
|
|
874
|
+
steps.push('Add the Tailwind CDN script to head for a quick preview, or paste the markup into a Tailwind project.');
|
|
875
|
+
}
|
|
876
|
+
if (needs.needsIcons) {
|
|
877
|
+
deps.push({ name: 'iconify-icon', via: 'CDN', cdn: GUIDE_CDN.iconify });
|
|
878
|
+
steps.push('Add the iconify-icon CDN script so the icon tags render.');
|
|
879
|
+
}
|
|
880
|
+
if (needs.needsKeyframes) steps.push('Keep the embedded style block with the markup: that is where the keyframes live.');
|
|
881
|
+
// ISS11+12: pagination + CSS scoping. Code ships truncated at codeChars; the scope
|
|
882
|
+
// prefix below prevents .button/.inner/.point collisions in multi-component scaffolds.
|
|
883
|
+
const scope = 'aura-' + String(row.slug || row.id).toLowerCase().replace(/[^a-z0-9]+/g, '');
|
|
884
|
+
const fullLen = String(row.code || '').length;
|
|
885
|
+
const isTrunc = /…\[truncated\]$/.test(String(row.code || ''));
|
|
886
|
+
if (isTrunc) {
|
|
887
|
+
files.push({ path: 'components/' + (row.slug || row.id) + '.part1.html', contains: 'markup part 1 of N — fetch aura_get_component again and concatenate code chunks' });
|
|
888
|
+
steps.push('Code is paginated (' + fullLen + ' chars shown of more): install part files in order, or re-fetch for the full body.');
|
|
889
|
+
} else {
|
|
890
|
+
files.push({ path: 'components/' + (row.slug || row.id) + '.html', contains: 'section markup plus its style block' });
|
|
891
|
+
}
|
|
892
|
+
steps.push('Scope global CSS before pasting: prefix bare selectors (.button, .inner, .point) with .' + scope + ' to avoid collisions in multi-component pages.');
|
|
893
|
+
steps.push('Paste the markup where the section belongs and update the CTA link (it ships as #).');
|
|
894
|
+
if (url) steps.push('Preview first: ' + url);
|
|
895
|
+
if (row._hotlinked && row._hotlinked.length) steps.push('Replace demo images before shipping (' + row._hotlinked.slice(0, 3).join(', ') + '): hotlinked placeholders 404 in production — swap for aura_search_assets results.');
|
|
896
|
+
if (needs.fonts.length) files.push({ path: 'styles/fonts.css', contains: 'font families referenced: ' + needs.fonts.join(', ') });
|
|
897
|
+
// Chunk concat helper: truncated installs carry the exact re-fetch plan so no
|
|
898
|
+
// agent is left guessing how part files map to chunks.
|
|
899
|
+
const ci = row.code_info || null;
|
|
900
|
+
const concat = isTrunc ? {
|
|
901
|
+
tool: 'aura_get_component',
|
|
902
|
+
id: row.slug || row.id,
|
|
903
|
+
chunks: (ci && ci.chunks) || 'unknown — call once with chunk:0 and read code_info.chunks',
|
|
904
|
+
how: 'call aura_get_component with chunk:0..chunks-1 and concatenate code in order; part files above map 1:1 to chunks',
|
|
905
|
+
} : null;
|
|
906
|
+
const css_scope_how = 'in the pasted <style>, rewrite every bare class selector as .' + scope + ' <selector> (e.g. .button -> .' + scope + ' .button); leave Tailwind utilities, :root vars, and @keyframes names untouched';
|
|
907
|
+
return { kind, title: row.title, page_url: url, free: row.premium === false, steps, deps, files, needs, css_scope: scope, css_scope_how, code_truncated: isTrunc, ...(concat ? { concat } : {}) };
|
|
908
|
+
}
|
|
909
|
+
if (kind === 'skills') {
|
|
910
|
+
steps.push('Read the SKILL.md content from aura_get_skill first: it names its own triggers and pitfalls.');
|
|
911
|
+
steps.push('Save it as SKILL.md inside your agent skills folder so the agent can load it.');
|
|
912
|
+
if (row.source_url) steps.push('Upstream source: ' + row.source_url);
|
|
913
|
+
return { kind, title: row.title, page_url: url, steps, deps, files: [{ path: 'skills/' + row.id + '/SKILL.md', contains: 'full skill content' }] };
|
|
914
|
+
}
|
|
915
|
+
if (kind === 'design_systems') {
|
|
916
|
+
// ISS6: derive preview deps from preview_html so agents don't ship static pages.
|
|
917
|
+
const ph = String(row.preview_html || '');
|
|
918
|
+
const pl = ph.toLowerCase();
|
|
919
|
+
const has = (...ss) => ss.some((s) => pl.includes(s));
|
|
920
|
+
if (has('cdn.tailwindcss.com', 'tailwind')) deps.push({ name: 'tailwindcss', via: 'CDN (in preview_html)', cdn: GUIDE_CDN.tailwind });
|
|
921
|
+
if (has('iconify', '<svg', 'lucide', 'data-lucide')) deps.push({ name: 'iconify-icon', via: 'CDN (in preview_html)', cdn: GUIDE_CDN.iconify });
|
|
922
|
+
if (has('gsap')) deps.push({ name: 'gsap', via: 'CDN (in preview_html)', cdn: 'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js' });
|
|
923
|
+
if (has('scrolltrigger')) deps.push({ name: 'gsap-ScrollTrigger', via: 'CDN (in preview_html)', cdn: 'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js' });
|
|
924
|
+
const fonts = new Set();
|
|
925
|
+
for (const m of pl.matchAll(/family=([a-z+]+)/g)) fonts.add(m[1].replace(/\+/g, '-'));
|
|
926
|
+
if (fonts.size) deps.push({ name: 'google-fonts:' + [...fonts].slice(0, 4).join(','), via: 'link (in preview_html)' });
|
|
927
|
+
steps.push('Copy the DESIGN.md content into your repo as DESIGN.md and treat it as the source of truth.');
|
|
928
|
+
steps.push('Apply the color, type, and spacing tokens before copying any component markup.');
|
|
929
|
+
if (deps.length) steps.push('Install preview deps first (' + deps.map((d) => d.name).join(', ') + ') — the preview_html needs them.');
|
|
930
|
+
return { kind, title: row.title, page_url: url, steps, deps, files: [{ path: 'DESIGN.md', contains: 'tokens and rules' }] };
|
|
931
|
+
}
|
|
932
|
+
steps.push('Use the image_800w URL for previews and image_original for production.');
|
|
933
|
+
return { kind, title: row.title, page_url: url, steps, deps, files };
|
|
934
|
+
};
|
|
935
|
+
|
|
936
|
+
// ISS5: full token surface — colors + typography + spacing + radius — so scaffolded
|
|
937
|
+
// pages don't lose the system. Also detects preview dark/light mode so agents don't
|
|
938
|
+
// apply light-cream tokens to a dark preview body.
|
|
939
|
+
function previewMode(previewHtml) {
|
|
940
|
+
const h = String(previewHtml || '').toLowerCase();
|
|
941
|
+
if (/bg-black|bg-neutral-950|bg-zinc-950|bg-slate-950|bg-\[#0/.test(h)) return 'dark';
|
|
942
|
+
if (/bg-white|bg-neutral-50|bg-slate-50/.test(h)) return 'light';
|
|
943
|
+
return 'unknown';
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function tokenHints(content, previewHtml) {
|
|
947
|
+
const Q = String.fromCharCode(39);
|
|
948
|
+
const lines = String(content || '').split(Q + 'n' === Q + 'n' ? '\n' : '\n');
|
|
949
|
+
const get = (key) => {
|
|
950
|
+
const line = lines.find((l) => { const t = l.trim().toLowerCase(); return t === key + ':' || t.startsWith(key + ': ') || t.startsWith(key + ' :') || t.startsWith(key + ':\"') || t.startsWith(key + ":'"); });
|
|
951
|
+
if (!line) return null;
|
|
952
|
+
const t = line.trim();
|
|
953
|
+
const ci = t.toLowerCase().indexOf(key);
|
|
954
|
+
let v = t.slice(ci + key.length).trim();
|
|
955
|
+
if (v.startsWith(':')) v = v.slice(1).trim();
|
|
956
|
+
while (v.startsWith(Q) || v.startsWith('"')) v = v.slice(1);
|
|
957
|
+
while (v.endsWith(Q) || v.endsWith('"')) v = v.slice(0, -1);
|
|
958
|
+
return v.trim() || null;
|
|
959
|
+
};
|
|
960
|
+
const tokens = {
|
|
961
|
+
primary: get('primary'),
|
|
962
|
+
secondary: get('secondary'),
|
|
963
|
+
accent: get('accent'),
|
|
964
|
+
background: get('background'),
|
|
965
|
+
surface: get('surface'),
|
|
966
|
+
text_primary: get('text-primary') || get('text_primary'),
|
|
967
|
+
text_secondary: get('text-secondary') || get('text_secondary'),
|
|
968
|
+
border: get('border'),
|
|
969
|
+
display_font: get('display_lg_fontfamily') || get('display-font'),
|
|
970
|
+
body_font: get('body_md_fontfamily') || get('body-font'),
|
|
971
|
+
mono_font: get('label_md_fontfamily') || get('mono-font'),
|
|
972
|
+
section_padding: get('section-padding') || get('section_padding'),
|
|
973
|
+
card_padding: get('card-padding') || get('card_padding'),
|
|
974
|
+
card_radius: get('card') === null ? get('card_radius') : null,
|
|
975
|
+
control_radius: get('control'),
|
|
976
|
+
pill_radius: get('pill'),
|
|
977
|
+
};
|
|
978
|
+
const pick = (obj, keys) => { const o = {}; for (const k of keys) if (obj[k]) o[k] = obj[k]; return o; };
|
|
979
|
+
const colors = pick(tokens, ['primary', 'secondary', 'accent', 'background', 'surface', 'text_primary', 'text_secondary', 'border']);
|
|
980
|
+
const rows = [];
|
|
981
|
+
for (const [k, v] of Object.entries(colors)) rows.push(' --aura-' + k.replace(/_/g, '-') + ': ' + v + ';');
|
|
982
|
+
if (tokens.body_font) rows.push(' --aura-font-body: ' + tokens.body_font + ';');
|
|
983
|
+
if (tokens.display_font) rows.push(' --aura-font-display: ' + tokens.display_font + ';');
|
|
984
|
+
if (tokens.mono_font) rows.push(' --aura-font-mono: ' + tokens.mono_font + ';');
|
|
985
|
+
if (tokens.section_padding) rows.push(' --aura-section-padding: ' + tokens.section_padding + ';');
|
|
986
|
+
if (tokens.card_padding) rows.push(' --aura-card-padding: ' + tokens.card_padding + ';');
|
|
987
|
+
if (tokens.card_radius) rows.push(' --aura-radius-card: ' + tokens.card_radius + ';');
|
|
988
|
+
if (tokens.control_radius) rows.push(' --aura-radius-control: ' + tokens.control_radius + ';');
|
|
989
|
+
if (tokens.pill_radius) rows.push(' --aura-radius-pill: ' + tokens.pill_radius + ';');
|
|
990
|
+
const css = rows.length ? ':root{' + '\n' + rows.join('\n') + '\n}' : '';
|
|
991
|
+
const mode = previewMode(previewHtml);
|
|
992
|
+
const warn = (mode === 'dark' && tokens.background && !/000|0f1115|111827|1a1a1a|09090b/i.test(tokens.background))
|
|
993
|
+
? 'tokens say light background ' + tokens.background + ' but preview_html renders dark — follow the preview body, not the tokens, or ask which mode is wanted'
|
|
994
|
+
: ((mode === 'light' && tokens.background && /000000|0f1115/i.test(tokens.background))
|
|
995
|
+
? 'tokens say dark background but preview renders light — follow the preview body'
|
|
996
|
+
: null);
|
|
997
|
+
return { tokens, css, ...(mode === 'unknown' ? {} : { preview_mode: mode }), mode_warning: warn };
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// Module: prompt — deep module, small interface (buildPrompt) over all
|
|
1001
|
+
// need-based prompt rules. Pure, no fetch: same seam for tool + tests + docs.
|
|
1002
|
+
// Pass 1 (no answers): draft + questions[<=4] + next_call. Agent MUST ask the
|
|
1003
|
+
// user via native question UI before Pass 2. Pass 2 (with answers): final.
|
|
1004
|
+
const PROMPT_SOURCES = [
|
|
1005
|
+
'https://www.aura.build/learn/tips-for-prompting',
|
|
1006
|
+
'https://www.aura.build/learn/introduction',
|
|
1007
|
+
'https://www.aura.build/design-systems',
|
|
1008
|
+
];
|
|
1009
|
+
const PROMPT_CREDIT = 'Built by swastiksingh-dev — catalogue by Aura (aura.build)';
|
|
1010
|
+
const PROMPT_ORDER = ['1 tokens.css', '2 system preview_html', '3 component markup in listed order'];
|
|
1011
|
+
|
|
1012
|
+
function promptThemeHint(goal, explicit) {
|
|
1013
|
+
if (explicit === 'dark' || explicit === 'light') return explicit;
|
|
1014
|
+
const t = String(goal || '').toLowerCase();
|
|
1015
|
+
if (/\bdark\b|\bmidnight\b|\bnoir\b|\bcinematic\b/.test(t) && !/\blight\b|\bbright\b|\bairy\b/.test(t)) return 'dark';
|
|
1016
|
+
if (/\blight\b|\bbright\b|\bairy\b/.test(t)) return 'light';
|
|
1017
|
+
return null;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function draftPromptText(goal, theme) {
|
|
1021
|
+
const th = theme ? theme + ' theme' : 'theme to confirm with user';
|
|
1022
|
+
return 'Build: ' + goal + ' (' + th + '). Use Tailwind CSS with container mx-auto px-4, hex palette, numeric color scale 50-900, 8px radius, 768px hamburger breakpoint, hover scale 1.05 + shadow. Typography: Inter 40-60 / 28-36 / 14-16, weights 640/560/460. Motion <500ms + prefers-reduced-motion. Tokens first (DESIGN.md), then markup. Confirm theme/sections/system with user before scaffolding.';
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function finalPromptText(goal, a) {
|
|
1026
|
+
const theme = a.theme || 'confirm with user';
|
|
1027
|
+
const sections = (a.sections && a.sections.length ? a.sections.join(', ') : 'hero + 2 sections');
|
|
1028
|
+
const system = a.system || 'auto-pick via aura_search_design_systems';
|
|
1029
|
+
const assets = a.assets || 'only if needed via aura_search_assets';
|
|
1030
|
+
return 'GOAL: ' + goal + '\n'
|
|
1031
|
+
+ 'THEME: ' + theme + ' (tokens first, follow preview body on mismatch)\n'
|
|
1032
|
+
+ 'SECTIONS: ' + sections + '\n'
|
|
1033
|
+
+ 'DESIGN SYSTEM: ' + system + ' — copy DESIGN.md as source of truth, apply color/type/spacing before markup\n'
|
|
1034
|
+
+ 'ASSETS: ' + assets + '\n'
|
|
1035
|
+
+ 'RULES: Tailwind container mx-auto px-4; hex palette 50-900; radius 8px; breakpoints 1024+/768-1023/<768, 44px touch targets; hover scale 1.05 + shadow; Inter 40-48 H1 / 16 body; motion <500ms + prefers-reduced-motion; vanilla HTML/Tailwind/JS, no proprietary runtime\n'
|
|
1036
|
+
+ 'ORDER: 1 tokens.css, 2 system preview_html, 3 component markup\n'
|
|
1037
|
+
+ 'CALLS: aura_recommend -> aura_get_* finalists -> aura_use_design_system -> aura_scaffold_page';
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function buildPrompt(input = {}) {
|
|
1041
|
+
const goal = typeof input.goal === 'string' ? input.goal.trim() : '';
|
|
1042
|
+
if (!goal) { const e = new Error('goal (string) is required'); e.code = 'BAD_ARGS'; throw e; }
|
|
1043
|
+
const themeIn = input.theme === 'dark' || input.theme === 'light' ? input.theme : undefined;
|
|
1044
|
+
if (input.theme !== undefined && themeIn === undefined) { const e = new Error('theme must be dark|light'); e.code = 'BAD_ARGS'; throw e; }
|
|
1045
|
+
const theme = promptThemeHint(goal, themeIn);
|
|
1046
|
+
const system = typeof input.system === 'string' && input.system.trim() ? input.system.trim() : null;
|
|
1047
|
+
const surfaces = Array.isArray(input.surfaces) ? input.surfaces.filter((s) => typeof s === 'string').slice(0, 4) : [];
|
|
1048
|
+
const answers = input.answers && typeof input.answers === 'object' && !Array.isArray(input.answers) ? input.answers : null;
|
|
1049
|
+
|
|
1050
|
+
if (!answers) {
|
|
1051
|
+
const questions = [
|
|
1052
|
+
{ key: 'theme', question: 'Which theme — dark or light?', options: ['dark', 'light'] },
|
|
1053
|
+
{ key: 'sections', question: 'Which sections (e.g. hero, pricing, testimonial, footer)?', options: [] },
|
|
1054
|
+
{ key: 'system', question: 'Which DESIGN system, or auto-pick?', options: system ? [system, 'auto-pick'] : ['auto-pick'] },
|
|
1055
|
+
{ key: 'assets', question: 'Need images/video?', options: ['no', 'images', 'video'] },
|
|
1056
|
+
];
|
|
1057
|
+
return {
|
|
1058
|
+
phase: 'draft',
|
|
1059
|
+
goal,
|
|
1060
|
+
theme_hint: theme,
|
|
1061
|
+
draft_prompt: draftPromptText(goal, theme),
|
|
1062
|
+
questions,
|
|
1063
|
+
missing: ['theme', 'sections', 'system', 'assets'],
|
|
1064
|
+
next_call: { tool: 'aura_prompt', arguments: { goal, answers: { theme: theme || 'dark', sections: [], system: system || 'auto-pick', assets: 'no' } } },
|
|
1065
|
+
plan_order: PROMPT_ORDER,
|
|
1066
|
+
sources: PROMPT_SOURCES,
|
|
1067
|
+
credit: PROMPT_CREDIT,
|
|
1068
|
+
note: 'Ask the user these questions via native question UI, then call aura_prompt again with goal + answers. Never skip confirmation.',
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
const a = {
|
|
1073
|
+
theme: answers.theme === 'dark' || answers.theme === 'light' ? answers.theme : (theme || 'dark'),
|
|
1074
|
+
sections: Array.isArray(answers.sections) ? answers.sections.filter((s) => typeof s === 'string').slice(0, 6) : [],
|
|
1075
|
+
system: typeof answers.system === 'string' && answers.system.trim() ? answers.system.trim() : (system || 'auto-pick'),
|
|
1076
|
+
assets: typeof answers.assets === 'string' ? answers.assets : 'no',
|
|
1077
|
+
};
|
|
1078
|
+
return {
|
|
1079
|
+
phase: 'final',
|
|
1080
|
+
goal,
|
|
1081
|
+
answers: a,
|
|
1082
|
+
final_prompt: finalPromptText(goal, a),
|
|
1083
|
+
plan_order: PROMPT_ORDER,
|
|
1084
|
+
suggested_calls: [
|
|
1085
|
+
{ tool: 'aura_recommend', arguments: { goal } },
|
|
1086
|
+
{ tool: 'aura_search_all', arguments: { query: goal, limit: 3 } },
|
|
1087
|
+
{ tool: 'aura_use_design_system', arguments: a.system !== 'auto-pick' ? { slug: a.system } : { slug: 'auto-pick: search first' } },
|
|
1088
|
+
{ tool: 'aura_scaffold_page', arguments: { goal, system: a.system } },
|
|
1089
|
+
],
|
|
1090
|
+
page_urls: ['https://www.aura.build/design-systems', 'https://www.aura.build/components'],
|
|
1091
|
+
sources: PROMPT_SOURCES,
|
|
1092
|
+
surfaces: surfaces.length ? surfaces : ['components', 'design_systems', 'assets', 'skills'],
|
|
1093
|
+
credit: PROMPT_CREDIT,
|
|
1094
|
+
};
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1097
|
+
// ---- tools.mjs ----
|
|
867
1098
|
// Module: tools — thin adapters. Each tool validates args at the seam,
|
|
868
|
-
// calls one catalog/guide method, wraps textResult. No retries/caching here
|
|
1099
|
+
// calls one catalog/guide method, wraps textResult. No retries/caching here
|
|
1100
|
+
// (except aura_recommend's single bounded auto-retry + editorial fill).
|
|
869
1101
|
const TOOL_KINDS = ['components', 'skills', 'assets', 'design_systems'];
|
|
870
1102
|
const TOOL_SORTS = ['popular', 'recent', 'trending', 'updated'];
|
|
871
1103
|
const TOOL_CATS = ['hero','section','button','card','background','header','logo','feature','pricing','testimonial','footer','form','heading'];
|
|
@@ -876,28 +1108,48 @@ function toolFn_bool(v, name) { if (v !== undefined && typeof v !== 'boolean') t
|
|
|
876
1108
|
function toolFn_sort(v) { if (v !== undefined && !TOOL_SORTS.includes(v)) toolFn_bad('sort must be one of ' + TOOL_SORTS.join('|')); return v; }
|
|
877
1109
|
function theme(v) { if (v !== undefined && v !== 'dark' && v !== 'light') toolFn_bad('theme must be dark|light'); return v; }
|
|
878
1110
|
function idList(v, name) { if (v === undefined) return v; if (!Array.isArray(v) || !v.length || v.length > 8 || v.some((x) => typeof x !== 'string' && typeof x !== 'number')) toolFn_bad(name + ' must be an array of 1-8 ids'); return v; }
|
|
1111
|
+
function normRefs(v, name) {
|
|
1112
|
+
if (v === undefined) return v;
|
|
1113
|
+
let preview = '';
|
|
1114
|
+
try { preview = JSON.stringify(v).slice(0, 120); } catch { preview = String(v).slice(0, 120); }
|
|
1115
|
+
const ex = name + ' must be an array like ["732","1364"] or [{id:"732"}] — got ' + preview;
|
|
1116
|
+
if (!Array.isArray(v) || !v.length || v.length > 8) toolFn_bad(ex);
|
|
1117
|
+
const out = [];
|
|
1118
|
+
for (const e of v) {
|
|
1119
|
+
if (typeof e === 'string' && e) out.push(e);
|
|
1120
|
+
else if (typeof e === 'number' && Number.isFinite(e)) out.push(String(e));
|
|
1121
|
+
else if (e && typeof e === 'object' && !Array.isArray(e)) {
|
|
1122
|
+
if (typeof e.id === 'string' && e.id) out.push(e.id);
|
|
1123
|
+
else if (typeof e.id === 'number' && Number.isFinite(e.id)) out.push(String(e.id));
|
|
1124
|
+
else if (typeof e.slug === 'string' && e.slug) out.push(e.slug);
|
|
1125
|
+
else toolFn_bad(ex);
|
|
1126
|
+
} else toolFn_bad(ex);
|
|
1127
|
+
}
|
|
1128
|
+
if (!out.length) toolFn_bad(ex);
|
|
1129
|
+
return out;
|
|
1130
|
+
}
|
|
879
1131
|
function toolFn_cat(v) { if (v !== undefined && !TOOL_CATS.includes(v)) toolFn_bad('category must be one of ' + TOOL_CATS.join('|')); return v; }
|
|
880
1132
|
const TOOL_DEFS = [
|
|
881
1133
|
{ name: 'aura_status', description: 'Catalogue health plus free counts (components, skills, assets, design systems). Free only, no login. Start here.', inputSchema: { type: 'object', properties: {}, additionalProperties: false } },
|
|
882
|
-
{ name: 'aura_search_components', description: 'Search free Aura UI components (2,495). Text over title and description, optional category tag, theme dark|light filter, negative_theme exclusion, min_views floor, sorts.
|
|
883
|
-
{ name: 'aura_get_component', description: 'Full free component detail with HTML/Tailwind source, preview image, page URL. Numeric id or slug. Chunk param pages large code (see code_info).', inputSchema: { type: 'object', properties: { id: {}, slug: { type: 'string' }, chunk: { type: 'number' } }, additionalProperties: false } },
|
|
884
|
-
{ name: 'aura_search_skills', description: 'Search free Aura agent skills (187). Metadata only; use aura_get_skill for the full SKILL.md content.', inputSchema: { type: 'object', properties: { query: { type: 'string' }, sort: { type: 'string', enum: TOOL_SORTS }, limit: { type: 'number' }, offset: { type: 'number' } }, additionalProperties: false } },
|
|
1134
|
+
{ name: 'aura_search_components', description: 'Search free Aura UI components (2,495). Text over title and description, optional category tag, theme dark|light filter, negative_theme exclusion, min_views floor, sorts. Slim rows by default (excerpt + fetch pointer, no full code; slim:false for full). exact_match:true skips fallbacks (honest empty).', inputSchema: { type: 'object', properties: { query: { type: 'string' }, category: { type: 'string', enum: TOOL_CATS }, theme: { type: 'string', enum: ['dark', 'light'] }, negative_theme: { type: 'string', enum: ['dark', 'light'] }, min_views: { type: 'number' }, sort: { type: 'string', enum: TOOL_SORTS }, limit: { type: 'number' }, offset: { type: 'number' }, slim: { type: 'boolean' }, exact_match: { type: 'boolean' } }, additionalProperties: false } },
|
|
1135
|
+
{ name: 'aura_get_component', description: 'Full free component detail with HTML/Tailwind source, preview image, page URL. Numeric id or slug. Chunk param pages large code (see code_info). full:true returns full code (ignores chunk).', inputSchema: { type: 'object', properties: { id: {}, slug: { type: 'string' }, chunk: { type: 'number' }, full: { type: 'boolean' } }, additionalProperties: false } },
|
|
1136
|
+
{ name: 'aura_search_skills', description: 'Search free Aura agent skills (187). Metadata only, slim by default; use aura_get_skill for the full SKILL.md content. exact_match:true skips fallbacks.', inputSchema: { type: 'object', properties: { query: { type: 'string' }, sort: { type: 'string', enum: TOOL_SORTS }, limit: { type: 'number' }, offset: { type: 'number' }, slim: { type: 'boolean' }, exact_match: { type: 'boolean' } }, additionalProperties: false } },
|
|
885
1137
|
{ name: 'aura_get_skill', description: 'Full free agent-skill content (SKILL.md body) plus source_url and page URL. Skill id. Chunk param pages large bodies (see content_info).', inputSchema: { type: 'object', properties: { id: { type: 'string' }, chunk: { type: 'number' } }, required: ['id'], additionalProperties: false } },
|
|
886
|
-
{ name: 'aura_search_assets', description: 'Search free Aura assets (images and video). Keywords, media_type image or video.', inputSchema: { type: 'object', properties: { query: { type: 'string' }, mediaType: { type: 'string', enum: ['image', 'video'] }, commercial_ok: { type: 'boolean' }, sort: { type: 'string', enum: TOOL_SORTS }, limit: { type: 'number' }, offset: { type: 'number' } }, additionalProperties: false } },
|
|
887
|
-
{ name: 'aura_search_design_systems', description: 'Search free Aura DESIGN.md systems (725). Metadata
|
|
1138
|
+
{ name: 'aura_search_assets', description: 'Search free Aura assets (images and video). Keywords, media_type image or video. Slim rows by default. exact_match:true skips fallbacks.', inputSchema: { type: 'object', properties: { query: { type: 'string' }, mediaType: { type: 'string', enum: ['image', 'video'] }, commercial_ok: { type: 'boolean' }, sort: { type: 'string', enum: TOOL_SORTS }, limit: { type: 'number' }, offset: { type: 'number' }, slim: { type: 'boolean' }, exact_match: { type: 'boolean' } }, additionalProperties: false } },
|
|
1139
|
+
{ name: 'aura_search_design_systems', description: 'Search free Aura DESIGN.md systems (725). Metadata + snippets, slim by default; use aura_get_design_system for content. exact_match:true skips fallbacks.', inputSchema: { type: 'object', properties: { query: { type: 'string' }, sort: { type: 'string', enum: TOOL_SORTS }, limit: { type: 'number' }, offset: { type: 'number' }, slim: { type: 'boolean' }, exact_match: { type: 'boolean' } }, additionalProperties: false } },
|
|
888
1140
|
{ name: 'aura_get_design_system', description: 'Full free DESIGN.md content plus preview_html, tokens, and page URL. System id or slug. Chunk param pages large preview_html (see preview_info).', inputSchema: { type: 'object', properties: { id: { type: 'string' }, slug: { type: 'string' }, chunk: { type: 'number' } }, additionalProperties: false } },
|
|
889
|
-
{ name: 'aura_search_all', description: 'One call across components, skills, assets, and design systems in parallel. Free only.', inputSchema: { type: 'object', properties: { query: { type: 'string' }, limit: { type: 'number' } }, additionalProperties: false } },
|
|
890
|
-
{ name: 'aura_recommend', description: 'Starter kit for a goal: top free
|
|
1141
|
+
{ name: 'aura_search_all', description: 'One call across components, skills, assets, and design systems in parallel. Free only. Slim rows by default (excerpts, no blobs). Each surface runs its own fallback pipeline, so mixed outcomes are normal (skills may fall back while components go honestly empty) — read isFallback/no_match per surface, not once globally.', inputSchema: { type: 'object', properties: { query: { type: 'string' }, limit: { type: 'number' }, slim: { type: 'boolean' }, theme: { type: 'string', enum: ['dark', 'light'] }, negative_theme: { type: 'string', enum: ['dark', 'light'] }, min_views: { type: 'number' }, exact_match: { type: 'boolean' } }, additionalProperties: false } },
|
|
1142
|
+
{ name: 'aura_recommend', description: 'Starter kit for a goal: top 3 free hits per surface with page URLs and rationale. Slim rows + commercial-use note. Empty components auto-retry once via suggested_queries, then editorial picks.', inputSchema: { type: 'object', properties: { goal: { type: 'string' }, theme: { type: 'string', enum: ['dark', 'light'] }, negative_theme: { type: 'string', enum: ['dark', 'light'] }, min_views: { type: 'number' }, slim: { type: 'boolean' } }, required: ['goal'], additionalProperties: false } },
|
|
891
1143
|
{ name: 'aura_install_component', description: 'Paste-ready setup for a free component: dependency list, setup steps, file map, fonts. Goes beyond the official Aura MCP, which only reads project source.', inputSchema: { type: 'object', properties: { id: {}, slug: { type: 'string' } }, additionalProperties: false } },
|
|
892
1144
|
{ name: 'aura_install_skill', description: 'Save-and-load plan for a free skill: where to put SKILL.md per client plus upstream source.', inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'], additionalProperties: false } },
|
|
893
1145
|
{ name: 'aura_use_design_system', description: 'Apply a free DESIGN.md system: token starter CSS plus copy order (tokens first, then markup).', inputSchema: { type: 'object', properties: { id: { type: 'string' }, slug: { type: 'string' } }, additionalProperties: false } },
|
|
894
|
-
{ name: 'aura_trending', description: 'What is new and popular across the free catalogue: top components, skills, assets, design systems in one call.', inputSchema: { type: 'object', properties: { limit: { type: 'number' } }, additionalProperties: false } },
|
|
895
1146
|
{ name: 'aura_bundle', description: 'Bulk-fetch 2-8 component details in one call (ids or slugs). Per-item errors never fail the batch.', inputSchema: { type: 'object', properties: { ids: { type: 'array', items: {} }, slugs: { type: 'array', items: { type: 'string' } } }, additionalProperties: false } },
|
|
896
|
-
{ name: 'aura_scaffold_page', description: 'One ordered page build: DESIGN.md tokens.css + system preview + component markup in dependency order, combined deps + files[]. Merges install_* + use_* in a single turn.', inputSchema: { type: 'object', properties: { goal: { type: 'string' }, system: { type: 'string' }, components: { type: 'array', items: {} } }, required: ['goal'], additionalProperties: false } },
|
|
1147
|
+
{ name: 'aura_scaffold_page', description: 'One ordered page build: DESIGN.md tokens.css + system preview + component markup in dependency order, combined deps + files[]. Merges install_* + use_* in a single turn. components accepts string|number|{id}|{slug} refs, e.g. ["732","1364"] or [{id:"732"}] or [{slug:"..."}]; full:true returns full bodies.', inputSchema: { type: 'object', properties: { goal: { type: 'string' }, system: { type: 'string' }, components: { type: 'array', items: {} }, full: { type: 'boolean' } }, required: ['goal'], additionalProperties: false } },
|
|
897
1148
|
{ name: 'aura_related', description: 'More-like-this: 3 related items for a component/skill/design-system by tag + text overlap. Discovery never dead-ends.', inputSchema: { type: 'object', properties: { kind: { type: 'string', enum: ['components', 'skills', 'design_systems'] }, id: {} }, required: ['kind', 'id'], additionalProperties: false } },
|
|
898
1149
|
{ name: 'aura_bulk_fetch', description: 'Bulk-fetch 2-8 details in one call for components, design_systems, or assets. Alias-friendly name for aura_bundle. Per-item errors never fail the batch.', inputSchema: { type: 'object', properties: { kind: { type: 'string', enum: ['components', 'design_systems', 'assets'] }, ids: { type: 'array', items: {} } }, required: ['kind', 'ids'], additionalProperties: false } },
|
|
899
1150
|
{ name: 'aura_install_asset', description: 'Legal drop-in plan for an asset: direct download URL, preview URL, license (all-rights-reserved per Aura Terms §4 — check page before commercial use), suggested file path.', inputSchema: { type: 'object', properties: { id: { type: 'number' } }, required: ['id'], additionalProperties: false } },
|
|
900
1151
|
{ name: 'aura_categories', description: 'The 13 component categories with live free counts. Pick one, then search within it.', inputSchema: { type: 'object', properties: {}, additionalProperties: false } },
|
|
1152
|
+
{ name: 'aura_prompt', description: 'Need-based design prompt in 2 passes: Pass 1 (goal only) returns draft + 4 questions + next_call; ask the user via native question UI, then Pass 2 (goal + answers) returns copy-paste final_prompt + plan order + suggested calls. Always confirm with user before scaffolding.', inputSchema: { type: 'object', properties: { goal: { type: 'string' }, theme: { type: 'string', enum: ['dark', 'light'] }, surfaces: { type: 'array', items: { type: 'string' } }, system: { type: 'string' }, answers: { type: 'object' } }, required: ['goal'], additionalProperties: false } },
|
|
901
1153
|
];
|
|
902
1154
|
function createHandlers(catalog) {
|
|
903
1155
|
const free = { freeOnly: true };
|
|
@@ -908,38 +1160,49 @@ function createHandlers(catalog) {
|
|
|
908
1160
|
negative_theme: a.negative_theme === undefined ? undefined : theme(a.negative_theme),
|
|
909
1161
|
min_views: toolFn_num(a.min_views, 'min_views'),
|
|
910
1162
|
sort: toolFn_sort(a.sort), limit: toolFn_num(a.limit, 'limit'), offset: toolFn_num(a.offset, 'offset'),
|
|
1163
|
+
slim: a.slim === undefined ? undefined : toolFn_bool(a.slim, 'slim'),
|
|
1164
|
+
exact_match: a.exact_match === undefined ? undefined : toolFn_bool(a.exact_match, 'exact_match'),
|
|
911
1165
|
})); },
|
|
912
1166
|
aura_get_component: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
|
|
913
1167
|
if (id === undefined || (typeof id !== 'string' && typeof id !== 'number')) toolFn_bad('provide id (number) or slug (string)');
|
|
1168
|
+
const full = a.full === undefined ? undefined : toolFn_bool(a.full, 'full');
|
|
1169
|
+
if (full === true) return textResult(await catalog.getItem('components', id, { full: true }));
|
|
914
1170
|
const chunk = a.chunk === undefined ? 0 : toolFn_num(a.chunk, 'chunk');
|
|
915
1171
|
return textResult(await catalog.getItem('components', id, { chunk })); },
|
|
916
1172
|
aura_search_skills: async (a) => { a = a || {}; return textResult(await catalog.searchCatalog('skills', {
|
|
917
1173
|
query: toolFn_str(a.query, 'query'), sort: toolFn_sort(a.sort), limit: toolFn_num(a.limit, 'limit'), offset: toolFn_num(a.offset, 'offset'),
|
|
1174
|
+
slim: a.slim === undefined ? undefined : toolFn_bool(a.slim, 'slim'),
|
|
1175
|
+
exact_match: a.exact_match === undefined ? undefined : toolFn_bool(a.exact_match, 'exact_match'),
|
|
918
1176
|
})); },
|
|
919
1177
|
aura_get_skill: async (a) => { a = a || {}; if (typeof a.id !== 'string' || !a.id) toolFn_bad('id (string) is required');
|
|
920
1178
|
const chunk = a.chunk === undefined ? 0 : toolFn_num(a.chunk, 'chunk');
|
|
921
1179
|
return textResult(await catalog.getItem('skills', a.id, { chunk })); },
|
|
922
1180
|
aura_search_assets: async (a) => { a = a || {}; const co = a.commercial_ok === undefined ? undefined : toolFn_bool(a.commercial_ok, 'commercial_ok');
|
|
923
|
-
const r = await catalog.searchCatalog('assets', { query: toolFn_str(a.query, 'query'), mediaType: a.mediaType === undefined ? undefined : (a.mediaType === 'image' || a.mediaType === 'video' ? a.mediaType : toolFn_bad('mediaType must be image|video')), freeOnly: true, sort: toolFn_sort(a.sort), limit: toolFn_num(a.limit, 'limit'), offset: toolFn_num(a.offset, 'offset') });
|
|
1181
|
+
const r = await catalog.searchCatalog('assets', { query: toolFn_str(a.query, 'query'), mediaType: a.mediaType === undefined ? undefined : (a.mediaType === 'image' || a.mediaType === 'video' ? a.mediaType : toolFn_bad('mediaType must be image|video')), freeOnly: true, sort: toolFn_sort(a.sort), limit: toolFn_num(a.limit, 'limit'), offset: toolFn_num(a.offset, 'offset'), slim: a.slim === undefined ? undefined : toolFn_bool(a.slim, 'slim'), exact_match: a.exact_match === undefined ? undefined : toolFn_bool(a.exact_match, 'exact_match') });
|
|
924
1182
|
// Gap 5: commercial_ok filter (all-rights-reserved per Aura Terms S4).
|
|
925
1183
|
// commercial_ok:true returns [] with explanation, never silence.
|
|
926
1184
|
if (co === true) return textResult({ ...r, items: [], commercial_ok: false, commercial_note: 'All Aura catalogue assets are all-rights-reserved per Aura Terms S4. Preview/personal use via page_url; commercial reuse needs Aura permission.' });
|
|
927
|
-
if (co === true) return textResult({ ...r, items: [], commercial_ok: false, commercial_note: 'x' });
|
|
928
|
-
if (co === true) return textResult({ ...r, items: [], commercial_ok: false, commercial_note: 'All Aura catalogue assets are all-rights-reserved per Aura Terms S4.' });
|
|
929
1185
|
return textResult(r); },
|
|
930
1186
|
aura_search_design_systems: async (a) => { a = a || {}; return textResult(await catalog.searchCatalog('design_systems', {
|
|
931
1187
|
query: toolFn_str(a.query, 'query'), sort: toolFn_sort(a.sort), limit: toolFn_num(a.limit, 'limit'), offset: toolFn_num(a.offset, 'offset'),
|
|
1188
|
+
slim: a.slim === undefined ? undefined : toolFn_bool(a.slim, 'slim'),
|
|
1189
|
+
exact_match: a.exact_match === undefined ? undefined : toolFn_bool(a.exact_match, 'exact_match'),
|
|
932
1190
|
})); },
|
|
933
1191
|
aura_get_design_system: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
|
|
934
1192
|
if (typeof id !== 'string' || !id) toolFn_bad('provide id or slug (string)');
|
|
935
1193
|
const chunk = a.chunk === undefined ? 0 : toolFn_num(a.chunk, 'chunk');
|
|
936
1194
|
return textResult(await catalog.getItem('design_systems', id, { chunk })); },
|
|
937
1195
|
aura_search_all: async (a) => { a = a || {}; const query = toolFn_str(a.query, 'query'), limit = toolFn_num(a.limit, 'limit') || 5;
|
|
1196
|
+
const slim = a.slim === undefined ? undefined : toolFn_bool(a.slim, 'slim');
|
|
1197
|
+
const themeV = a.theme === undefined ? undefined : theme(a.theme);
|
|
1198
|
+
const negV = a.negative_theme === undefined ? undefined : theme(a.negative_theme);
|
|
1199
|
+
const minV = toolFn_num(a.min_views, 'min_views');
|
|
1200
|
+
const exactV = a.exact_match === undefined ? undefined : toolFn_bool(a.exact_match, 'exact_match');
|
|
938
1201
|
const r = await Promise.all([
|
|
939
|
-
catalog.searchCatalog('components', { query, freeOnly: true, limit }),
|
|
940
|
-
catalog.searchCatalog('skills', { query, limit }),
|
|
941
|
-
catalog.searchCatalog('assets', { query, freeOnly: true, limit }),
|
|
942
|
-
catalog.searchCatalog('design_systems', { query, limit }),
|
|
1202
|
+
catalog.searchCatalog('components', { query, freeOnly: true, limit, slim, theme: themeV, negative_theme: negV, min_views: minV, exact_match: exactV }),
|
|
1203
|
+
catalog.searchCatalog('skills', { query, limit, slim, min_views: minV, exact_match: exactV }),
|
|
1204
|
+
catalog.searchCatalog('assets', { query, freeOnly: true, limit, slim, min_views: minV, exact_match: exactV }),
|
|
1205
|
+
catalog.searchCatalog('design_systems', { query, limit, slim, min_views: minV, exact_match: exactV }),
|
|
943
1206
|
]);
|
|
944
1207
|
return textResult({ query: query || null, components: r[0], skills: r[1], assets: r[2], design_systems: r[3] }); },
|
|
945
1208
|
aura_recommend: async (a) => { a = a || {}; if (typeof a.goal !== 'string' || !a.goal.trim()) toolFn_bad('goal (string) is required');
|
|
@@ -948,16 +1211,44 @@ function createHandlers(catalog) {
|
|
|
948
1211
|
const themeV = a.theme === undefined ? undefined : theme(a.theme);
|
|
949
1212
|
const negative_themeV = a.negative_theme === undefined ? undefined : theme(a.negative_theme);
|
|
950
1213
|
const min_views = toolFn_num(a.min_views, 'min_views');
|
|
1214
|
+
const slim = a.slim === undefined ? undefined : toolFn_bool(a.slim, 'slim');
|
|
951
1215
|
const r = await Promise.all([
|
|
952
|
-
catalog.searchCatalog('components', { query: a.goal, freeOnly: true, limit:
|
|
953
|
-
catalog.searchCatalog('skills', { query: a.goal, limit:
|
|
954
|
-
catalog.searchCatalog('assets', { query: a.goal, freeOnly: true, limit: 3 }),
|
|
955
|
-
catalog.searchCatalog('design_systems', { query: a.goal, limit: 3 }),
|
|
1216
|
+
catalog.searchCatalog('components', { query: a.goal, freeOnly: true, limit: 3, theme: themeV, negative_theme: negative_themeV, min_views, slim }),
|
|
1217
|
+
catalog.searchCatalog('skills', { query: a.goal, limit: 3, slim }),
|
|
1218
|
+
catalog.searchCatalog('assets', { query: a.goal, freeOnly: true, limit: 3, slim }),
|
|
1219
|
+
catalog.searchCatalog('design_systems', { query: a.goal, limit: 3, slim }),
|
|
956
1220
|
]);
|
|
957
|
-
|
|
958
|
-
|
|
1221
|
+
// P0 auto-retry (bounded: max 1 retry + 1 picks call, no loops): an honest-empty
|
|
1222
|
+
// components surface (no_match) retries once via suggested_queries[0] with the
|
|
1223
|
+
// same filters, then falls back to human-grade editorial picks so the kit fills.
|
|
1224
|
+
if (r[0] && r[0].no_match === true && (!Array.isArray(r[0].items) || !r[0].items.length)) {
|
|
1225
|
+
const sq = Array.isArray(r[0].suggested_queries) ? r[0].suggested_queries.filter((q) => typeof q === 'string' && q.trim()) : [];
|
|
1226
|
+
let filled = false;
|
|
1227
|
+
if (sq.length) {
|
|
1228
|
+
try {
|
|
1229
|
+
const retry = await catalog.searchCatalog('components', { query: sq[0], freeOnly: true, limit: 3, theme: themeV, negative_theme: negative_themeV, min_views, slim });
|
|
1230
|
+
if (retry && Array.isArray(retry.items) && retry.items.length) {
|
|
1231
|
+
retry.retried = true;
|
|
1232
|
+
retry.retried_from = sq[0];
|
|
1233
|
+
r[0] = retry;
|
|
1234
|
+
filled = true;
|
|
1235
|
+
}
|
|
1236
|
+
} catch { /* keep the original honest-empty */ }
|
|
1237
|
+
}
|
|
1238
|
+
if (!filled && (!Array.isArray(r[0].items) || !r[0].items.length)) {
|
|
1239
|
+
try {
|
|
1240
|
+
const picks = typeof catalog.editorialPicks === 'function' ? await catalog.editorialPicks(3) : [];
|
|
1241
|
+
if (Array.isArray(picks) && picks.length) {
|
|
1242
|
+
r[0] = { kind: 'components', total: picks.length, items: picks, editorial_fallback: true, editorial_note: 'Goal matched nothing — showing human-grade free picks instead.' };
|
|
1243
|
+
}
|
|
1244
|
+
} catch { /* keep the original honest-empty */ }
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
r[0].items = (r[0].items || []).slice(0, 3);
|
|
1248
|
+
const pick = (x) => ((x && x.items) || []).slice(0, 3).map((i) => ({ title: i.title, page_url: i.page_url }));
|
|
959
1249
|
return textResult({ goal: a.goal, freeOnly: true,
|
|
960
|
-
rationale: 'Top free catalogue hits per surface, ranked by Aura views. Open the page_url previews, then fetch full source or content for finalists.',
|
|
1250
|
+
rationale: 'Top 3 free catalogue hits per surface, ranked by Aura views. Open the page_url previews, then fetch full source or content for finalists.',
|
|
1251
|
+
commercial_note: 'Assets (and component art) are all-rights-reserved per Aura Terms §4: fine to prototype, get Aura permission before commercial ship.',
|
|
961
1252
|
components: r[0], skills: r[1], assets: r[2], design_systems: r[3],
|
|
962
1253
|
starter_kit: { components: pick(r[0]), skills: pick(r[1]), assets: pick(r[2]), design_systems: pick(r[3]) } }); },
|
|
963
1254
|
aura_install_component: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
|
|
@@ -972,20 +1263,11 @@ function createHandlers(catalog) {
|
|
|
972
1263
|
if (typeof id !== 'string' || !id) toolFn_bad('provide id or slug (string)');
|
|
973
1264
|
const got = await catalog.getItem('design_systems', id);
|
|
974
1265
|
return textResult({ item: got.item, install: installGuide('design_systems', got.item), tokens: tokenHints(got.item.content || '', got.item.preview_html || '') }); },
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
catalog.searchCatalog('components', { freeOnly: true, sort: 'trending', limit, window_days: windowDays }),
|
|
981
|
-
catalog.searchCatalog('skills', { sort: 'trending', limit }),
|
|
982
|
-
catalog.searchCatalog('assets', { freeOnly: true, sort: 'trending', limit }),
|
|
983
|
-
catalog.searchCatalog('design_systems', { sort: 'trending', limit }),
|
|
984
|
-
]);
|
|
985
|
-
const picks = await catalog.editorialPicks(Math.min(limit, 5)).catch(() => []);
|
|
986
|
-
return textResult({ window: 'last ' + windowDays + ' days by views', window_note: 'Components at 90d/views are low-signal (total 2, views 0-5): see editorial_picks for human-grade component leads; skills/assets trending is organic.', editorial_picks: picks, components: r[0], skills: r[1], assets: r[2], design_systems: r[3] });},
|
|
987
|
-
aura_categories: async () => textResult({ categories: await catalog.categoryCounts() }),
|
|
988
|
-
aura_bundle: async (a) => { a = a || {}; const ids = idList(a.ids, 'ids') || idList(a.slugs, 'slugs');
|
|
1266
|
+
aura_categories: async () => { const cats = await catalog.categoryCounts();
|
|
1267
|
+
const s = (cats || []).reduce((n, c) => n + (typeof c.free === 'number' ? c.free : 0), 0);
|
|
1268
|
+
return textResult({ categories: cats, overlap_note: 'Counts overlap: components carry multiple tags, so per-category free counts sum to ' + s + ' (> 2495 free total). They triage depth, not a partition.' }); },
|
|
1269
|
+
aura_prompt: async (a) => { a = a || {}; return textResult(buildPrompt({ goal: typeof a.goal === 'string' ? a.goal : undefined, theme: a.theme, surfaces: a.surfaces, system: a.system, answers: a.answers })); },
|
|
1270
|
+
aura_bundle: async (a) => { a = a || {}; const ids = normRefs(a.ids, 'ids') || normRefs(a.slugs, 'slugs');
|
|
989
1271
|
if (!ids || !ids.length) toolFn_bad('provide ids (array of 1-8 numbers/strings) or slugs (array of strings)');
|
|
990
1272
|
return textResult({ kind: 'components', results: await catalog.bundleItems('components', ids) }); },
|
|
991
1273
|
aura_related: async (a) => { a = a || {}; const k = a.kind;
|
|
@@ -998,15 +1280,17 @@ function createHandlers(catalog) {
|
|
|
998
1280
|
return textResult({ item: it, install: { kind: 'assets', title: it.title, page_url: it.page_url, license: fx.license || 'all-rights-reserved (Aura Terms §4)', terms_url: 'https://www.aura.build/terms', download: fx.download || null, preview: fx.preview || null, files: [{ path: 'assets/' + it.id + '-' + String(it.title || 'asset').toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 40) + '.jpg', contains: 'downloaded original' }], steps: ['Check the license on the Aura asset page before commercial use — this server reports unknown, never assumes free-to-sell.', 'Download the download URL into the suggested path.', 'Use the preview URL for <img> srcset while drafting.'] } }); },
|
|
999
1281
|
aura_bulk_fetch: async (a) => { a = a || {}; const kind = a.kind;
|
|
1000
1282
|
if (kind !== 'components' && kind !== 'design_systems' && kind !== 'assets') toolFn_bad('kind must be components|design_systems|assets');
|
|
1001
|
-
const ids =
|
|
1283
|
+
const ids = normRefs(a.ids, 'ids');
|
|
1002
1284
|
if (!ids || !ids.length) toolFn_bad('provide ids (array of 1-8)');
|
|
1003
1285
|
return textResult({ kind, results: await catalog.bundleItems(kind, ids) }); },
|
|
1004
1286
|
aura_scaffold_page: async (a) => { a = a || {}; if (typeof a.goal !== 'string' || !a.goal.trim()) toolFn_bad('goal (string) is required');
|
|
1005
1287
|
const sysRef = (typeof a.system === 'string' && a.system) ? a.system : null;
|
|
1006
|
-
const compRefs =
|
|
1288
|
+
const compRefs = normRefs(a.components, 'components') || [];
|
|
1289
|
+
const full = a.full === undefined ? undefined : toolFn_bool(a.full, 'full');
|
|
1290
|
+
const fullOpt = full === undefined ? {} : { full };
|
|
1007
1291
|
const [sys, comps] = await Promise.all([
|
|
1008
|
-
sysRef ? catalog.getItem('design_systems', sysRef).catch(() => null) : catalog.searchCatalog('design_systems', { query: a.goal, limit: 1 }).then((r) => (r.items[0] ? { item: r.items[0] } : null)),
|
|
1009
|
-
compRefs.length ? catalog.bundleItems('components', compRefs).then((rs) => rs.filter((x) => x.ok).map((x) => x.item)) : catalog.searchCatalog('components', { query: a.goal, freeOnly: true, limit: 3 }).then((r) => r.items),
|
|
1292
|
+
sysRef ? catalog.getItem('design_systems', sysRef, fullOpt).catch(() => null) : catalog.searchCatalog('design_systems', { query: a.goal, limit: 1 }).then((r) => (r.items[0] ? { item: r.items[0] } : null)),
|
|
1293
|
+
compRefs.length ? catalog.bundleItems('components', compRefs, 5, fullOpt).then((rs) => rs.filter((x) => x.ok).map((x) => x.item)) : catalog.searchCatalog('components', { query: a.goal, freeOnly: true, limit: 3 }).then((r) => r.items),
|
|
1010
1294
|
]);
|
|
1011
1295
|
const sysItem = sys && sys.item ? sys.item : null;
|
|
1012
1296
|
const toks = sysItem ? tokenHints(sysItem.content || '') : { tokens: {}, css: '' };
|
|
@@ -1016,75 +1300,75 @@ function createHandlers(catalog) {
|
|
|
1016
1300
|
const files = [{ path: 'styles/tokens.css', contains: 'tokens.css from use_design_system' }];
|
|
1017
1301
|
if (sysItem) files.push({ path: 'DESIGN.md', contains: 'system rules' });
|
|
1018
1302
|
for (const g of guides) for (const f of (g.files || [])) files.push(f);
|
|
1019
|
-
return textResult({ goal: a.goal, order: ['1 tokens.css', '2 system preview_html', '3 component markup in listed order'], tokens_css: toks.css || '', system: sysItem, components: comps || [], guides, combined_deps: deps, files }); },
|
|
1303
|
+
return textResult({ goal: a.goal, order: ['1 tokens.css', '2 system preview_html', '3 component markup in listed order'], tokens_css: toks.css || '', system: sysItem, components: comps || [], guides, combined_deps: deps, files, license_note: 'Markup and any pulled assets are all-rights-reserved per Aura Terms §4 — fine to prototype, check each page_url before commercial ship.' }); },
|
|
1020
1304
|
};
|
|
1021
1305
|
};
|
|
1022
|
-
|
|
1023
|
-
// ---- server.mjs (main) ----
|
|
1024
|
-
// Module: server — composition root. Wires config -> fetcher -> catalog ->
|
|
1025
|
-
// tools -> protocol session over stdio. Only framing + wiring lives here.
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
const config = loadConfig();
|
|
1034
|
-
const fetcher = createFetcher({ fetchImpl: globalThis.fetch, timeoutMs: config.timeoutMs, retries: config.retries, userAgent: config.userAgent });
|
|
1035
|
-
const catalog = createCatalog({ fetcher, config, facetsFn: facets });
|
|
1036
|
-
const handlers = createHandlers(catalog);
|
|
1037
|
-
|
|
1038
|
-
let buffer = "";
|
|
1039
|
-
const send = (msg) => process.stdout.write(JSON.stringify(msg) + "\n");
|
|
1040
|
-
const { dispatch } = createSession(send, { tools: TOOL_DEFS, handlers });
|
|
1041
|
-
|
|
1042
|
-
process.stdin.setEncoding("utf8");
|
|
1043
|
-
process.stdin.on("data", (chunk) => {
|
|
1044
|
-
buffer += chunk;
|
|
1045
|
-
let i;
|
|
1046
|
-
while ((i = buffer.indexOf("\n")) >= 0) {
|
|
1047
|
-
const line = buffer.slice(0, i).trim();
|
|
1048
|
-
buffer = buffer.slice(i + 1);
|
|
1049
|
-
if (!line) continue;
|
|
1050
|
-
let msg;
|
|
1051
|
-
try { msg = JSON.parse(line); }
|
|
1052
|
-
catch { send({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } }); continue; }
|
|
1053
|
-
dispatch(msg);
|
|
1054
|
-
}
|
|
1055
|
-
});
|
|
1056
|
-
process.stdin.on("end", () => process.exit(0));
|
|
1057
|
-
process.stdin.resume();
|
|
1058
|
-
|
|
1059
|
-
// Startup cinematics live on stderr only: stdout is reserved for JSON-RPC.
|
|
1060
|
-
// Sequence: aura moment -> catalogue credit -> builder credit -> running.
|
|
1061
|
-
const BANNER = [
|
|
1062
|
-
" <<< aura-components-mcp >>>",
|
|
1063
|
-
" free Aura catalogue, every AI agent",
|
|
1064
|
-
];
|
|
1065
|
-
const sleepMs = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1066
|
-
const paint = (s) => process.stderr.write(s + "\n");
|
|
1067
|
-
async function typeLine(text, cps = 90) {
|
|
1068
|
-
for (const ch of text) { process.stderr.write(ch); await sleepMs(1000 / cps); }
|
|
1069
|
-
process.stderr.write("\n");
|
|
1070
|
-
}
|
|
1071
|
-
async function dots(label, n = 3) {
|
|
1072
|
-
process.stderr.write(label);
|
|
1073
|
-
for (let i = 0; i < n; i++) { await sleepMs(160); process.stderr.write("."); }
|
|
1074
|
-
process.stderr.write("\n");
|
|
1075
|
-
}
|
|
1076
|
-
async function boot() {
|
|
1077
|
-
if (process.env.AURA_QUIET === "1" || process.argv.includes("--quiet")) {
|
|
1078
|
-
paint(SERVER_INFO.name + " v" + SERVER_INFO.version + " listening on stdio");
|
|
1079
|
-
return;
|
|
1080
|
-
}
|
|
1081
|
-
for (const l of BANNER) paint(l);
|
|
1082
|
-
await typeLine("starting your aura for designing...");
|
|
1083
|
-
await dots("warming catalogue cache");
|
|
1084
|
-
paint("catalogue credit: Aura (aura.build) — components, skills, assets, systems");
|
|
1085
|
-
await sleepMs(180);
|
|
1086
|
-
paint("built by swastiksingh-dev — free to use, MIT, no paywall");
|
|
1087
|
-
await sleepMs(180);
|
|
1088
|
-
paint(SERVER_INFO.name + " v" + SERVER_INFO.version + " running —
|
|
1089
|
-
}
|
|
1090
|
-
boot();
|
|
1306
|
+
|
|
1307
|
+
// ---- server.mjs (main) ----
|
|
1308
|
+
// Module: server — composition root. Wires config -> fetcher -> catalog ->
|
|
1309
|
+
// tools -> protocol session over stdio. Only framing + wiring lives here.
|
|
1310
|
+
|
|
1311
|
+
|
|
1312
|
+
|
|
1313
|
+
|
|
1314
|
+
|
|
1315
|
+
|
|
1316
|
+
|
|
1317
|
+
const config = loadConfig();
|
|
1318
|
+
const fetcher = createFetcher({ fetchImpl: globalThis.fetch, timeoutMs: config.timeoutMs, retries: config.retries, userAgent: config.userAgent });
|
|
1319
|
+
const catalog = createCatalog({ fetcher, config, facetsFn: facets });
|
|
1320
|
+
const handlers = createHandlers(catalog);
|
|
1321
|
+
|
|
1322
|
+
let buffer = "";
|
|
1323
|
+
const send = (msg) => process.stdout.write(JSON.stringify(msg) + "\n");
|
|
1324
|
+
const { dispatch } = createSession(send, { tools: TOOL_DEFS, handlers });
|
|
1325
|
+
|
|
1326
|
+
process.stdin.setEncoding("utf8");
|
|
1327
|
+
process.stdin.on("data", (chunk) => {
|
|
1328
|
+
buffer += chunk;
|
|
1329
|
+
let i;
|
|
1330
|
+
while ((i = buffer.indexOf("\n")) >= 0) {
|
|
1331
|
+
const line = buffer.slice(0, i).trim();
|
|
1332
|
+
buffer = buffer.slice(i + 1);
|
|
1333
|
+
if (!line) continue;
|
|
1334
|
+
let msg;
|
|
1335
|
+
try { msg = JSON.parse(line); }
|
|
1336
|
+
catch { send({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } }); continue; }
|
|
1337
|
+
dispatch(msg);
|
|
1338
|
+
}
|
|
1339
|
+
});
|
|
1340
|
+
process.stdin.on("end", () => process.exit(0));
|
|
1341
|
+
process.stdin.resume();
|
|
1342
|
+
|
|
1343
|
+
// Startup cinematics live on stderr only: stdout is reserved for JSON-RPC.
|
|
1344
|
+
// Sequence: aura moment -> catalogue credit -> builder credit -> running.
|
|
1345
|
+
const BANNER = [
|
|
1346
|
+
" <<< aura-components-mcp >>>",
|
|
1347
|
+
" free Aura catalogue, every AI agent",
|
|
1348
|
+
];
|
|
1349
|
+
const sleepMs = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1350
|
+
const paint = (s) => process.stderr.write(s + "\n");
|
|
1351
|
+
async function typeLine(text, cps = 90) {
|
|
1352
|
+
for (const ch of text) { process.stderr.write(ch); await sleepMs(1000 / cps); }
|
|
1353
|
+
process.stderr.write("\n");
|
|
1354
|
+
}
|
|
1355
|
+
async function dots(label, n = 3) {
|
|
1356
|
+
process.stderr.write(label);
|
|
1357
|
+
for (let i = 0; i < n; i++) { await sleepMs(160); process.stderr.write("."); }
|
|
1358
|
+
process.stderr.write("\n");
|
|
1359
|
+
}
|
|
1360
|
+
async function boot() {
|
|
1361
|
+
if (process.env.AURA_QUIET === "1" || process.argv.includes("--quiet")) {
|
|
1362
|
+
paint(SERVER_INFO.name + " v" + SERVER_INFO.version + " listening on stdio");
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
for (const l of BANNER) paint(l);
|
|
1366
|
+
await typeLine("starting your aura for designing...");
|
|
1367
|
+
await dots("warming catalogue cache");
|
|
1368
|
+
paint("catalogue credit: Aura (aura.build) — components, skills, assets, systems");
|
|
1369
|
+
await sleepMs(180);
|
|
1370
|
+
paint("built by swastiksingh-dev — free to use, MIT, no paywall");
|
|
1371
|
+
await sleepMs(180);
|
|
1372
|
+
paint(SERVER_INFO.name + " v" + SERVER_INFO.version + " running — 20 tools on stdio");
|
|
1373
|
+
}
|
|
1374
|
+
boot();
|