Files
agent-skills/scripts/aoe-register-remote.py
T
naps62 7cb03f8796 feat: ship the remaining hooks and aoe-register-remote
settings.json references git-autoupdate, tmux-attention, tmux-reset and
scripts/aoe-register-remote.py, but they only existed as untracked files
on one machine, so hooks failed everywhere else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 08:56:44 +00:00

148 lines
4.9 KiB
Python
Executable File

#!/usr/bin/env python3
"""Register `claude remote-control` sessions (desktop/Android) as AoE rows.
Hook mode (SessionStart, stdin JSON): registers the current session if a
`claude remote-control` process is an ancestor.
Backfill mode (--backfill): scans ~/.claude/projects for transcripts whose cwd
sits under a `.claude/worktrees/` dir -- process ancestry is gone by then, so
path shape is the only signal left.
Rows are created UNLAUNCHED and pinned to the conversation. Starting one runs
`claude --resume <id>`, which appends to the SAME transcript the phone is using:
only take over once the remote-control child is idle/dead, else two writers
interleave the jsonl.
"""
import json
import os
import re
import subprocess
import sys
from pathlib import Path
SESSIONS_JSON = Path.home() / ".config/agent-of-empires/profiles/default/sessions.json"
PROJECTS_DIR = Path.home() / ".claude/projects"
GROUP = "remote"
# terminal -> `claude --resume` on start (clean takeover, no live view while phone drives)
# structured -> transcript replay under `aoe serve` (live view, ACP takeover)
ROW_MODE = os.environ.get("AOE_REMOTE_ROW_MODE", "terminal")
def known_agent_session_ids():
try:
rows = json.loads(SESSIONS_JSON.read_text())
except (OSError, ValueError):
return set()
# import-created rows carry agent_session_id; `set-session-id` writes
# resume_intent.value and only becomes agent_session_id once aoe runs it
ids = {r.get("agent_session_id") for r in rows}
ids |= {(r.get("resume_intent") or {}).get("value") for r in rows}
return {i for i in ids if i}
def has_remote_control_ancestor(pid=None):
pid = pid or os.getppid()
for _ in range(20):
try:
cmdline = Path(f"/proc/{pid}/cmdline").read_bytes().decode(errors="replace")
status = Path(f"/proc/{pid}/status").read_text()
except OSError:
return False
if "remote-control" in cmdline:
return True
m = re.search(r"^PPid:\s*(\d+)", status, re.M)
if not m or m.group(1) == "0" or m.group(1) == "1":
return False
pid = int(m.group(1))
return False
def title_for(cwd, session_id):
# remote-control worktrees are all named `bridge-cse_<task-id>`, so the dir
# name carries no signal -- name rows after the repo instead
cwd = str(cwd)
marker = "/.claude/worktrees/"
name = Path(cwd.split(marker)[0]).name if marker in cwd else Path(cwd).name
return f"{name}-{session_id.split('-')[0]}"
def register(cwd, session_id, mode=ROW_MODE):
if session_id in known_agent_session_ids():
return False
if not Path(cwd).is_dir():
return False
if mode == "structured":
subprocess.run(
["aoe", "session", "import", "--structured", "--group", GROUP, "-y", cwd],
check=False, capture_output=True, text=True,
)
return True
out = subprocess.run(
["aoe", "add", cwd, "--title", title_for(cwd, session_id), "--group", GROUP],
check=False, capture_output=True, text=True,
).stdout
m = re.search(r"^\s*ID:\s*(\w+)", out, re.M)
if not m:
return False
subprocess.run(
["aoe", "session", "set-session-id", m.group(1), session_id],
check=False, capture_output=True, text=True,
)
return True
def transcript_meta(path):
try:
with path.open() as f:
for line in f:
try:
rec = json.loads(line)
except ValueError:
continue
sid, cwd = rec.get("sessionId"), rec.get("cwd")
if sid and cwd:
return sid, cwd
except OSError:
pass
return None, None
def backfill(dry_run=False):
known = known_agent_session_ids()
seen, made = set(), []
for jsonl in sorted(PROJECTS_DIR.glob("*/*.jsonl"), key=lambda p: p.stat().st_mtime):
sid, cwd = transcript_meta(jsonl)
if not sid or sid in known or sid in seen:
continue
if "/.claude/worktrees/" not in cwd or not Path(cwd).is_dir():
continue
seen.add(sid)
if dry_run:
made.append(f"would add {title_for(cwd, sid)} {cwd}")
elif register(cwd, sid):
made.append(f"added {title_for(cwd, sid)} {cwd}")
print("\n".join(made) if made else "nothing to register")
def main():
if "--backfill" in sys.argv:
backfill(dry_run="--dry-run" in sys.argv)
return
if os.environ.get("AOE_INSTANCE_ID"): # already an AoE-launched session
return
try:
payload = json.loads(sys.stdin.read() or "{}")
except ValueError:
return
sid, cwd = payload.get("session_id"), payload.get("cwd") or os.getcwd()
if not sid or not has_remote_control_ancestor():
return
register(cwd, sid)
if __name__ == "__main__":
try:
main()
except Exception:
pass # a hook must never block session start