diff --git a/helpers/stash_recall.py b/helpers/stash_recall.py new file mode 100644 index 0000000..8d406e1 --- /dev/null +++ b/helpers/stash_recall.py @@ -0,0 +1,29 @@ +import urllib.request, json, threading, queue, sys +BASE="http://192.168.88.184:8080" +q=queue.Queue() +def sse(): + try: + r=urllib.request.urlopen(BASE+"/sse", timeout=30) + for raw in r: + s=raw.decode(errors="replace").strip() + if s.startswith("data:"): q.put(s[5:].strip()) + except Exception as e: q.put("ERR:"+str(e)) +threading.Thread(target=sse,daemon=True).start() +try: + ep=q.get(timeout=10) + if ep.startswith("ERR:"): print("stash unreachable:",ep); sys.exit(1) + purl=BASE+ep if ep.startswith("/") else ep + def post(o): + urllib.request.urlopen(urllib.request.Request(purl,data=json.dumps(o).encode(),headers={"Content-Type":"application/json"}),timeout=15).read() + post({"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"apropos","version":"1"}}}) + q.get(timeout=10) + post({"jsonrpc":"2.0","method":"notifications/initialized"}) + query=sys.argv[1] if len(sys.argv)>1 else "" + limit=int(sys.argv[2]) if len(sys.argv)>2 else 3 + post({"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"recall","arguments":{"query":query,"limit":limit}}}) + d=json.loads(q.get(timeout=25)) + txt=d.get("result",{}).get("content",[{}])[0].get("text","[]") + facts=json.loads(txt) + if not facts: print("(no memory found for: %s)"%query); sys.exit(0) + for f in facts: print("- %s (score %.2f)" % (f.get("content","").strip(), f.get("score",0))) +except Exception as e: print("recall error:",e); sys.exit(1) diff --git a/tools.d/hertz.lua b/tools.d/hertz.lua index 1953c21..5a81d09 100644 --- a/tools.d/hertz.lua +++ b/tools.d/hertz.lua @@ -254,3 +254,39 @@ server:tool("mediagrab", return run("incus exec doppler -- python3 /opt/mediagrab/mediagrab.py " .. a.command, 120) end) + +-- ---- Fleet wake (pipi: wake-only, no power-off) ---- +server:tool("wake_fleet", + "Wake a fleet NUC (pve1..pve4) via Fritz!Box Wake-on-LAN. Powers a node ON only; it cannot power anything off. Node boots in ~30-60s.", + { + type = "object", + properties = { + node = { type = "string", description = "Node to wake: '1'..'4' or 'pve1'..'pve4'" }, + }, + required = { "node" }, + }, + function(a) + local node = tostring(a.node or ""):gsub("[^%w]", "") + if not node:match("^p?v?e?[1-4]$") then + return "Error: node must be 1-4 or pve1-pve4 (got: " .. tostring(a.node) .. ")" + end + return run("sudo /root/.local/bin/wake-pve " .. node .. " 2>&1", 15) + end) + +-- ---- apropos: lean facade for stash recall (read-only memory) ---- +server:tool("apropos", + "Search shared fleet memory (stash) for facts about the fleet, projects, decisions, and preferences. query = 2-6 words on the topic; limit = max results (default 3). Read-only.", + { + type = "object", + properties = { + query = { type = "string", description = "2-6 words describing what to recall" }, + limit = { type = "integer", description = "max results, default 3" }, + }, + required = { "query" }, + }, + function(a) + local q = tostring(a.query or ""):gsub("[^%w%s%-%.]", " "):gsub("%s+", " ") + if q:gsub("%s","") == "" then return "Error: query required" end + local lim = tonumber(a.limit) or 3 + return run("python3 /opt/lmcp/helpers/stash_recall.py '" .. q .. "' " .. lim, 30) + end)