1- """Registry client — fetches component data from the Hanzo UI registry server.
1+ """Registry client — fetches component data from the Hanzo UI site.
2+
3+ Works with both:
4+ - Static hosting (CF Pages, GitHub Pages): fetches pre-built JSON from /api/registry/
5+ - Server mode (Hanzo PaaS, dev): hits Next.js API routes at /api/registry/
26
37Mirrors the GitHubAPIClient / LocalUIClient interface so it can be used
48as a drop-in backend in UiTool. Has its own short TTL cache to avoid
1115
1216import httpx
1317
18+ # Registry URLs by framework
19+ REGISTRY_URLS : dict [str , str ] = {
20+ "hanzo" : "https://ui.hanzo.ai" ,
21+ "lux" : "https://ui.lux.finance" ,
22+ }
1423DEFAULT_REGISTRY_URL = "https://ui.hanzo.ai"
15- CLIENT_CACHE_TTL = 60 # 1 minute local cache
24+ CLIENT_CACHE_TTL = 300 # 5 min local cache (server data is pre-built)
1625
1726
1827class RegistryClient :
19- """Client for the Hanzo UI registry server ."""
28+ """Client for the Hanzo UI registry (static or dynamic) ."""
2029
2130 def __init__ (self , base_url : str | None = None ):
2231 self ._base_url = (
@@ -27,12 +36,16 @@ def __init__(self, base_url: str | None = None):
2736 self ._client : httpx .AsyncClient | None = None
2837 self ._cache : dict [str , tuple [Any , float ]] = {}
2938 self ._available : bool | None = None
39+ # Full index cache — hydrated on first use
40+ self ._index : dict | None = None
41+ self ._index_ts : float = 0
3042
3143 async def _get_client (self ) -> httpx .AsyncClient :
3244 if self ._client is None :
3345 self ._client = httpx .AsyncClient (
3446 base_url = self ._base_url ,
3547 timeout = 15.0 ,
48+ follow_redirects = True ,
3649 headers = {"User-Agent" : "Hanzo-MCP-UI-Tool" },
3750 )
3851 return self ._client
@@ -64,11 +77,17 @@ async def _get(self, path: str, params: dict | None = None) -> Any:
6477 return data
6578
6679 async def check_available (self ) -> bool :
67- """Check if the registry server is reachable."""
80+ """Check if the registry is reachable (tries static files first) ."""
6881 if self ._available is not None :
6982 return self ._available
7083 try :
7184 client = await self ._get_client ()
85+ # Try the static components.json (works on CF Pages / static hosting)
86+ resp = await client .get ("/api/registry/components.json" , timeout = 5.0 )
87+ if resp .status_code == 200 :
88+ self ._available = True
89+ return True
90+ # Try server-mode health endpoint
7291 resp = await client .get ("/api/health" , timeout = 5.0 )
7392 self ._available = resp .status_code == 200
7493 except Exception :
@@ -77,65 +96,133 @@ async def check_available(self) -> bool:
7796
7897 @property
7998 def available (self ) -> bool | None :
80- """Cached availability check result. None if not yet checked."""
8199 return self ._available
82100
101+ async def _ensure_index (self ) -> dict :
102+ """Fetch and cache the full index (single HTTP call, all components)."""
103+ if self ._index and time .time () - self ._index_ts < CLIENT_CACHE_TTL :
104+ return self ._index
105+
106+ try :
107+ # Try static file first (CF Pages)
108+ data = await self ._get ("/api/registry/index.json" )
109+ except Exception :
110+ # Fallback to server-mode API route
111+ data = await self ._get ("/api/registry/index" )
112+
113+ self ._index = data
114+ self ._index_ts = time .time ()
115+ return data
116+
117+ def _extract_source (self , component : dict ) -> str | None :
118+ """Extract source code from a registry component entry."""
119+ files = component .get ("files" , [])
120+ if not files :
121+ return None
122+ first = files [0 ]
123+ if isinstance (first , dict ):
124+ return first .get ("content" )
125+ return None
126+
83127 # --- Mirror of GitHubAPIClient / LocalUIClient interface ---
84128
85129 async def list_components (self , framework : str = "hanzo" ) -> list [dict ]:
86- data = await self ._get ("/api/components" , {"framework" : framework })
130+ try :
131+ data = await self ._get ("/api/registry/components.json" )
132+ except Exception :
133+ data = await self ._get ("/api/registry" , {"type" : f"components:ui" })
87134 return data .get ("components" , [])
88135
89136 async def fetch_component (self , name : str , framework : str = "hanzo" ) -> str :
90- data = await self ._get (f"/api/components/{ name } " , {"framework" : framework })
91- source = data .get ("source" )
137+ try :
138+ # Try static file (CF Pages)
139+ data = await self ._get (f"/api/registry/components/{ name } .json" )
140+ except Exception :
141+ # Fallback to API route
142+ data = await self ._get (f"/api/registry/components/{ name } " )
143+
144+ source = self ._extract_source (data )
92145 if source is None :
93146 raise FileNotFoundError (f"Component '{ name } ' source not available" )
94147 return source
95148
96149 async def fetch_component_demo (self , name : str , framework : str = "hanzo" ) -> str :
97- data = await self ._get (
98- f"/api/components/{ name } /demo" , {"framework" : framework }
99- )
100- demo = data .get ("demo" )
101- if demo is None :
150+ try :
151+ data = await self ._get (f"/api/registry/components/{ name } -demo.json" )
152+ except Exception :
153+ try :
154+ data = await self ._get (f"/api/registry/components/{ name } -demo" )
155+ except Exception :
156+ raise FileNotFoundError (f"Demo for '{ name } ' not available" )
157+
158+ source = self ._extract_source (data )
159+ if source is None :
102160 raise FileNotFoundError (f"Demo for '{ name } ' not available" )
103- return demo
161+ return source
104162
105163 async def fetch_component_metadata (
106164 self , name : str , framework : str = "hanzo"
107165 ) -> dict :
108- data = await self ._get (
109- f"/api/components/{ name } /metadata" , {"framework" : framework }
110- )
111- return data .get ("metadata" , data )
166+ try :
167+ data = await self ._get (f"/api/registry/components/{ name } .json" )
168+ except Exception :
169+ data = await self ._get (f"/api/registry/components/{ name } " )
170+
171+ return {
172+ "name" : data .get ("name" , name ),
173+ "type" : data .get ("type" ),
174+ "dependencies" : data .get ("dependencies" , []),
175+ "registryDependencies" : data .get ("registryDependencies" , []),
176+ "source" : "registry" ,
177+ }
112178
113179 async def list_blocks (self , framework : str = "hanzo" ) -> list [dict ]:
114- data = await self ._get ( "/api/blocks" , { " framework" : framework } )
115- return data .get ("blocks " , [])
180+ comps = await self .list_components ( framework )
181+ return [ c for c in comps if "block" in c .get ("type " , "" ). lower ()]
116182
117183 async def fetch_block (self , name : str , framework : str = "hanzo" ) -> str :
118- data = await self ._get (f"/api/blocks/{ name } " , {"framework" : framework })
119- source = data .get ("source" )
120- if source is None :
121- raise FileNotFoundError (f"Block '{ name } ' source not available" )
122- return source
184+ return await self .fetch_component (name , framework )
123185
124- async def search_components (self , query : str , framework : str = "hanzo" ) -> list [dict ]:
125- data = await self ._get ("/api/search" , {"q" : query , "framework" : framework })
126- return data .get ("results" , [])
186+ async def search_components (
187+ self , query : str , framework : str = "hanzo"
188+ ) -> list [dict ]:
189+ try :
190+ # Try server-mode search
191+ data = await self ._get ("/api/registry/search" , {"q" : query })
192+ return data .get ("results" , [])
193+ except Exception :
194+ pass
195+
196+ # Fallback: search client-side using the static index
197+ try :
198+ data = await self ._get ("/api/registry/search-index.json" )
199+ except Exception :
200+ data = await self .list_components (framework )
201+ q = query .lower ()
202+ return [c for c in data if q in c .get ("name" , "" ).lower ()]
203+
204+ q = query .lower ()
205+ return [
206+ {"name" : item ["n" ], "type" : item .get ("t" , "" )}
207+ for item in data
208+ if q in item .get ("n" , "" ).lower ()
209+ ]
127210
128211 async def get_directory_structure (
129212 self , path : str , framework : str = "hanzo"
130213 ) -> dict :
131- return await self ._get ("/api/structure" , {"path" : path , "framework" : framework })
214+ # Not available as static — return component list as structure
215+ comps = await self .list_components (framework )
216+ return {
217+ "path" : path or "/" ,
218+ "children" : [
219+ {"name" : c ["name" ], "type" : "file" } for c in comps
220+ ],
221+ }
132222
133223 async def fetch_full_index (self , framework : str = "hanzo" ) -> dict :
134- """Fetch the full registry index — all components in one payload.
135-
136- Use this to hydrate a local cache with a single HTTP call.
137- """
138- return await self ._get ("/registry/index.json" , {"framework" : framework })
224+ """Fetch the full registry index — all components in one payload."""
225+ return await self ._ensure_index ()
139226
140227 async def close (self ) -> None :
141228 if self ._client is not None :
0 commit comments