zugzbot 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,17 +20,27 @@ import path from "path"
20
20
  // TS-CSS / TS-TW) = 536 items. Fuente: /r/registry.json
21
21
  // oficial shadcn-compatible. Default variant para el stack
22
22
  // Next.js 16 + Tailwind v4 es TS-TW. TTL 1d.
23
+ // - blocks-so: https://blocks.so (ephraimduncan/blocks)
24
+ // 60+ bloques Radix prod-ready organizados en 11 categorias
25
+ // (stats, login, form-layout, file-upload, tables, dialogs,
26
+ // command-menu, sidebar, onboarding, ai, grid-list). Cada
27
+ // bloque incluye `categories[]` explicito en el JSON.
28
+ // Fuente: GitHub Contents API sobre /content/components.
29
+ // Default para forms/stats/tables/file-upload/dialogs/login/
30
+ // command-menu/onboarding/ai/grid-list. TTL 7d.
23
31
  //
24
32
  // Cache local:
25
33
  // - .openspec/cache/shadcn-index.json
26
34
  // - .openspec/cache/basecn-index.json
27
35
  // - .openspec/cache/reactbits-index.json
36
+ // - .openspec/cache/blocks-so-index.json
28
37
  // - .openspec/cache/blocks/<registry>/[<author>/]<name>.json
29
38
  //
30
39
  // Registries configurables via env:
31
40
  // - SDD_CATALOG_TTL_DAYS_AKASH (default 7)
32
41
  // - SDD_CATALOG_TTL_DAYS_BASECN (default 7)
33
42
  // - SDD_CATALOG_TTL_DAYS_REACTBITS (default 1)
43
+ // - SDD_CATALOG_TTL_DAYS_BLOCKSSO (default 7)
34
44
  //
35
45
  // Tools exportadas (prefijo opencode = "sdd_catalog_<export>"):
36
46
  // - list_blocks Lista componentes/bloques por registry, categoria, query.
@@ -42,6 +52,8 @@ import path from "path"
42
52
  // - reactbits: "Dither" (base, resuelve a Dither-TS-TW por default)
43
53
  // "Dither-JS-CSS" (variante canonica completa)
44
54
  // "https://reactbits.dev/r/Dither-TS-TW.json" (URL directa)
55
+ // - blocks-so: "stats-01", "login-03", "form-layout-02", etc.
56
+ // "https://blocks.so/r/stats-01.json" (URL directa)
45
57
  // ============================================================================
46
58
 
47
59
  // ---------------------------------------------------------------------------
@@ -135,6 +147,36 @@ const REACTBITS_CATEGORY_BY_BASE: Map<string, string> = (() => {
135
147
  return map
136
148
  })()
137
149
 
150
+ // ---------------------------------------------------------------------------
151
+ // blocks.so (ephraimduncan/blocks)
152
+ // ---------------------------------------------------------------------------
153
+ // Repo: ephraimduncan/blocks
154
+ // Public registry URL: https://blocks.so/r/<name>.json
155
+ // Categorias oficiales (11): ai, command-menu, dialogs, file-upload,
156
+ // form-layout, grid-list, login, onboarding, sidebar, stats, tables.
157
+ // Fuente de descubrimiento: GitHub Contents API sobre /content/components.
158
+ // Cada bloque es un .tsx bajo content/components/<category>/; el nombre
159
+ // canonico es `<category>-<NN>` (ej: stats-01, login-03, form-layout-02).
160
+ // Cada JSON de bloque incluye `categories[]` explicito y `target` para
161
+ // inyeccion Zero-Touch en el proyecto Next.js destino.
162
+ const BLOCKSSO_REPO = "ephraimduncan/blocks"
163
+ const BLOCKSSO_CATEGORIES_API =
164
+ `https://api.github.com/repos/${BLOCKSSO_REPO}/contents/content/components`
165
+ const BLOCKSSO_CATEGORY_FILES_API = (category: string) =>
166
+ `https://api.github.com/repos/${BLOCKSSO_REPO}/contents/content/components/${category}`
167
+ const BLOCKSSO_RAW = (name: string) =>
168
+ `https://raw.githubusercontent.com/${BLOCKSSO_REPO}/main/content/components/${name}.json`
169
+ const BLOCKSSO_INSTALL = (name: string) => `https://blocks.so/r/${name}.json`
170
+ const BLOCKSSO_PREVIEW = (category: string, name: string) =>
171
+ `https://blocks.so/${category}/${name}`
172
+ const BLOCKSSO_TTL_DAYS = Number(process.env.SDD_CATALOG_TTL_DAYS_BLOCKSSO || 7)
173
+
174
+ // Categorias oficiales de blocks.so (orden estable para la UI).
175
+ const BLOCKSSO_CATEGORIES = [
176
+ "ai", "command-menu", "dialogs", "file-upload", "form-layout",
177
+ "grid-list", "login", "onboarding", "sidebar", "stats", "tables"
178
+ ] as const
179
+
138
180
  // ---------------------------------------------------------------------------
