Harden M8 trust and guidance workflows
This commit is contained in:
+92
-30
@@ -2,8 +2,10 @@
|
||||
|
||||
import sys
|
||||
import os
|
||||
import select
|
||||
import tty
|
||||
import termios
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import List, Tuple, Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
@@ -30,6 +32,7 @@ GREEN = "\033[32m"
|
||||
RESET = "\033[0m"
|
||||
|
||||
MAX_VISIBLE = 8 # Show at most 8 items
|
||||
_registry_cache = {}
|
||||
|
||||
# Output stream for UI (stderr when stdout is piped, stdout otherwise)
|
||||
_ui_out = None
|
||||
@@ -90,31 +93,60 @@ def search_registry(query: str) -> List[dict]:
|
||||
"""Search the registry for tools matching the query.
|
||||
|
||||
Returns a list of dicts with name, desc, and registry metadata.
|
||||
Returns empty list if no token configured or search fails.
|
||||
Registry search is public; authentication is not required.
|
||||
"""
|
||||
if not query or len(query) < 2:
|
||||
return []
|
||||
normalized = query.strip().lower()
|
||||
if normalized in _registry_cache:
|
||||
return [dict(item) for item in _registry_cache[normalized]]
|
||||
try:
|
||||
from ..registry_client import get_client
|
||||
from ..registry_client import RegistryError, get_client
|
||||
client = get_client()
|
||||
if not client or not client.token:
|
||||
return []
|
||||
results = client.search_tools(query, per_page=5)
|
||||
items = results.data if hasattr(results, "data") else results
|
||||
return [
|
||||
client.timeout = min(getattr(client, "timeout", 3), 3)
|
||||
client.max_retries = 1
|
||||
semantic = client.semantic_search(query, limit=5)
|
||||
if semantic.get("available") and semantic.get("data"):
|
||||
items = semantic["data"]
|
||||
else:
|
||||
results = client.search_tools(query, per_page=5)
|
||||
items = results.data if hasattr(results, "data") else results
|
||||
found = [
|
||||
{
|
||||
"name": f"{item.get('owner', '')}/{item.get('name', '')}",
|
||||
"desc": (item.get("description") or "")[:50],
|
||||
"args": [],
|
||||
"registry": True,
|
||||
"downloads": item.get("downloads", 0),
|
||||
"relevance": item.get("similarity", item.get("score")),
|
||||
"quality_score": item.get("quality_score"),
|
||||
"quality_coverage": item.get("quality_coverage"),
|
||||
}
|
||||
for item in items
|
||||
]
|
||||
except Exception:
|
||||
_registry_cache[normalized] = found
|
||||
return [dict(item) for item in found]
|
||||
except RegistryError:
|
||||
return []
|
||||
|
||||
|
||||
def _install_registry_selection(tool: dict) -> PickerResult:
|
||||
"""Install a registry result and retain its qualified identity for execution."""
|
||||
from ..resolver import install_from_registry
|
||||
|
||||
install_from_registry(tool["name"])
|
||||
return PickerResult(tool["name"], {})
|
||||
|
||||
|
||||
def _local_selection(tool: dict, arguments: Optional[dict] = None) -> PickerResult:
|
||||
if tool.get("deprecated"):
|
||||
guidance = tool.get("deprecated_message") or "This tool is no longer maintained."
|
||||
if tool.get("replacement"):
|
||||
guidance += f" Use '{tool['replacement']}' instead."
|
||||
_write(f"{YELLOW}Warning: {guidance}{RESET}\n")
|
||||
return PickerResult(tool["name"], arguments or {})
|
||||
|
||||
|
||||
class TTYInput:
|
||||
"""Read from /dev/tty for keyboard input, even when stdin is piped."""
|
||||
|
||||
@@ -135,10 +167,14 @@ class TTYInput:
|
||||
if self.tty:
|
||||
self.tty.close()
|
||||
|
||||
def getch(self):
|
||||
def getch(self, timeout: Optional[float] = None):
|
||||
"""Read a single character."""
|
||||
tty.setraw(self.fd)
|
||||
try:
|
||||
if timeout is not None:
|
||||
ready, _, _ = select.select([self.tty], [], [], timeout)
|
||||
if not ready:
|
||||
return None
|
||||
ch = self.tty.read(1)
|
||||
if ch == '\x1b':
|
||||
# Read escape sequence - bytes arrive together from terminal
|
||||
@@ -164,19 +200,25 @@ def clear_dropdown(n_lines: int):
|
||||
def run_picker(tty_input: TTYInput) -> Optional[PickerResult]:
|
||||
"""Run inline picker."""
|
||||
tools = get_tools()
|
||||
if not tools:
|
||||
_write("No tools. Create one: cmdforge\n")
|
||||
return None
|
||||
|
||||
query = ""
|
||||
selected = 0
|
||||
scroll = 0
|
||||
last_drawn = 0
|
||||
registry_results = {}
|
||||
registry_future = None
|
||||
registry_future_query = None
|
||||
registry_executor = ThreadPoolExecutor(max_workers=1)
|
||||
|
||||
_write(HIDE_CURSOR)
|
||||
|
||||
try:
|
||||
while True:
|
||||
if registry_future is not None and registry_future.done():
|
||||
registry_results[registry_future_query] = registry_future.result()
|
||||
registry_future = None
|
||||
registry_future_query = None
|
||||
|
||||
# Filter
|
||||
matches = []
|
||||
for t in tools:
|
||||
@@ -190,12 +232,17 @@ def run_picker(tty_input: TTYInput) -> Optional[PickerResult]:
|
||||
filtered = [m[0] for m in matches]
|
||||
|
||||
# If no local matches, search registry
|
||||
registry_results = []
|
||||
if query and len(query) >= 2 and not filtered:
|
||||
registry_results = search_registry(query)
|
||||
for rt in registry_results:
|
||||
normalized_query = query.strip().lower()
|
||||
found = registry_results.get(normalized_query, [])
|
||||
if registry_future is None and normalized_query not in registry_results:
|
||||
registry_future_query = normalized_query
|
||||
registry_future = registry_executor.submit(
|
||||
search_registry, query
|
||||
)
|
||||
for rt in found:
|
||||
rt["_registry"] = True
|
||||
filtered = registry_results
|
||||
filtered = found
|
||||
|
||||
if selected >= len(filtered):
|
||||
selected = max(0, len(filtered) - 1)
|
||||
@@ -232,6 +279,12 @@ def run_picker(tty_input: TTYInput) -> Optional[PickerResult]:
|
||||
# Add registry marker
|
||||
if is_registry:
|
||||
prefix += f" {YELLOW}[registry]{RESET}"
|
||||
if t.get("quality_score") is not None:
|
||||
prefix += f" Q{t['quality_score']}"
|
||||
if t.get("quality_coverage") is not None:
|
||||
prefix += f"/{t['quality_coverage']}%"
|
||||
if t.get("relevance") is not None:
|
||||
prefix += f" R{float(t['relevance']):.2f}"
|
||||
# Add deprecation marker
|
||||
if is_deprecated:
|
||||
prefix += f" {YELLOW}[deprecated]{RESET}"
|
||||
@@ -251,7 +304,11 @@ def run_picker(tty_input: TTYInput) -> Optional[PickerResult]:
|
||||
last_drawn = len(lines)
|
||||
|
||||
# Input
|
||||
ch = tty_input.getch()
|
||||
ch = tty_input.getch(
|
||||
timeout=0.1 if registry_future is not None else None
|
||||
)
|
||||
if ch is None:
|
||||
continue
|
||||
|
||||
if ch in ('\r', '\n'): # Enter - run
|
||||
if filtered:
|
||||
@@ -261,29 +318,32 @@ def run_picker(tty_input: TTYInput) -> Optional[PickerResult]:
|
||||
# If it's a registry tool, install it first
|
||||
if selected_tool.get("_registry") or selected_tool.get("registry"):
|
||||
_write(f"{YELLOW}Installing {selected_tool['name']}...{RESET}\n")
|
||||
import subprocess
|
||||
proc = subprocess.run(
|
||||
["cmdforge", "registry", "install", selected_tool["name"]],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
_write(f"Install failed: {proc.stderr}\n")
|
||||
try:
|
||||
return _install_registry_selection(selected_tool)
|
||||
except Exception as exc:
|
||||
_write(f"Install failed: {exc}\n")
|
||||
return None
|
||||
# Extract tool name from "owner/name" format
|
||||
tool_name = selected_tool["name"].split("/")[-1]
|
||||
return PickerResult(tool_name, {})
|
||||
return PickerResult(selected_tool["name"], {})
|
||||
return _local_selection(selected_tool)
|
||||
|
||||
elif ch == '\t': # Tab - configure args or run
|
||||
if filtered:
|
||||
tool = filtered[selected]
|
||||
clear_dropdown(last_drawn)
|
||||
if tool.get("_registry") or tool.get("registry"):
|
||||
_write(f"{YELLOW}Installing {tool['name']}...{RESET}\n")
|
||||
try:
|
||||
_write(SHOW_CURSOR)
|
||||
return _install_registry_selection(tool)
|
||||
except Exception as exc:
|
||||
_write(f"Install failed: {exc}\n")
|
||||
_write(SHOW_CURSOR)
|
||||
return None
|
||||
if tool['args']:
|
||||
args = pick_args(tty_input, tool)
|
||||
_write(SHOW_CURSOR)
|
||||
return PickerResult(tool["name"], args) if args is not None else None
|
||||
return _local_selection(tool, args) if args is not None else None
|
||||
_write(SHOW_CURSOR)
|
||||
return PickerResult(tool["name"], {})
|
||||
return _local_selection(tool)
|
||||
|
||||
elif ch == '\x1b' or ch == '\x03': # Esc or Ctrl+C
|
||||
clear_dropdown(last_drawn)
|
||||
@@ -308,6 +368,8 @@ def run_picker(tty_input: TTYInput) -> Optional[PickerResult]:
|
||||
except Exception:
|
||||
_write(SHOW_CURSOR)
|
||||
raise
|
||||
finally:
|
||||
registry_executor.shutdown(wait=False, cancel_futures=True)
|
||||
|
||||
|
||||
def pick_args(tty_input: TTYInput, tool: dict) -> Optional[dict]:
|
||||
|
||||
Reference in New Issue
Block a user