diff --git a/hub.lua b/hub.lua index ab774c5..27d0475 100644 --- a/hub.lua +++ b/hub.lua @@ -631,6 +631,62 @@ server:tool("remote_search_files", "find-by-pattern on a fleet host.", } } ) +-- ---- Fleet-central local tools (migrated from tools.d/hertz.lua, 2026-07-18) ---- +-- These run LOCALLY on hertz (where the hub process also lives), so no ssh +-- backend hop. Consolidated here so the hub is the single fleet-management +-- endpoint. Still also served by hertz-tools (:8080) for now — remove there +-- once every client (pi-agents etc.) has a @hub session. +local function run_local(cmd, timeout) + local full = timeout and ("timeout " .. tostring(timeout) .. " " .. cmd) or cmd + local p = io.popen(full .. " 2>&1") + if not p then return "Error: popen failed" end + local out = p:read("*a") + p:close() + return out or "" +end + +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_local("python3 /opt/lmcp/helpers/stash_recall.py '" .. q .. "' " .. lim, 30) + end, + { annotations = { + title = "Apropos (fleet memory)", + readOnlyHint = true, + destructiveHint = false, + idempotentHint = true, + openWorldHint = true, + } } +) + +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_local("sudo /root/.local/bin/wake-pve " .. node, 15) + end, + { annotations = { + title = "Wake fleet NUC", + readOnlyHint = false, + destructiveHint = false, + idempotentHint = true, + openWorldHint = true, + } } +) + io.stderr:write(string.format("lmcp-hub starting on port %d with %d backends from %s\n", server.port, (function() local n = 0; for _ in pairs(backends) do n = n + 1 end; return n end)(), CONF_PATH)) server:run()