app
Session Monitor
Public Made by Adomby adom
Drive your Claude Code sessions from your phone — every container's sessions in one board. Talk to any of them by voice or text, pick the model per session, and get a push when a session asks a question or is ready for you. Install this CLI to hook a container in and manage its keeper.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
#!/usr/bin/env python3
"""session-monitor — the Session Monitor CLI.
Manage THIS container's keeper and query the fleet. The keeper is a long-running
daemon; this CLI is how you install/start/stop/inspect it. (The
/adom-remote-control skill just *guides* you to these commands — the CLI does the
work.)
session-monitor update pull latest + restart the keeper onto it (one-shot)
session-monitor keeper start [--token T] [--service URL] install + supervise the keeper here
session-monitor keeper stop stop this container's keeper
session-monitor keeper status local process + code version + reporting (flags STALE)
session-monitor keeper logs [-n N] tail the keeper log
session-monitor status keeper-fleet health (all your containers)
session-monitor open print the dashboard URL (with your token)
session-monitor token mint <email> mint a token (operator; on the service container)
session-monitor version
Add --json to any command for a machine-readable envelope.
"""
import os, sys, json, socket, subprocess, urllib.request
HOME = os.path.expanduser("~")
KEEP_DIR = os.path.join(HOME, ".session-keeper")
CLI_HOME = os.path.join(HOME, ".session-monitor-cli") # where the pkg install hook drops the runtime
SKILL_DIR = os.path.join(HOME, ".claude", "skills", "adom-remote-control") # legacy bundled location (fallback)
ENV_FILE = os.path.join(KEEP_DIR, "env")
# realpath so a symlinked command (~/.local/bin/session-monitor) resolves to its
# real install dir, where keeper.py / setup.sh live.
SELF_DIR = os.path.dirname(os.path.realpath(__file__))
DEFAULT_SERVICE = "https://session-monitor-21nypoktx513.adom.cloud"
VERSION = "0.8.3" # keep in lockstep with package.json "version" AND keeper.py KEEPER_VERSION
def _skill_file(*names):
"""Find a bundled runtime file (keeper installer / stop.sh / keeper.py) — next to
this CLI, in the CLI home, or the legacy skill dir. Accepts alternate names (the
keeper installer is `setup.sh` in the package, `install.sh` in the source tree)."""
for d in (SELF_DIR, CLI_HOME, SKILL_DIR):
for name in names:
p = os.path.join(d, name)
if os.path.exists(p):
return p
return os.path.join(CLI_HOME, names[0])
def _env():
d = {}
try:
for ln in open(ENV_FILE):
ln = ln.strip()
if "=" in ln and not ln.startswith("#"):
k, v = ln.split("=", 1)
d[k] = v.strip().strip("'\"")
except Exception:
pass
return d
def _service():
return (_env().get("SM_SERVICE_URL") or os.environ.get("SM_SERVICE_URL") or DEFAULT_SERVICE).rstrip("/")
def _token():
return _env().get("SM_TOKEN") or os.environ.get("SM_TOKEN") or ""
def _machine():
return socket.gethostname()
def _flag(args, name):
if name in args:
i = args.index(name)
if i + 1 < len(args):
return args[i + 1]
return None
def _pgrep(pat):
try:
return subprocess.run(["pgrep", "-f", pat], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL).returncode == 0
except Exception:
return False
def _running_keeper_version():
try:
return open(os.path.join(KEEP_DIR, "keeper.version")).read().strip() or None
except Exception:
return None # missing → a pre-0.3 (unversioned) keeper is running
def _pkg_keeper_version():
"""KEEPER_VERSION baked into the INSTALLED package keeper.py — the code a restart
would swap in. Parsed textually so we never import the keeper."""
try:
for ln in open(_skill_file("keeper.py")):
ln = ln.strip()
if ln.startswith("KEEPER_VERSION"):
return ln.split("=", 1)[1].strip().strip("'\"")
except Exception:
pass
return None
def _keeper_stale():
"""(stale, running_ver, pkg_ver): True iff a keeper is RUNNING but its code is
older than the installed package (or predates versioning). This is the signal
that used to be invisible — a container reporting fine on ancient keeper code."""
run, pkg = _running_keeper_version(), _pkg_keeper_version()
stale = _pgrep(f"{KEEP_DIR}/keeper.py") and bool(pkg) and run != pkg
return (stale, run, pkg)
def _api(path):
req = urllib.request.Request(_service() + path,
headers={"X-SM-Token": _token(), "User-Agent": "session-monitor-cli/" + VERSION})
with urllib.request.urlopen(req, timeout=15) as r:
return json.loads(r.read())
def _out(status, msg, data=None, hints=None):
"""AI-oriented output: a human line + Hint: lines + optional JSON envelope."""
print(f"{status}: {msg}")
for h in (hints or []):
print(f"Hint: {h}")
if "--json" in sys.argv:
print(json.dumps({"status": status, "message": msg, "data": data or {}, "hints": hints or []}))
return 0 if status == "OK" else 1
def cmd_keeper(args):
sub = args[0] if args else "status"
rest = args[1:]
if sub == "start":
setup = _skill_file("setup.sh", "install.sh")
token = _flag(rest, "--token") or _token()
service = _flag(rest, "--service") or _service()
if not os.path.exists(setup):
return _out("ERROR", "keeper setup script not found",
hints=["Reinstall the CLI: adom-wiki pkg install adom/session-monitor-cli"])
if not token:
return _out("ERROR", "no token given",
hints=["Pass --token <T> (ask whoever runs the monitor), or set SM_TOKEN"])
subprocess.run(["bash", setup, service, token])
return _out("OK", f"keeper started → {service}", hints=["Verify: session-monitor keeper status"])
if sub == "stop":
stop = _skill_file("stop.sh")
if not os.path.exists(stop):
return _out("ERROR", "stop script not found")
subprocess.run(["bash", stop])
return _out("OK", "keeper stopped on this container")
if sub == "logs":
n = _flag(rest, "-n") or "40"
log = os.path.join(KEEP_DIR, "keeper.log")
if not os.path.exists(log):
return _out("ERROR", "no keeper log yet", hints=["Start it: session-monitor keeper start --token <T>"])
subprocess.run(["tail", "-n", str(n), log])
return 0
# default: status
daemon = _pgrep(f"{KEEP_DIR}/daemon.sh")
keeper = _pgrep(f"{KEEP_DIR}/keeper.py")
stale, run_ver, pkg_ver = _keeper_stale()
svc = None
try:
svc = _api(f"/api/keeper-status?machine={_machine()}")
except Exception as e:
svc = {"error": str(e)}
line = f"local daemon={'up' if daemon else 'DOWN'} keeper={'up' if keeper else 'DOWN'}"
if keeper:
line += f" (code {run_ver or 'pre-0.3 unversioned'})"
if svc and not svc.get("error"):
line += (f" | service {'reporting' if svc.get('reporting') else 'NOT reporting'}"
f" (last seen {svc.get('age_sec')}s ago, {svc.get('sessions', 0)} sessions)")
running = daemon and keeper
data = {"daemon": daemon, "keeper": keeper, "keeper_version": run_ver,
"package_version": pkg_ver, "stale": stale, "service": svc}
if not running:
return _out("ERROR", line, data=data,
hints=["Revive it: session-monitor keeper start --token <T>"])
if stale:
# Running fine but on OLD code — the exact case that used to read as "healthy"
# while a container ran a dismally outdated keeper. Report it as NOT-OK so no
# agent can declare victory here. Refreshing is a safe restart of a process
# that is ALREADY egressing — it is NOT new data egress.
return _out("ERROR", line + f" ** STALE keeper: running {run_ver or 'pre-0.3'}, {pkg_ver} installed **",
data=data,
hints=["Refresh the running keeper (safe restart, NOT new egress): session-monitor update",
" (or: session-monitor keeper start — reuses the saved token)"])
return _out("OK", line, data=data)
def cmd_status(args):
try:
d = _api("/api/keeper-health")
except Exception as e:
return _out("ERROR", f"can't reach the monitor service: {e}",
hints=["Check your token: session-monitor open"])
return _out("OK", f"fleet: {d['reporting']} reporting · {d['down']} down · "
f"{d['containers']} containers · {d['owners']} owner(s)", data=d)
def cmd_open(args):
tok = _token()
if not tok:
return _out("ERROR", "no token set here", hints=["Run: session-monitor keeper start --token <T> first"])
return _out("OK", f"{_service()}/?token={tok}",
hints=["Open on your phone; Safari → Add to Home Screen for a PWA (mic permission sticks)."])
def cmd_token(args):
if len(args) < 2 or args[0] != "mint":
return _out("ERROR", "usage: session-monitor token mint <email>")
email = args[1]
add = os.path.join(HOME, "adom-session-monitor", "service", "add-user.sh")
if not os.path.exists(add):
return _out("ERROR", "token minting runs on the SERVICE container (add-user.sh not found here)",
hints=["SSH to the service container and run: service/add-user.sh " + email])
subprocess.run(["bash", add, email])
return _out("OK", f"minted a token for {email}")
def cmd_update(args):
"""One-shot update: pull the latest package, then restart the running keeper
onto it. Safe to re-run. On an already-hooked container it reuses the saved
token, and the restart is a safe cycle of a process that is ALREADY reporting —
NOT new data egress — so it never needs fresh consent."""
steps = []
try:
r = subprocess.run(["adom-wiki", "pkg", "install", "adom/session-monitor-cli"],
capture_output=True, text=True)
steps.append("pkg install " + ("ok" if r.returncode == 0 else "FAILED"))
except FileNotFoundError:
steps.append("pkg install skipped (adom-wiki not on PATH)")
setup = _skill_file("setup.sh", "install.sh")
token, service = _token(), _service()
if not os.path.exists(setup):
return _out("ERROR", "keeper setup script not found",
hints=["Reinstall: adom-wiki pkg install adom/session-monitor-cli"])
if not token:
return _out("ERROR", "no saved token — this container isn't hooked in yet",
hints=["First hookup: session-monitor keeper start --token <T>"])
subprocess.run(["bash", setup, service, token]) # copies the new keeper.py + clean restart
stale, run_ver, pkg_ver = _keeper_stale()
steps.append(f"keeper restarted (code {run_ver or '?'})")
ok = not stale
return _out("OK" if ok else "ERROR", "update: " + " · ".join(steps),
data={"stale": stale, "keeper_version": run_ver, "package_version": pkg_ver},
hints=[] if ok else ["still stale — see: session-monitor keeper logs -n 40"])
def main():
argv = [a for a in sys.argv[1:] if a != "--json"]
cmd = argv[0] if argv else "help"
rest = argv[1:]
if cmd in ("version", "-v", "--version"):
return _out("OK", "session-monitor " + VERSION)
if cmd in ("help", "-h", "--help"):
print(__doc__)
return 0
dispatch = {"keeper": cmd_keeper, "status": cmd_status, "open": cmd_open,
"token": cmd_token, "update": cmd_update}
fn = dispatch.get(cmd)
if not fn:
return _out("ERROR", f"unknown command: {cmd}", hints=["Run: session-monitor help"])
return fn(rest)
if __name__ == "__main__":
sys.exit(main())