139
181
  // Categorias (heuristica por nombre de archivo para Akash/Basecn)
140
182
  // ---------------------------------------------------------------------------
@@ -156,7 +198,7 @@ const CATEGORY_PREFIXES = [
156
198
  // Helpers
157
199
  // ============================================================================
158
200
 
159
- type RegistryName = "shadcn" | "basecn" | "reactbits"
201
+ type RegistryName = "shadcn" | "basecn" | "reactbits" | "blocks-so"
160
202
 
161
203
  const getRoot = (context: any): string => {
162
204
  if (context?.directory && context.directory !== "/") return context.directory
@@ -505,6 +547,208 @@ const resolveReactbitsSlug = (
505
547
  }
506
548
  }
507
549
 
550
+ // ---------------------------------------------------------------------------
551
+ // blocks.so index builder
552
+ // ---------------------------------------------------------------------------
553
+ //
554
+ // Estrategia de descubrimiento en 2 niveles (idempotente, TTL 7d):
555
+ // 1. Listar subdirectorios bajo /content/components/ (las 11 categorias).
556
+ // 2. Para cada categoria, listar archivos .tsx y derivar name canonico.
557
+ //
558
+ // Output: IndexEntry[] con category real (de la carpeta GitHub), install_url
559
+ // apuntando a https://blocks.so/r/<name>.json y preview_url al demo vivo.
560
+ //
561
+ // Para no saturar la GitHub API (rate limit 60/h sin token), cacheamos el
562
+ // indice completo durante 7d. Cualquier update en el repo se reflejara
563
+ // solo tras `force_refresh: true` o expiracion del TTL.
564
+
565
+ type GitHubContentEntry = {
566
+ name: string
567
+ path: string
568
+ size: number
569
+ sha: string
570
+ type: "dir" | "file"
571
+ }
572
+
573
+ const fetchGithubContents = async (
574
+ apiUrl: string,
575
+ repoLabel: string
576
+ ): Promise<GitHubContentEntry[]> => {
577
+ const res = await fetch(apiUrl, {
578
+ headers: {
579
+ "User-Agent": "zugzbot-sdd-catalog/1.0",
580
+ Accept: "application/vnd.github+json"
581
+ }
582
+ })
583
+ if (!res.ok) {
584
+ if (res.status === 403) {
585
+ throw new Error(
586
+ `GitHub API rate limit hit consultando ${repoLabel}. ` +
587
+ `Espera 1h o usa force_refresh=false con cache existente.`
588
+ )
589
+ }
590
+ if (res.status === 404) {
591
+ throw new Error(
592
+ `Repositorio ${repoLabel} no encontrado o ruta cambiada. ` +
593
+ `Verifica que ${apiUrl} siga vigente.`
594
+ )
595
+ }
596
+ throw new Error(`GitHub API ${res.status} ${res.statusText} consultando ${repoLabel}`)
597
+ }
598
+ return (await res.json()) as GitHubContentEntry[]
599
+ }
600
+
601
+ const buildBlocksSoIndex = async (
602
+ root: string,
603
+ forceRefresh: boolean
604
+ ): Promise<{ index: Index; refreshed: boolean; error?: string }> => {
605
+ const indexPath = getIndexPath(root, "blocks-so")
606
+ const ttl = ttlMs(BLOCKSSO_TTL_DAYS)
607
+
608
+ const cached = readJson<Index>(indexPath)
609
+ if (!forceRefresh && !isCacheStale(cached, ttl)) {
610
+ return { index: cached!, refreshed: false }
611
+ }
612
+
613
+ try {
614
+ const entries: IndexEntry[] = []
615
+
616
+ // Paso 1: listar las 11 categorias oficiales.
617
+ // Si la API falla 404 (ej: rama renombrada) usamos la lista hardcodeada
618
+ // como fallback para no romper el resto del catalogo.
619
+ let categories = [...BLOCKSSO_CATEGORIES] as readonly string[]
620
+ try {
621
+ const catEntries = await fetchGithubContents(
622
+ BLOCKSSO_CATEGORIES_API,
623
+ BLOCKSSO_REPO
624
+ )
625
+ const live = catEntries
626
+ .filter((e) => e.type === "dir")
627
+ .map((e) => e.name)
628
+ .filter((n) => !n.startsWith("_") && !n.startsWith("."))
629
+ if (live.length > 0) categories = live
630
+ } catch (e) {
631
+ if (!cached) throw e
632
+ }
633
+
634
+ // Paso 2: por cada categoria, enumerar .tsx (formato plano) O
635
+ // subdirectorios (formato multi-file, usado por sidebar) y derivar
636
+ // el name canonico. Estructuras mixtas soportadas:
637
+ // - ai/ ai-01.tsx ... ai-05.tsx
638
+ // - command-menu/ command-menu-01.tsx ... -03.tsx
639
+ // - dialogs/ dialog-01.tsx ... -12.tsx (prefijo singular)
640
+ // - login/ login-01.tsx ... -09.tsx
641
+ // - onboarding/ onboarding-01.tsx ... -07.tsx
642
+ // - stats/ stats-01.tsx ... -15.tsx
643
+ // - tables/ table-01.tsx ... -05.tsx (prefijo singular)
644
+ // - form-layout/ form-layout-01.tsx ... -05.tsx
645
+ // - grid-list/ grid-list-01.tsx ... -03.tsx
646
+ // - file-upload/ file-upload-01.tsx ... -06.tsx
647
+ // (+ file-upload-01/ subdir durante transicion)
648
+ // - sidebar/ sidebar-01/ ... sidebar-06/ (SUBDIRS multi-file)
649
+ //
650
+ // Si un mismo name aparece como .tsx Y como subdir (transicion en el
651
+ // repo fuente), preferimos el subdir (multi-file) y descartamos el .tsx
652
+ // huerfano. Esto evita entradas duplicadas en el indice.
653
+ const seenNames = new Set<string>()
654
+ const dedupEntries: IndexEntry[] = []
655
+
656
+ for (const cat of categories) {
657
+ let items: GitHubContentEntry[]
658
+ try {
659
+ items = await fetchGithubContents(
660
+ BLOCKSSO_CATEGORY_FILES_API(cat),
661
+ BLOCKSSO_REPO
662
+ )
663
+ } catch (e) {
664
+ // Si falla una categoria puntual, seguimos con las demas para no
665
+ // abortar el indice completo. Logueamos via error[] abajo.
666
+ continue
667
+ }
668
+ for (const it of items) {
669
+ // Ignorar archivos no-componente (index.ts, .DS_Store, etc.)
670
+ if (it.name.startsWith("index.")) continue
671
+ if (it.name.startsWith("_")) continue
672
+ if (it.name.startsWith(".")) continue
673
+
674
+ // Caso A: archivo .tsx plano. name = file sin extension.
675
+ if (it.type === "file" && it.name.endsWith(".tsx")) {
676
+ const name = it.name.replace(/\.tsx$/, "")
677
+ if (!name) continue
678
+ if (seenNames.has(name)) continue // dedup
679
+ seenNames.add(name)
680
+ dedupEntries.push({
681
+ name,
682
+ filename: it.name,
683
+ category: cat,
684
+ registry: "blocks-so",
685
+ author: "ephraimduncan",
686
+ description: `blocks.so ${cat} block (${name})`,
687
+ tags: [cat, "shadcn-registry", "blocks-so"],
688
+ install_url: BLOCKSSO_INSTALL(name),
689
+ preview_url: BLOCKSSO_PREVIEW(cat, name),
690
+ source: "github"
691
+ })
692
+ continue
693
+ }
694
+
695
+ // Caso B: subdirectorio (multi-file block, ej. sidebar-XX/).
696
+ // El nombre del subdir ES el canonical name del bloque.
697
+ // Sobrescribe cualquier .tsx huerfano con el mismo name.
698
+ if (it.type === "dir") {
699
+ const name = it.name
700
+ if (!name) continue
701
+ // Si ya vimos un .tsx con este name, lo removemos para
702
+ // reemplazarlo por la version multi-file (mas completa).
703
+ const existingIdx = dedupEntries.findIndex(
704
+ (e) => e.name === name && e.category === cat
705
+ )
706
+ if (existingIdx >= 0) dedupEntries.splice(existingIdx, 1)
707
+ seenNames.add(name)
708
+ dedupEntries.push({
709
+ name,
710
+ filename: `${name}/`,
711
+ category: cat,
712
+ registry: "blocks-so",
713
+ author: "ephraimduncan",
714
+ description: `blocks.so ${cat} block (${name}, multi-file)`,
715
+ tags: [cat, "shadcn-registry", "blocks-so", "multi-file"],
716
+ install_url: BLOCKSSO_INSTALL(name),
717
+ preview_url: BLOCKSSO_PREVIEW(cat, name),
718
+ source: "github"
719
+ })
720
+ continue
721
+ }
722
+ }
723
+ }
724
+
725
+ dedupEntries.sort((a, b) => {
726
+ if (a.category !== b.category) return a.category.localeCompare(b.category)
727
+ return a.name.localeCompare(b.name)
728
+ })
729
+ entries.push(...dedupEntries)
730
+
731
+ const index: Index = {
732
+ cached_at: new Date().toISOString(),
733
+ ttl_days: BLOCKSSO_TTL_DAYS,
734
+ source: `${BLOCKSSO_REPO} (${BLOCKSSO_CATEGORIES_API})`,
735
+ total: entries.length,
736
+ entries
737
+ }
738
+ writeJson(indexPath, index)
739
+ return { index, refreshed: true }
740
+ } catch (e) {
741
+ if (cached) {
742
+ return {
743
+ index: cached,
744
+ refreshed: false,
745
+ error: `Refresh fallo (${(e as Error).message}); usando cache de ${cached.cached_at}`
746
+ }
747
+ }
748
+ throw e
749
+ }
750
+ }
751
+
508
752
  // ---------------------------------------------------------------------------
509
753
  // Fetchers compartidos
510
754
  // ---------------------------------------------------------------------------
@@ -550,6 +794,8 @@ const normalizeBlock = (parsed: any, registry: RegistryName, name: string) => {
550
794
  installUrl = REACTBITS_INSTALL(name)
551
795
  } else if (registry === "basecn") {
552
796
  installUrl = BASECN_INSTALL(name)
797
+ } else if (registry === "blocks-so") {
798
+ installUrl = BLOCKSSO_INSTALL(name)
553
799
  } else {
554
800
  installUrl = AKASH_INSTALL(name)
555
801
  }
@@ -591,16 +837,22 @@ type ListResult = {
591
837
  hints?: string[]
592
838
  }
593
839
 
594
- const REGISTRIES_ALL: RegistryName[] = ["shadcn", "basecn", "reactbits"]
840
+ const REGISTRIES_ALL: RegistryName[] = ["shadcn", "basecn", "reactbits", "blocks-so"]
841
+
842
+
843
+
595
844
 
596
845
  export const list_blocks = tool({
597
846
  description:
598
847
  "Lista bloques/componentes del catalogo unificado sdd_catalog. " +
599
- "Soporta 3 registries con catalogo JSON oficial: 'shadcn' (akash3444/shadcn-ui-blocks, " +
600
- "Radix), 'basecn' (akash3444/basecn, fork Base UI) y 'reactbits' (reactbits.dev, " +
601
- "primitivas animadas; para cada base se exponen sus 4 variantes JS-CSS/JS-TW/TS-CSS/TS-TW). " +
602
- "Cachea en .openspec/cache/ con TTL por registry (7d shadcn/basecn, 1d reactbits). " +
603
- "Filtra por categoria (hero, footer, pricing, backgrounds, text, cards, etc) " +
848
+ "Soporta 4 registries con catalogo JSON oficial: 'shadcn' (akash3444/shadcn-ui-blocks, " +
849
+ "Radix), 'basecn' (akash3444/basecn, fork Base UI), 'reactbits' (reactbits.dev, " +
850
+ "primitivas animadas; para cada base se exponen sus 4 variantes JS-CSS/JS-TW/TS-CSS/TS-TW) " +
851
+ "y 'blocks-so' (ephraimduncan/blocks, 60+ bloques Radix prod-ready con categories[] explicito; " +
852
+ "default para stats, login, form-layout, file-upload, tables, dialogs, sidebar, " +
853
+ "command-menu, ai, onboarding, grid-list). " +
854
+ "Cachea en .openspec/cache/ con TTL por registry (7d shadcn/basecn/blocks-so, 1d reactbits). " +
855
+ "Filtra por categoria (hero, footer, pricing, backgrounds, text, cards, stats, login, etc) " +
604
856
  "y por query libre (substring sobre name/description/tags).",
605
857
  args: {
606
858
  category: tool.schema
@@ -615,10 +867,10 @@ export const list_blocks = tool({
615
867
  .optional()
616
868
  .describe("Busqueda libre por substring sobre name/filename/description. Opcional."),
617
869
  registry: tool.schema
618
- .enum(["shadcn", "basecn", "reactbits", "all"])
870
+ .enum(["shadcn", "basecn", "reactbits", "blocks-so", "all"])
619
871
  .optional()
620
872
  .default("all")
621
- .describe("Que catalogos consultar. 'all' = shadcn+basecn+reactbits."),
873
+ .describe("Que catalogos consultar. 'all' = shadcn+basecn+reactbits+blocks-so."),
622
874
  limit: tool.schema
623
875
  .number()
624
876
  .optional()
@@ -661,6 +913,24 @@ export const list_blocks = tool({
661
913
  "TS-TW (TypeScript + Tailwind v4). Para otra variante, pasa " +
662
914
  "`variant='JS-CSS'` a `sdd_catalog_get_block`."
663
915
  )
916
+ } else if (reg === "blocks-so") {
917
+ const { index, refreshed, error } = await buildBlocksSoIndex(
918
+ root,
919
+ args.force_refresh || false
920
+ )
921
+ cachedAt[reg] = index.cached_at
922
+ if (refreshed) {
923
+ // ok
924
+ }
925
+ if (error) errors.push(`[${reg}] ${error}`)
926
+ allEntries.push(...index.entries)
927
+ hints.push(
928
+ "[blocks-so] ephraimduncan/blocks (https://blocks.so). " +
929
+ "Default para sus 11 categorias: stats, login, form-layout, " +
930
+ "file-upload, tables, dialogs, sidebar, command-menu, ai, " +
931
+ "onboarding, grid-list. Install: `npx shadcn@latest add " +
932
+ "https://blocks.so/r/<name>.json --yes`."
933
+ )
664
934
  } else {
665
935
  const { index, refreshed, error } = await buildOrLoadIndex(
666
936
  root,
@@ -758,9 +1028,12 @@ export const get_block = tool({
758
1028
  "especifico del catalogo unificado. " +
759
1029
  "Para shadcn/basecn: `name='hero-06'`. " +
760
1030
  "Para reactbits: `name='Dither'` (resuelve a Dither-TS-TW por default) o " +
761
- "`name='Dither-JS-CSS'` (variante canonica completa). Tambien acepta URL " +
762
- "directa `name='https://reactbits.dev/r/Dither-TS-TW.json'` y namespace shadcn " +
763
- "`name='@acme/button'`. Cachea en .openspec/cache/blocks/<registry>/<name>.json. " +
1031
+ "`name='Dither-JS-CSS'` (variante canonica completa). " +
1032
+ "Para blocks-so: `name='stats-01'`. " +
1033
+ "Tambien acepta URL directa `name='https://reactbits.dev/r/Dither-TS-TW.json'` " +
1034
+ "o `name='https://blocks.so/r/stats-01.json'`, y namespace shadcn " +
1035
+ "`name='@acme/button'` (incluido `@blocks-so/stats-01`). " +
1036
+ "Cachea en .openspec/cache/blocks/<registry>/<name>.json. " +
764
1037
  "Devuelve `install_command` listo para ejecutar con `npx shadcn@latest add`.",
765
1038
  args: {
766
1039
  name: tool.schema
@@ -768,12 +1041,13 @@ export const get_block = tool({
768
1041
  .describe(
769
1042
  "Identificador del bloque. Formatos aceptados:\n" +
770
1043
  " - shadcn/basecn: 'hero-06', 'sidebar-07'\n" +
1044
+ " - blocks-so: 'stats-01', 'login-03', 'form-layout-02'\n" +
771
1045
  " - reactbits: 'Dither' (default TS-TW) o 'Dither-JS-CSS' (canonica)\n" +
772
- " - URL directa: 'https://reactbits.dev/r/Dither-TS-TW.json'\n" +
773
- " - Namespace shadcn: '@acme/button'"
1046
+ " - URL directa: 'https://reactbits.dev/r/Dither-TS-TW.json' o 'https://blocks.so/r/stats-01.json'\n" +
1047
+ " - Namespace shadcn: '@acme/button' o '@blocks-so/stats-01'"
774
1048
  ),
775
1049
  registry: tool.schema
776
- .enum(["auto", "shadcn", "basecn", "reactbits"])
1050
+ .enum(["auto", "shadcn", "basecn", "reactbits", "blocks-so"])
777
1051
  .optional()
778
1052
  .default("auto")
779
1053
  .describe(
@@ -823,6 +1097,18 @@ export const get_block = tool({
823
1097
  baseName = resolved.baseName
824
1098
  variant = resolved.variant
825
1099
  directUrl = url
1100
+ } else if (url.includes("blocks.so/r/")) {
1101
+ const m = url.match(/\/r\/([a-zA-Z0-9_.-]+)\.json/)
1102
+ if (!m) {
1103
+ return JSON.stringify({
1104
+ status: "ERROR",
1105
+ message: `URL blocks.so invalida: ${url}. Esperado patron /r/<name>.json.`,
1106
+ block: null
1107
+ }, null, 2)
1108
+ }
1109
+ registry = "blocks-so"
1110
+ canonicalName = m[1]
1111
+ directUrl = url
826
1112
  } else {
827
1113
  // URL generica shadcn: tratarla como shadcn (1 sola parte)
828
1114
  const m = url.match(/\/([a-zA-Z0-9_.-]+)\.json/)
@@ -838,7 +1124,8 @@ export const get_block = tool({
838
1124
  directUrl = url
839
1125
  }
840
1126
  } else if (rawName.startsWith("@")) {
841
- // Namespace shadcn @acme/item
1127
+ // Namespace shadcn @<author>/<item>. Detectamos @blocks-so y lo
1128
+ // redirigimos al registry blocks-so (canonicalName sin prefijo).
842
1129
  const m = rawName.match(/^@([a-zA-Z0-9_.-]+)\/(.+)$/)
843
1130
  if (!m) {
844
1131
  return JSON.stringify({
@@ -847,8 +1134,13 @@ export const get_block = tool({
847
1134
  block: null
848
1135
  }, null, 2)
849
1136
  }
850
- registry = "shadcn"
851
- canonicalName = `${m[1]}/${m[2]}`
1137
+ if (m[1] === "blocks-so" || m[1] === "blocks_so") {
1138
+ registry = "blocks-so"
1139
+ canonicalName = m[2]
1140
+ } else {
1141
+ registry = "shadcn"
1142
+ canonicalName = `${m[1]}/${m[2]}`
1143
+ }
852
1144
  } else {
853
1145
  // Auto-detectar: si contiene '-JS-' o '-TS-' seguido de CSS/TW, es reactbits canonica.
854
1146
  // Si el registry forzado es reactbits, resolver siempre.
@@ -866,6 +1158,9 @@ export const get_block = tool({
866
1158
  } else if (requestedReg === "basecn") {
867
1159
  registry = "basecn"
868
1160
  canonicalName = rawName
1161
+ } else if (requestedReg === "blocks-so") {
1162
+ registry = "blocks-so"
1163
+ canonicalName = rawName
869
1164
  } else {
870
1165
  registry = "shadcn"
871
1166
  canonicalName = rawName
@@ -897,6 +1192,19 @@ export const get_block = tool({
897
1192
  installCommand = `npx shadcn@latest add ${installUrl} --yes`
898
1193
  category = inferCategory(canonicalName)
899
1194
  notes = "Bloque Base UI (fork). Verifica que tu proyecto tenga Base UI instalado."
1195
+ } else if (registry === "blocks-so") {
1196
+ // blocks.so: el JSON crudo vive en /content/components/<category>/<name>.tsx
1197
+ // pero el contrato del arnes siempre expone el `name` canonico
1198
+ // (ej: "stats-01"), NO el path del repo. La URL raw debe apuntar al
1199
+ // archivo .tsx fuente para que `content` contenga el codigo completo.
1200
+ rawUrl = directUrl || BLOCKSSO_RAW(canonicalName)
1201
+ cachePath = path.resolve(getBlocksDir(root, "blocks-so"), `${canonicalName}.json`)
1202
+ installUrl = BLOCKSSO_INSTALL(canonicalName)
1203
+ const resolvedCategory = inferCategory(canonicalName)
1204
+ previewUrl = BLOCKSSO_PREVIEW(resolvedCategory, canonicalName)
1205
+ installCommand = `npx shadcn@latest add ${installUrl} --yes`
1206
+ category = resolvedCategory
1207
+ notes = "Bloque blocks.so (ephraimduncan/blocks). Formato shadcn registry estandar (registry:block). Compatible con stack nextjs-shadcn."
900
1208
  } else {
901
1209
  rawUrl = AKASH_RAW(canonicalName)
902
1210
  cachePath = path.resolve(getBlocksDir(root, "shadcn"), `${canonicalName}.json`)
@@ -989,10 +1297,10 @@ export const warm_index = tool({
989
1297
  "Pre-calienta (descarga) los indices de los registries soportados " +
990
1298
  "para evitar la latencia del primer list_blocks/get_block. " +
991
1299
  "Idempotente: si el cache existe y es fresco, no re-descarga. " +
992
- "TTL por registry: shadcn/basecn 7d, reactbits 1d.",
1300
+ "TTL por registry: shadcn/basecn/blocks-so 7d, reactbits 1d.",
993
1301
  args: {
994
1302
  registry: tool.schema
995
- .enum(["shadcn", "basecn", "reactbits", "all"])
1303
+ .enum(["shadcn", "basecn", "reactbits", "blocks-so", "all"])
996
1304
  .optional()
997
1305
  .default("all")
998
1306
  .describe("Que catalogos pre-calentar.")
@@ -1016,6 +1324,13 @@ export const warm_index = tool({
1016
1324
  ? `${reg} (re-descargado, ${index.total} bases)`
1017
1325
  : `${reg} (cache fresco de ${index.cached_at})`
1018
1326
  )
1327
+ } else if (reg === "blocks-so") {
1328
+ const { index, refreshed } = await buildBlocksSoIndex(root, false)
1329
+ warmed.push(
1330
+ refreshed
1331
+ ? `${reg} (re-descargado, ${index.total} bloques)`
1332
+ : `${reg} (cache fresco de ${index.cached_at})`
1333
+ )
1019
1334
  } else {
1020
1335
  const { index, refreshed } = await buildOrLoadIndex(root, reg, false)
1021
1336
  warmed.push(