From 8554c73ba040ad415deefa8f77f6297e535c09bf Mon Sep 17 00:00:00 2001 From: chrome-storm-c442 Date: Tue, 24 Feb 2026 07:02:38 -0500 Subject: [PATCH] fix: word-boundary fuzzy alias search, _resolve_alias in remove_server - "tor" now matches "API TOR contabo..." but NOT "investor" - Whole-word match first, substring fallback second - remove_server() now uses _resolve_alias() for consistency Co-Authored-By: Claude Opus 4.6 --- tools/ssh.py | 47 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/tools/ssh.py b/tools/ssh.py index 6ee076a..508474e 100644 --- a/tools/ssh.py +++ b/tools/ssh.py @@ -250,12 +250,39 @@ def list_servers(full=False): print(f"{alias:<20} {stype:<10} {has_key:<6} {notes}") +def _resolve_alias(alias: str, servers: dict) -> str: + """Resolve alias — exact match, then whole-word search, then substring fallback.""" + if alias in servers: + return alias + query = alias.lower() + # 1) Whole-word match (e.g. "tor" matches "API TOR contabo" but NOT "investor") + import re + word_re = re.compile(r'\b' + re.escape(query) + r'\b', re.IGNORECASE) + word_matches = [a for a in servers if word_re.search(a)] + if len(word_matches) == 1: + return word_matches[0] + if len(word_matches) > 1: + print(f"Ambiguous: '{alias}' matches multiple servers:") + for m in word_matches: + print(f" - {m}") + sys.exit(1) + # 2) Substring fallback (e.g. "cont" matches "contabo") + sub_matches = [a for a in servers if query in a.lower()] + if len(sub_matches) == 1: + return sub_matches[0] + if len(sub_matches) > 1: + print(f"Ambiguous: '{alias}' matches multiple servers:") + for m in sub_matches: + print(f" - {m}") + sys.exit(1) + print(f"Unknown: '{alias}'. Available: {', '.join(servers.keys())}") + sys.exit(1) + + def server_info(alias: str): """Show server info safe for AI context — NO ip, user, password, port, totp_secret.""" _, servers = load_servers() - if alias not in servers: - print(f"Unknown: {alias}. Available: {', '.join(servers.keys())}") - sys.exit(1) + alias = _resolve_alias(alias, servers) s = servers[alias] has_key = "yes" if os.path.exists(SSH_KEY_PATH) else "no" print(f"Alias: {s['alias']}") @@ -318,9 +345,7 @@ def add_server(args): def set_note(alias: str, note: str): """Update server notes — safe for AI (no credentials exposed).""" data, servers = load_servers() - if alias not in servers: - print(f"Unknown: {alias}. Available: {', '.join(servers.keys())}") - sys.exit(1) + alias = _resolve_alias(alias, servers) for s in data["servers"]: if s["alias"] == alias: s["notes"] = note @@ -331,9 +356,7 @@ def set_note(alias: str, note: str): def remove_server(alias: str): data, servers = load_servers() - if alias not in servers: - print(f"ERROR: Unknown '{alias}'") - sys.exit(1) + alias = _resolve_alias(alias, servers) data["servers"] = [s for s in data["servers"] if s["alias"] != alias] save_servers(data) remove_from_ssh_config(alias) @@ -395,12 +418,10 @@ def main(): if cmd == "--remove" and len(sys.argv) >= 3: remove_server(sys.argv[2]); sys.exit(0) - # Server commands + # Server commands — exact match first, then fuzzy search by keyword alias = cmd _, servers = load_servers() - if alias not in servers: - print(f"Unknown: {alias}. Available: {', '.join(servers.keys())}") - sys.exit(1) + alias = _resolve_alias(alias, servers) server = servers[alias] if len(sys.argv) < 3: