sync: auto-sync from GURU-5070 at 2026-06-15 11:20:33
Author: Mike Swanson Machine: GURU-5070 Timestamp: 2026-06-15 11:20:33
This commit is contained in:
138
.claude/scripts/rmm-search.py
Normal file
138
.claude/scripts/rmm-search.py
Normal file
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python3
|
||||
"""rmm-search engine. Reads the GuruRMM agents JSON array on stdin; reads
|
||||
QUERY/CLIENT/ONLINE/JSON/LISTC/LIMIT from the environment. Flexible, forgiving
|
||||
multi-field search — see rmm-search.sh for usage. Kept as a sidecar (not a
|
||||
heredoc) because the agents payload is too large to pass as a CLI argument."""
|
||||
import sys, os, json, re
|
||||
from datetime import datetime, timezone
|
||||
|
||||
raw = sys.stdin.read()
|
||||
agents = json.loads(raw) if raw.strip() else []
|
||||
query = os.environ.get("QUERY", "").strip()
|
||||
client = os.environ.get("CLIENT", "").strip()
|
||||
online_only = os.environ.get("ONLINE") == "1"
|
||||
as_json = os.environ.get("JSON") == "1"
|
||||
list_clients = os.environ.get("LISTC") == "1"
|
||||
try:
|
||||
limit = int(os.environ.get("LIMIT", "0") or 0)
|
||||
except ValueError:
|
||||
limit = 0
|
||||
|
||||
|
||||
def norm(s):
|
||||
return re.sub(r'[^a-z0-9]', '', (s or '').lower())
|
||||
|
||||
|
||||
def is_subseq(t, v):
|
||||
it = iter(v)
|
||||
return all(c in it for c in t)
|
||||
|
||||
|
||||
def field_score(val, term, allow_subseq):
|
||||
"""How well a single query word matches one normalized field value."""
|
||||
if not val or not term:
|
||||
return 0
|
||||
if val == term:
|
||||
return 100
|
||||
if val.startswith(term):
|
||||
return 80
|
||||
if term in val:
|
||||
return 60
|
||||
if allow_subseq and len(term) >= 3 and is_subseq(term, val):
|
||||
return 25
|
||||
return 0
|
||||
|
||||
|
||||
# field -> (key, weight, allow_subsequence)
|
||||
FIELDS = [
|
||||
('hostname', 1.00, True),
|
||||
('client_name', 0.75, False),
|
||||
('site_name', 0.60, False),
|
||||
('os_type', 0.55, False),
|
||||
('id', 0.40, False),
|
||||
]
|
||||
|
||||
|
||||
def fresh(last_seen):
|
||||
if not last_seen:
|
||||
return False
|
||||
try:
|
||||
dt = datetime.fromisoformat(last_seen.replace('Z', '+00:00'))
|
||||
return (datetime.now(timezone.utc) - dt).total_seconds() < 300
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
if list_clients:
|
||||
for n in sorted({(a.get('client_name') or 'Unassigned') for a in agents}):
|
||||
print(n)
|
||||
sys.exit(0)
|
||||
|
||||
# --- explicit hard client scope (normalized; "valleywide" -> "Valley Wide Plastering") ---
|
||||
if client:
|
||||
cn = norm(client)
|
||||
matched = sorted({(a.get('client_name') or '') for a in agents
|
||||
if cn and cn in norm(a.get('client_name'))} - {''})
|
||||
if not matched:
|
||||
print(f"[ERROR] no client matches '{client}'. Try: rmm-search.sh --list-clients", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
if len(matched) > 1:
|
||||
exact = [m for m in matched if norm(m) == cn]
|
||||
if len(exact) == 1:
|
||||
matched = exact
|
||||
else:
|
||||
print(f"[AMBIGUOUS] '{client}' matches {len(matched)} clients - narrow --client:", file=sys.stderr)
|
||||
for m in matched:
|
||||
print(" - " + m, file=sys.stderr)
|
||||
sys.exit(3)
|
||||
agents = [a for a in agents if (a.get('client_name') or '') == matched[0]]
|
||||
|
||||
if online_only:
|
||||
agents = [a for a in agents if fresh(a.get('last_seen'))]
|
||||
|
||||
terms = [t for t in (norm(x) for x in query.split()) if t]
|
||||
joined = norm(query)
|
||||
|
||||
|
||||
def score_agent(a):
|
||||
if not terms:
|
||||
return 1 # no query (e.g. "all machines for client X")
|
||||
nf = [(norm(a.get(k)), w, sub) for k, w, sub in FIELDS]
|
||||
total = 0.0
|
||||
for term in terms:
|
||||
best = max(field_score(val, term, sub) * w for val, w, sub in nf)
|
||||
if best == 0:
|
||||
return 0 # AND: every word must hit some field
|
||||
total += best
|
||||
if joined and joined in norm(a.get('hostname')):
|
||||
total += 60 # whole query lands in hostname -> clearly THE machine
|
||||
return total
|
||||
|
||||
|
||||
results = sorted(((score_agent(a), a) for a in agents),
|
||||
key=lambda t: (-t[0], (t[1].get('hostname') or '').lower()))
|
||||
results = [(s, a) for s, a in results if s > 0]
|
||||
if limit > 0:
|
||||
results = results[:limit]
|
||||
|
||||
if as_json:
|
||||
print(json.dumps([{
|
||||
'hostname': a.get('hostname'), 'id': a.get('id'), 'client': a.get('client_name'),
|
||||
'site': a.get('site_name'), 'os': a.get('os_type'),
|
||||
'online': fresh(a.get('last_seen')), 'last_seen': a.get('last_seen'), 'score': round(s, 1),
|
||||
} for s, a in results], indent=2))
|
||||
sys.exit(0)
|
||||
|
||||
scope = f" in '{(agents[0].get('client_name') if (client and agents) else client)}'" if client else ""
|
||||
if not results:
|
||||
print(f"No machines match '{query}'{scope}. (try fewer/looser words, or --list-clients)")
|
||||
sys.exit(0)
|
||||
|
||||
hdr = f"{'HOSTNAME':<22} {'CLIENT':<26} {'SITE':<16} {'OS':<8} {'STAT':<7} ID"
|
||||
print(f"{len(results)} match(es) for '{query}'{scope} (best first):")
|
||||
print(hdr)
|
||||
print("-" * len(hdr))
|
||||
for s, a in results:
|
||||
st = 'online' if fresh(a.get('last_seen')) else 'offline'
|
||||
print(f"{(a.get('hostname') or '')[:21]:<22} {(a.get('client_name') or '?')[:25]:<26} "
|
||||
f"{(a.get('site_name') or '')[:15]:<16} {(a.get('os_type') or '')[:7]:<8} {st:<7} {a.get('id')}")
|
||||
Reference in New Issue
Block a user