"""search-file cmdlet: Search for files in storage backends (Hydrus).""" from __future__ import annotations from typing import Any, Dict, Sequence, List, Optional import uuid from pathlib import Path import re import json import sys from SYS.logger import log, debug from ProviderCore.registry import get_search_provider, list_search_providers from SYS.rich_display import ( show_provider_config_panel, show_store_config_panel, show_available_providers_panel, ) from SYS.database import insert_worker, update_worker, append_worker_stdout from ._shared import ( Cmdlet, CmdletArg, SharedArgs, get_field, should_show_help, normalize_hash, first_title_tag, parse_hash_query, ) from SYS import pipeline as ctx class _WorkerLogger: def __init__(self, worker_id: str) -> None: self.worker_id = worker_id def __enter__(self) -> "_WorkerLogger": return self def __exit__(self, exc_type, exc, tb) -> None: # type: ignore[override] return None def insert_worker( self, worker_id: str, worker_type: str, title: str = "", description: str = "", **kwargs: Any, ) -> None: try: insert_worker(worker_id, worker_type, title=title, description=description) except Exception: pass def update_worker_status(self, worker_id: str, status: str) -> None: try: normalized = (status or "").lower() kwargs: dict[str, str] = {"status": status} if normalized in {"completed", "error", "cancelled"}: kwargs["result"] = normalized update_worker(worker_id, **kwargs) except Exception: pass def append_worker_stdout(self, worker_id: str, content: str) -> None: try: append_worker_stdout(worker_id, content) except Exception: pass class search_file(Cmdlet): """Class-based search-file cmdlet for searching storage backends.""" def __init__(self) -> None: super().__init__( name="search-file", summary="Search storage backends (Hydrus) or external providers (via -provider).", usage="search-file [-query ] [-store BACKEND] [-limit N] [-provider NAME]", arg=[ CmdletArg( "limit", type="integer", description="Limit results (default: 100)" ), SharedArgs.STORE, SharedArgs.QUERY, CmdletArg( "provider", type="string", description="External provider name (e.g., tidal, youtube, soulseek, etc)", ), CmdletArg( "open", type="integer", description="(alldebrid) Open folder/magnet by ID and list its files", ), ], detail=[ "Search across storage backends: Hydrus instances", "Use -store to search a specific backend by name", "URL search: url:* (any URL) or url: (URL substring)", "Extension search: ext: (e.g., ext:png)", "Hydrus-style extension: system:filetype = png", "Results include hash for downstream commands (get-file, add-tag, etc.)", "Examples:", "search-file -query foo # Search all storage backends", "search-file -store home -query '*' # Search 'home' Hydrus instance", "search-file -store home -query 'video' # Search 'home' Hydrus instance", "search-file -query 'hash:deadbeef...' # Search by SHA256 hash", "search-file -query 'url:*' # Files that have any URL", "search-file -query 'url:youtube.com' # Files whose URL contains substring", "search-file -query 'ext:png' # Files whose metadata ext is png", "search-file -query 'system:filetype = png' # Hydrus: native", "", "Provider search (-provider):", "search-file -provider youtube 'tutorial' # Search YouTube provider", "search-file -provider alldebrid '*' # List AllDebrid magnets", "search-file -provider alldebrid -open 123 '*' # Show files for a magnet", ], exec=self.run, ) self.register() # --- Helper methods ------------------------------------------------- @staticmethod def _normalize_extension(ext_value: Any) -> str: """Sanitize extension strings to alphanumerics and cap at 5 chars.""" ext = str(ext_value or "").strip().lstrip(".") for sep in (" ", "|", "(", "[", "{", ",", ";"): if sep in ext: ext = ext.split(sep, 1)[0] break if "." in ext: ext = ext.split(".")[-1] ext = "".join(ch for ch in ext if ch.isalnum()) return ext[:5] @staticmethod def _normalize_lookup_target(value: Optional[str]) -> str: """Normalize candidate names for store/provider matching.""" raw = str(value or "").strip().lower() return "".join(ch for ch in raw if ch.isalnum()) def _ensure_storage_columns(self, payload: Dict[str, Any]) -> Dict[str, Any]: """Ensure storage results have the necessary fields for result_table display.""" # Ensure we have title field if "title" not in payload: payload["title"] = ( payload.get("name") or payload.get("target") or payload.get("path") or "Result" ) # Ensure we have ext field if ("ext" not in payload) or (not str(payload.get("ext") or "").strip()): title = str(payload.get("title", "")) path_obj = Path(title) if path_obj.suffix: payload["ext"] = self._normalize_extension(path_obj.suffix.lstrip(".")) else: payload["ext"] = payload.get("ext", "") # Ensure size_bytes is present for display (already set by search_file()) # result_table will handle formatting it # Don't create manual columns - let result_table handle display # This allows the table to respect max_columns and apply consistent formatting return payload def _run_provider_search( self, *, provider_name: str, query: str, limit: int, limit_set: bool, open_id: Optional[int], args_list: List[str], refresh_mode: bool, config: Dict[str, Any], ) -> int: """Execute external provider search.""" if not provider_name or not query: from SYS import pipeline as ctx_mod progress = None if hasattr(ctx_mod, "get_pipeline_state"): progress = ctx_mod.get_pipeline_state().live_progress if progress: try: progress.stop() except Exception: pass log("Error: search-file -provider requires both provider and query", file=sys.stderr) log(f"Usage: {self.usage}", file=sys.stderr) providers_map = list_search_providers(config) available = [n for n, a in providers_map.items() if a] unconfigured = [n for n, a in providers_map.items() if not a] if unconfigured: show_provider_config_panel(unconfigured) if available: show_available_providers_panel(available) return 1 # Align with provider default when user did not set -limit. if not limit_set: limit = 50 from SYS import pipeline as ctx_mod progress = None if hasattr(ctx_mod, "get_pipeline_state"): progress = ctx_mod.get_pipeline_state().live_progress provider = get_search_provider(provider_name, config) if not provider: if progress: try: progress.stop() except Exception: pass show_provider_config_panel([provider_name]) providers_map = list_search_providers(config) available = [n for n, a in providers_map.items() if a] if available: show_available_providers_panel(available) return 1 worker_id = str(uuid.uuid4()) try: insert_worker( worker_id, "search-file", title=f"Search: {query}", description=f"Provider: {provider_name}, Query: {query}", ) except Exception: pass try: results_list: List[Dict[str, Any]] = [] from SYS.result_table import Table provider_text = str(provider_name or "").strip() provider_lower = provider_text.lower() # Dynamic query/filter extraction via provider normalized_query = str(query or "").strip() provider_filters: Dict[str, Any] = {} try: normalized_query, provider_filters = provider.extract_query_arguments(query) except Exception: provider_filters = {} normalized_query = (normalized_query or "").strip() query = normalized_query or "*" search_filters = dict(provider_filters or {}) # Dynamic table generation via provider table_title = provider.get_table_title(query, search_filters).strip().rstrip(":") table_type = provider.get_table_type(query, search_filters) table_meta = provider.get_table_metadata(query, search_filters) preserve_order = provider.preserve_order table = Table(table_title)._perseverance(preserve_order) table.set_table(table_type) try: table.set_table_metadata(table_meta) except Exception: pass # Dynamic source command via provider source_cmd, source_args = provider.get_source_command(args_list) table.set_source_command(source_cmd, source_args) debug(f"[search-file] Calling {provider_name}.search(filters={search_filters})") results = provider.search(query, limit=limit, filters=search_filters or None) debug(f"[search-file] {provider_name} -> {len(results or [])} result(s)") # Allow providers to apply provider-specific UX transforms (e.g. auto-expansion) try: post = getattr(provider, "postprocess_search_results", None) if callable(post) and isinstance(results, list): results, table_type_override, table_meta_override = post( query=query, results=results, filters=search_filters or None, limit=int(limit or 0), table_type=str(table_type or ""), table_meta=dict(table_meta) if isinstance(table_meta, dict) else None, ) if table_type_override: table_type = str(table_type_override) table.set_table(table_type) if isinstance(table_meta_override, dict) and table_meta_override: table_meta = dict(table_meta_override) try: table.set_table_metadata(table_meta) except Exception: pass except Exception: pass if not results: log(f"No results found for query: {query}", file=sys.stderr) try: append_worker_stdout(worker_id, json.dumps([], indent=2)) update_worker(worker_id, status="completed") except Exception: pass return 0 for search_result in results: item_dict = ( search_result.to_dict() if hasattr(search_result, "to_dict") else dict(search_result) if isinstance(search_result, dict) else {"title": str(search_result)} ) if "table" not in item_dict: item_dict["table"] = table_type # Ensure provider source is present so downstream cmdlets (select) can resolve provider if "source" not in item_dict: item_dict["source"] = provider_name row_index = len(table.rows) table.add_result(search_result) results_list.append(item_dict) ctx.emit(item_dict) if refresh_mode: ctx.set_last_result_table_preserve_history(table, results_list) else: ctx.set_last_result_table(table, results_list) ctx.set_current_stage_table(table) try: append_worker_stdout(worker_id, json.dumps(results_list, indent=2)) update_worker(worker_id, status="completed") except Exception: pass return 0 except Exception as exc: log(f"Error searching provider '{provider_name}': {exc}", file=sys.stderr) import traceback debug(traceback.format_exc()) try: update_worker(worker_id, status="error") except Exception: pass return 1 # --- Execution ------------------------------------------------------ def run(self, result: Any, args: Sequence[str], config: Dict[str, Any]) -> int: """Search storage backends for files by various criteria. Supports searching by: - Hash (-query "hash:...") - Title (-query "title:...") - Tag (-query "tag:...") - URL (-query "url:...") - Other backend-specific fields Optimizations: - Extracts tags from metadata response (avoids duplicate API calls) - Only calls get_tag() separately for backends that don't include tags Args: result: Piped input (typically empty for new search) args: Search criteria and options config: Application configuration Returns: 0 on success, 1 on error """ if should_show_help(args): log(f"Cmdlet: {self.name}\nSummary: {self.summary}\nUsage: {self.usage}") return 0 args_list = [str(arg) for arg in (args or [])] refresh_mode = any( str(a).strip().lower() in {"--refresh", "-refresh", "-internal-refresh"} for a in args_list ) def _format_command_title(command: str, raw_args: List[str]) -> str: def _quote(value: str) -> str: text = str(value) if not text: return '""' needs_quotes = any(ch.isspace() for ch in text) or '"' in text if not needs_quotes: return text return '"' + text.replace('"', '\\"') + '"' cleaned = [ str(a) for a in (raw_args or []) if str(a).strip().lower() not in {"--refresh", "-refresh", "-internal-refresh"} ] if not cleaned: return command return " ".join([command, *[_quote(a) for a in cleaned]]) raw_title = None try: raw_title = ( ctx.get_current_stage_text("") if hasattr(ctx, "get_current_stage_text") else None ) except Exception: raw_title = None command_title = (str(raw_title).strip() if raw_title else "") or _format_command_title("search-file", list(args_list)) # Build dynamic flag variants from cmdlet arg definitions. # This avoids hardcoding flag spellings in parsing loops. flag_registry = self.build_flag_registry() query_flags = { f.lower() for f in (flag_registry.get("query") or {"-query", "--query"}) } store_flags = { f.lower() for f in (flag_registry.get("store") or {"-store", "--store"}) } limit_flags = { f.lower() for f in (flag_registry.get("limit") or {"-limit", "--limit"}) } provider_flags = { f.lower() for f in (flag_registry.get("provider") or {"-provider", "--provider"}) } open_flags = { f.lower() for f in (flag_registry.get("open") or {"-open", "--open"}) } # Parse arguments query = "" storage_backend: Optional[str] = None provider_name: Optional[str] = None open_id: Optional[int] = None limit = 100 limit_set = False searched_backends: List[str] = [] i = 0 while i < len(args_list): arg = args_list[i] low = arg.lower() if low in query_flags and i + 1 < len(args_list): chunk = args_list[i + 1] query = f"{query} {chunk}".strip() if query else chunk i += 2 continue if low in provider_flags and i + 1 < len(args_list): provider_name = args_list[i + 1] i += 2 continue if low in open_flags and i + 1 < len(args_list): try: open_id = int(args_list[i + 1]) except ValueError: log( f"Warning: Invalid open value '{args_list[i + 1]}', ignoring", file=sys.stderr, ) open_id = None i += 2 continue if low in store_flags and i + 1 < len(args_list): storage_backend = args_list[i + 1] i += 2 elif low in limit_flags and i + 1 < len(args_list): limit_set = True try: limit = int(args_list[i + 1]) except ValueError: limit = 100 i += 2 elif not arg.startswith("-"): query = f"{query} {arg}".strip() if query else arg i += 1 else: i += 1 query = query.strip() if provider_name: return self._run_provider_search( provider_name=provider_name, query=query, limit=limit, limit_set=limit_set, open_id=open_id, args_list=args_list, refresh_mode=refresh_mode, config=config, ) store_filter: Optional[str] = None if query: match = re.search(r"\bstore:([^\s,]+)", query, flags=re.IGNORECASE) if match: store_filter = match.group(1).strip() or None query = re.sub(r"\s*[,]?\s*store:[^\s,]+", " ", query, flags=re.IGNORECASE) query = re.sub(r"\s{2,}", " ", query) query = query.strip().strip(",") if store_filter and not storage_backend: storage_backend = store_filter # If the user accidentally used `-store ` or `store:`, # prefer to treat it as a provider search (providers like 'alldebrid' are not store backends). try: from Store.registry import list_configured_backend_names providers_map = list_search_providers(config) configured = list_configured_backend_names(config or {}) if storage_backend: matched = None storage_hint = self._normalize_lookup_target(storage_backend) if storage_hint: for p in (providers_map or {}): if self._normalize_lookup_target(p) == storage_hint: matched = p break if matched and str(storage_backend) not in configured: log(f"Note: Treating '-store {storage_backend}' as provider search for '{matched}'", file=sys.stderr) return self._run_provider_search( provider_name=matched, query=query, limit=limit, limit_set=limit_set, open_id=open_id, args_list=args_list, refresh_mode=refresh_mode, config=config, ) elif store_filter: matched = None store_hint = self._normalize_lookup_target(store_filter) if store_hint: for p in (providers_map or {}): if self._normalize_lookup_target(p) == store_hint: matched = p break if matched and str(store_filter) not in configured: log(f"Note: Treating 'store:{store_filter}' as provider search for '{matched}'", file=sys.stderr) return self._run_provider_search( provider_name=matched, query=query, limit=limit, limit_set=limit_set, open_id=open_id, args_list=args_list, refresh_mode=refresh_mode, config=config, ) except Exception: # Be conservative: if provider detection fails, fall back to store behaviour pass hash_query = parse_hash_query(query) if not query: log("Provide a search query", file=sys.stderr) return 1 worker_id = str(uuid.uuid4()) from Store import Store storage_registry = Store(config=config or {}) if not storage_registry.list_backends(): # Internal refreshes should not trigger config panels or stop progress. if "-internal-refresh" in args_list: return 1 from SYS import pipeline as ctx_mod progress = None if hasattr(ctx_mod, "get_pipeline_state"): progress = ctx_mod.get_pipeline_state().live_progress if progress: try: progress.stop() except Exception: pass show_store_config_panel(["Hydrus Network"]) return 1 # Use a lightweight worker logger to track search results in the central DB with _WorkerLogger(worker_id) as db: try: if "-internal-refresh" not in args_list: db.insert_worker( worker_id, "search-file", title=f"Search: {query}", description=f"Query: {query}", pipe=ctx.get_current_command_text(), ) results_list = [] from SYS.result_table import Table table = Table(command_title) try: table.set_source_command("search-file", list(args_list)) except Exception: pass if hash_query: try: table._perseverance(True) except Exception: pass from Store.registry import list_configured_backend_names, get_backend_instance from Store._base import Store as BaseStore backend_to_search = storage_backend or None if hash_query: # Explicit hash list search: build rows from backend metadata. backends_to_try: List[str] = [] if backend_to_search: backends_to_try = [backend_to_search] else: backends_to_try = list_configured_backend_names(config or {}) found_any = False for h in hash_query: resolved_backend_name: Optional[str] = None resolved_backend = None for backend_name in backends_to_try: backend = None try: backend = get_backend_instance(config, backend_name, suppress_debug=True) if backend is None: # Last-resort: instantiate full registry for this backend only from Store import Store as _Store _store = _Store(config=config, suppress_debug=True) if _store.is_available(backend_name): backend = _store[backend_name] except Exception: backend = None if backend is None: continue try: # If get_metadata works, consider it a hit; get_file can be optional (e.g. remote URL). meta = backend.get_metadata(h) if meta is None: continue resolved_backend_name = backend_name resolved_backend = backend break except Exception: continue if resolved_backend_name is None or resolved_backend is None: continue found_any = True searched_backends.append(resolved_backend_name) # Resolve a path/URL string if possible path_str: Optional[str] = None # Avoid calling get_file() for remote backends during search/refresh. meta_obj: Dict[str, Any] = {} try: meta_obj = resolved_backend.get_metadata(h) or {} except Exception: meta_obj = {} # Extract tags from metadata response instead of separate get_tag() call # Metadata already includes tags if fetched with include_service_keys_to_tags=True tags_list: List[str] = [] # First try to extract from metadata tags dict metadata_tags = meta_obj.get("tags") if isinstance(metadata_tags, dict): for service_data in metadata_tags.values(): if isinstance(service_data, dict): display_tags = service_data.get("display_tags", {}) if isinstance(display_tags, dict): for tag_list in display_tags.values(): if isinstance(tag_list, list): tags_list = [ str(t).strip() for t in tag_list if isinstance(t, str) and str(t).strip() ] break if tags_list: break # Fallback: if metadata didn't include tags, call get_tag() separately # (This maintains compatibility with backends that don't include tags in metadata) if not tags_list: try: tag_result = resolved_backend.get_tag(h) if isinstance(tag_result, tuple) and tag_result: maybe_tags = tag_result[0] else: maybe_tags = tag_result if isinstance(maybe_tags, list): tags_list = [ str(t).strip() for t in maybe_tags if isinstance(t, str) and str(t).strip() ] except Exception: tags_list = [] title_from_tag: Optional[str] = None try: title_tag = first_title_tag(tags_list) if title_tag and ":" in title_tag: title_from_tag = title_tag.split(":", 1)[1].strip() except Exception: title_from_tag = None title = title_from_tag or meta_obj.get("title") or meta_obj.get( "name" ) if not title and path_str: try: title = Path(path_str).stem except Exception: title = path_str ext_val = meta_obj.get("ext") or meta_obj.get("extension") if not ext_val and path_str: try: ext_val = Path(path_str).suffix except Exception: ext_val = None if not ext_val and title: try: ext_val = Path(str(title)).suffix except Exception: ext_val = None size_bytes = meta_obj.get("size") if size_bytes is None: size_bytes = meta_obj.get("size_bytes") try: size_bytes_int: Optional[int] = ( int(size_bytes) if size_bytes is not None else None ) except Exception: size_bytes_int = None payload: Dict[str, Any] = { "title": str(title or h), "hash": h, "store": resolved_backend_name, "path": path_str, "ext": self._normalize_extension(ext_val), "size_bytes": size_bytes_int, "tag": tags_list, "url": meta_obj.get("url") or [], } table.add_result(payload) results_list.append(payload) ctx.emit(payload) if found_any: table.title = command_title # Add-file refresh quality-of-life: if exactly 1 item is being refreshed, # show the detailed item panel instead of a single-row table. if refresh_mode and len(results_list) == 1: try: from SYS.rich_display import render_item_details_panel render_item_details_panel(results_list[0]) table._rendered_by_cmdlet = True except Exception: pass if refresh_mode: ctx.set_last_result_table_preserve_history( table, results_list ) else: ctx.set_last_result_table(table, results_list) db.append_worker_stdout( worker_id, json.dumps(results_list, indent=2) ) db.update_worker_status(worker_id, "completed") return 0 log("No results found", file=sys.stderr) if refresh_mode: try: table.title = command_title ctx.set_last_result_table_preserve_history(table, []) except Exception: pass db.append_worker_stdout(worker_id, json.dumps([], indent=2)) db.update_worker_status(worker_id, "completed") return 0 if backend_to_search: searched_backends.append(backend_to_search) try: target_backend = get_backend_instance(config, backend_to_search, suppress_debug=True) if target_backend is None: from Store import Store as _Store _store = _Store(config=config, suppress_debug=True) if _store.is_available(backend_to_search): target_backend = _store[backend_to_search] else: debug(f"[search-file] Requested backend '{backend_to_search}' not found") return 1 except Exception as exc: log(f"Backend '{backend_to_search}' not found: {exc}", file=sys.stderr) db.update_worker_status(worker_id, "error") return 1 if type(target_backend).search is BaseStore.search: log( f"Backend '{backend_to_search}' does not support searching", file=sys.stderr, ) db.update_worker_status(worker_id, "error") return 1 debug(f"[search-file] Searching '{backend_to_search}'") results = target_backend.search(query, limit=limit) debug( f"[search-file] '{backend_to_search}' -> {len(results or [])} result(s)" ) else: all_results = [] for backend_name in list_configured_backend_names(config or {}): try: backend = get_backend_instance(config, backend_name, suppress_debug=True) if backend is None: from Store import Store as _Store _store = _Store(config=config, suppress_debug=True) if _store.is_available(backend_name): backend = _store[backend_name] else: # Configured backend name exists but has no registered implementation or failed to load. # (e.g. 'all-debrid' being treated as a store but having no store provider). continue searched_backends.append(backend_name) if type(backend).search is BaseStore.search: continue debug(f"[search-file] Searching '{backend_name}'") backend_results = backend.search( query, limit=limit - len(all_results) ) debug( f"[search-file] '{backend_name}' -> {len(backend_results or [])} result(s)" ) if backend_results: all_results.extend(backend_results) if len(all_results) >= limit: break except Exception as exc: log( f"Backend {backend_name} search failed: {exc}", file=sys.stderr ) results = all_results[:limit] if results: for item in results: def _as_dict(obj: Any) -> Dict[str, Any]: if isinstance(obj, dict): return dict(obj) if hasattr(obj, "to_dict") and callable(getattr(obj, "to_dict")): return obj.to_dict() # type: ignore[arg-type] return { "title": str(obj) } item_dict = _as_dict(item) if store_filter: store_val = str(item_dict.get("store") or "").lower() if store_filter != store_val: continue # Normalize storage results (ensure title, ext, etc.) normalized = self._ensure_storage_columns(item_dict) # If normalize skipped it due to STORAGE_ORIGINS, do it manually if "title" not in normalized: normalized["title"] = ( item_dict.get("title") or item_dict.get("name") or item_dict.get("path") or item_dict.get("target") or "Result" ) if "ext" not in normalized: t = str(normalized.get("title", "")) if "." in t: normalized["ext"] = t.split(".")[-1].lower()[:5] # Make hash/store available for downstream cmdlet without rerunning search hash_val = normalized.get("hash") store_val = normalized.get("store") or item_dict.get("store") or backend_to_search if hash_val and not normalized.get("hash"): normalized["hash"] = hash_val if store_val and not normalized.get("store"): normalized["store"] = store_val # Populate default selection args for interactive @N selection/hash/url handling try: sel_args: Optional[List[str]] = None sel_action: Optional[List[str]] = None # Prefer explicit path when available p_val = normalized.get("path") or normalized.get("target") or normalized.get("url") if p_val: p_str = str(p_val or "").strip() if p_str: if p_str.startswith(("http://", "https://", "magnet:", "torrent:")): h = normalized.get("hash") or normalized.get("file_hash") or normalized.get("hash_hex") s_val = normalized.get("store") if h and s_val and "/view_file" in p_str: try: h_norm = normalize_hash(h) except Exception: h_norm = str(h) sel_args = ["-query", f"hash:{h_norm}", "-store", str(s_val)] sel_action = ["get-metadata", "-query", f"hash:{h_norm}", "-store", str(s_val)] else: sel_args = ["-url", p_str] sel_action = ["download-file", "-url", p_str] else: try: from SYS.utils import expand_path full_path = expand_path(p_str) # Prefer showing metadata details when we have a hash+store context h = normalized.get("hash") or normalized.get("file_hash") or normalized.get("hash_hex") s_val = normalized.get("store") if h and s_val: try: h_norm = normalize_hash(h) except Exception: h_norm = str(h) sel_args = ["-query", f"hash:{h_norm}", "-store", str(s_val)] sel_action = ["get-metadata", "-query", f"hash:{h_norm}", "-store", str(s_val)] else: sel_args = ["-path", str(full_path)] # Default action for local paths: get-file to fetch or operate on the path sel_action = ["get-file", "-path", str(full_path)] except Exception: sel_args = ["-path", p_str] sel_action = ["get-file", "-path", p_str] # Fallback: use hash+store when available if sel_args is None: h = normalized.get("hash") or normalized.get("file_hash") or normalized.get("hash_hex") s_val = normalized.get("store") if h and s_val: try: h_norm = normalize_hash(h) except Exception: h_norm = str(h) sel_args = ["-query", f"hash:{h_norm}", "-store", str(s_val)] # Show metadata details by default for store/hash selections sel_action = ["get-metadata", "-query", f"hash:{h_norm}", "-store", str(s_val)] if sel_args: normalized["_selection_args"] = [str(x) for x in sel_args] if sel_action: normalized["_selection_action"] = [str(x) for x in sel_action] except Exception: pass table.add_result(normalized) results_list.append(normalized) ctx.emit(normalized) table.title = command_title # If exactly 1 item is being refreshed, show the detailed item panel. if refresh_mode and len(results_list) == 1: try: from SYS.rich_display import render_item_details_panel render_item_details_panel(results_list[0]) table._rendered_by_cmdlet = True except Exception: pass if refresh_mode: # For internal refresh, use overlay mode to avoid adding to history try: # Parse out the store/hash context if possible subject_context = None if "hash:" in query: subject_hash = query.split("hash:")[1].split(",")[0].strip() subject_context = {"store": backend_to_search, "hash": subject_hash} ctx.set_last_result_table_overlay(table, results_list, subject=subject_context) except Exception: ctx.set_last_result_table_preserve_history(table, results_list) else: ctx.set_last_result_table(table, results_list) db.append_worker_stdout( worker_id, json.dumps(results_list, indent=2) ) else: log("No results found", file=sys.stderr) if refresh_mode: try: table.title = command_title ctx.set_last_result_table_preserve_history(table, []) except Exception: pass db.append_worker_stdout(worker_id, json.dumps([], indent=2)) db.update_worker_status(worker_id, "completed") return 0 except Exception as exc: log(f"Search failed: {exc}", file=sys.stderr) import traceback traceback.print_exc(file=sys.stderr) try: db.update_worker_status(worker_id, "error") except Exception: pass return 1 CMDLET = search_file()