From 86d82fcd727a5e79e69ad4178ea28513892c79b3 Mon Sep 17 00:00:00 2001 From: rob Date: Tue, 13 Jan 2026 20:36:21 -0400 Subject: [PATCH] Fix TUI registry browser and harden search API - TUI: Use list_tools for browsing (no query), search_tools only when user enters a search term. Fixes 500 error on initial registry load. - API: Sanitize FTS5 queries by escaping special characters (* " ( ) etc) Prevents SQL errors from malformed search queries like "*" Co-Authored-By: Claude Opus 4.5 --- src/cmdforge/registry/app.py | 9 +++++++++ src/cmdforge/ui_urwid/__init__.py | 11 +++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/cmdforge/registry/app.py b/src/cmdforge/registry/app.py index ab2ff5e..ed587e7 100644 --- a/src/cmdforge/registry/app.py +++ b/src/cmdforge/registry/app.py @@ -479,6 +479,15 @@ def create_app() -> Flask: query_text = request.args.get("q", "").strip() if not query_text: return error_response("VALIDATION_ERROR", "Missing search query") + + # Sanitize query for FTS5 - escape special characters that cause syntax errors + # FTS5 special chars: * " ( ) : ^ - NOT AND OR NEAR + # For safety, we'll quote the entire query if it contains special chars + fts5_special = set('*"():^-') + if any(c in fts5_special for c in query_text) or query_text.upper() in ('NOT', 'AND', 'OR', 'NEAR'): + # Escape double quotes and wrap in quotes for literal search + query_text = '"' + query_text.replace('"', '""') + '"' + page, per_page, sort, order, error = parse_pagination("/tools/search", "downloads") if error: return error diff --git a/src/cmdforge/ui_urwid/__init__.py b/src/cmdforge/ui_urwid/__init__.py index e9e4990..610d2cb 100644 --- a/src/cmdforge/ui_urwid/__init__.py +++ b/src/cmdforge/ui_urwid/__init__.py @@ -1465,12 +1465,19 @@ No explanations, no markdown fencing, just the code.""" def do_search(_=None): query = search_edit.base_widget.edit_text.strip() self._registry_search_query = query - status_text.set_text(('label', f"Searching for '{query}'...")) + if query: + status_text.set_text(('label', f"Searching for '{query}'...")) + else: + status_text.set_text(('label', "Loading all tools...")) self.refresh() try: client = RegistryClient() - result = client.search_tools(query=query if query else "*", per_page=50) + # Use list_tools for browsing, search_tools only when there's a query + if query: + result = client.search_tools(query=query, per_page=50) + else: + result = client.list_tools(per_page=50) self._registry_tools = result.data # Update the list