feat(tui): SGR mouse fallback, focus partition highlights, multi-trigger context menus & box tui
- bin/muse-tui.py: * Parse raw SGR 1006 (\033[<btn;x;yM/m) and Xterm mouse escape sequences in _handle_escape_sequence fallback. * Multi-trigger context menus: Button 3, Button 2, Ctrl/Shift/Alt+Click, double-click, click on active item, or click [⚡] / [sid] target. * Render permanent [⚡] action target across all sidebar thread rows. * Separate focus partitions for FLEET AGENTS and SIDECHATS with partition-specific wheel scrolling and keyboard navigation (j/k, Enter, h/l). * Space key support in NORMAL mode to open context menus. * Active pane highlighting and updated footer hints. - bin/super-cli.py: * Add 'box tui' command dispatching directly to muse-tui.py --mode box. - tests: * Add unit tests in test_context_menus.py, test_focus_highlight.py, and test_main_nav.py (51/51 passing).
This commit is contained in:
+813
-325
File diff suppressed because it is too large
Load Diff
+833
-8
@@ -6,7 +6,9 @@ autonomous browser agents (muse, pip, 646, opm) on bl and interface
|
||||
with the Google Cloud VM web surfaces.
|
||||
|
||||
Domains:
|
||||
super fleet — Node health, CDP status, active tabs, watch, restart
|
||||
super muse-choices — Muse TUI A/B/C auto-answer daemon (on/off/status/logs)
|
||||
super fleet — Node health, CDP status, active tabs, watch, restart, heal
|
||||
super watchdog — Watchdog timers: status, trigger runs
|
||||
super dm — Inter-agent DMs, work orders ([WO]), acks, live log tail
|
||||
super thread — Inspect agent main chats, sidechats, and scrollbacks
|
||||
super job — Manage scheduled jobs, systemd timers, triggers, logs
|
||||
@@ -16,7 +18,8 @@ Domains:
|
||||
super vars — Shortcut to inspect & adjust runtime control variables
|
||||
|
||||
Usage:
|
||||
super fleet [status|watch|restart|cdp]
|
||||
super fleet [status|watch|restart|cdp|heal]
|
||||
super watchdog [status|run <node|relay>]
|
||||
super dm [send|wo|ack|log|tail]
|
||||
super thread [list|view]
|
||||
super job [list|status|run|log|create|enable|disable|delete]
|
||||
@@ -392,7 +395,7 @@ def cmd_fleet_status(args):
|
||||
print(c_dim(" [*] status via host watchdog evidence (local probes blind in this shell)"))
|
||||
if has_any_approval:
|
||||
print("\n" + c_yellow(" ⚠ Agent(s) held up on browser approval. Run 'box approvals' to inspect/allow."))
|
||||
print("\n" + c_dim(" Commands: super fleet watch | super fleet restart <node> | box approvals [check|allow|auto]") + "\n")
|
||||
print("\n" + c_dim(" Commands: super fleet watch | super fleet heal <node> | box approvals [check|allow|auto]") + "\n")
|
||||
|
||||
def cmd_fleet_watch(args):
|
||||
interval = getattr(args, "interval", 2)
|
||||
@@ -447,6 +450,204 @@ def cmd_fleet_cdp(args):
|
||||
print(f" SSH Forward : {c_yellow(f'ssh -L {port}:{peer}:{port} super@100.123.153.75')}")
|
||||
print(f" Local Connect : {c_dim(f'http://127.0.0.1:{port}')} (after forwarding)\n")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Domain: FLEET HEAL + WATCHDOG
|
||||
# ---------------------------------------------------------------------------
|
||||
SYSTEMD_SYSTEM_DIR = Path("/etc/systemd/system")
|
||||
WATCHDOG_LOCK_TMPL = "/tmp/chromebox-watchdog-{node}.lock"
|
||||
WARP_PROBE_URL = "https://1.1.1.1/cdn-cgi/trace"
|
||||
CHROMEBOX_TIMER_TMPL = """[Unit]
|
||||
Description=Run chromebox watchdog for {node} every 2 minutes
|
||||
|
||||
[Timer]
|
||||
RandomizedDelaySec=30s
|
||||
OnBootSec=2min
|
||||
OnUnitActiveSec=2min
|
||||
Unit=chromebox-watchdog@{node}.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
"""
|
||||
|
||||
|
||||
def _sh(cmd, timeout=120, input_text=None):
|
||||
"""Run cmd, capture output. Returns (returncode, combined_output)."""
|
||||
try:
|
||||
r = subprocess.run(cmd, capture_output=True, text=True,
|
||||
timeout=timeout, input=input_text)
|
||||
return r.returncode, ((r.stdout or "") + (r.stderr or "")).strip()
|
||||
except subprocess.TimeoutExpired:
|
||||
return 124, "timed out after %ds: %s" % (timeout, " ".join(cmd))
|
||||
except OSError as e:
|
||||
return 127, str(e)
|
||||
|
||||
|
||||
def _heal_lock_step(node):
|
||||
"""Clear a stale unwritable watchdog lock (blocks watchdog startup)."""
|
||||
lock = Path(WATCHDOG_LOCK_TMPL.format(node=node))
|
||||
if not lock.exists() or os.access(lock, os.W_OK):
|
||||
return True, "lock writable or absent"
|
||||
rc, out = _sh(["sudo", "rm", "-f", str(lock)], timeout=30)
|
||||
if rc == 0:
|
||||
return True, "removed stale lock %s" % lock
|
||||
return False, "cannot remove %s: %s" % (lock, out)
|
||||
|
||||
|
||||
def _heal_timer_step(node):
|
||||
"""Install (if missing) and enable the node's watchdog timer."""
|
||||
timer = "chromebox-watchdog-%s.timer" % node
|
||||
path = SYSTEMD_SYSTEM_DIR / timer
|
||||
if not path.exists():
|
||||
rc, out = _sh(["sudo", "tee", str(path)], timeout=30,
|
||||
input_text=CHROMEBOX_TIMER_TMPL.format(node=node))
|
||||
if rc != 0:
|
||||
return False, "cannot install %s: %s" % (timer, out)
|
||||
rc, out = _sh(["sudo", "systemctl", "daemon-reload"], timeout=60)
|
||||
if rc != 0:
|
||||
return False, "daemon-reload failed: %s" % out
|
||||
rc, out = _sh(["sudo", "systemctl", "enable", "--now", timer], timeout=60)
|
||||
if rc != 0:
|
||||
return False, "enable --now failed: %s" % out
|
||||
return True, "timer installed+active"
|
||||
|
||||
|
||||
def _heal_tunnel_step(node):
|
||||
"""Restart the node's Warp tunnel + CDP relay via netvm-node-up.sh."""
|
||||
rc, out = _sh(["sudo", str(BIN_DIR / "netvm-node-up.sh"), node], timeout=240)
|
||||
last = out.strip().splitlines()[-1] if out.strip() else "(no output)"
|
||||
if rc != 0:
|
||||
return False, "netvm-node-up.sh rc=%d: %s" % (rc, last)
|
||||
if "egress=unknown" in out or "no handshake yet" in out:
|
||||
return False, last
|
||||
return True, last
|
||||
|
||||
|
||||
def _warp_egress_ok(node):
|
||||
"""True when the node's netns reaches the warp probe URL."""
|
||||
rc, out = _sh(["sudo", "ip", "netns", "exec", "warp-%s" % node,
|
||||
"curl", "-sk", "--max-time", "10", WARP_PROBE_URL],
|
||||
timeout=30)
|
||||
if rc != 0:
|
||||
tail = out.splitlines()[-1] if out else "curl failed"
|
||||
return False, tail
|
||||
for line in out.splitlines():
|
||||
if line.startswith("ip="):
|
||||
return True, line
|
||||
return False, "no ip= line in probe response"
|
||||
|
||||
|
||||
def cmd_fleet_heal(args):
|
||||
node = args.node
|
||||
if node not in VALID_NODES:
|
||||
print(f"Error: Unknown node '{node}'. Valid: {', '.join(VALID_NODES)}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
as_json = getattr(args, "json", False)
|
||||
info = get_node_network_info(node)
|
||||
steps = []
|
||||
|
||||
def record(step, ok, detail):
|
||||
steps.append({"step": step, "ok": ok, "detail": detail})
|
||||
if not as_json:
|
||||
mark = c_green("ok") if ok else c_red("FAIL")
|
||||
print(f" [{mark}] {step}: {detail}")
|
||||
|
||||
if not as_json:
|
||||
print(f"\n{c_bold('Healing node ' + node)} ({info['netns']}, {info['peer_ip']}:{info['cdp_port']})")
|
||||
|
||||
record("lock", *_heal_lock_step(node))
|
||||
record("timer", *_heal_timer_step(node))
|
||||
record("tunnel", *_heal_tunnel_step(node))
|
||||
egress_ok, egress_detail = _warp_egress_ok(node)
|
||||
record("egress", egress_ok, egress_detail)
|
||||
relay = probe_cdp_status(info["peer_ip"], info["cdp_port"])
|
||||
if relay["ok"]:
|
||||
record("relay", True, "CDP answers in %sms" % relay["latency_ms"])
|
||||
else:
|
||||
record("relay", False, relay.get("error", "unreachable"))
|
||||
|
||||
verdict = "RECOVERED" if egress_ok else "DOWN"
|
||||
if as_json:
|
||||
print(json.dumps({"ok": egress_ok, "node": node,
|
||||
"verdict": verdict, "steps": steps}, indent=2))
|
||||
else:
|
||||
print(f"\n Verdict: {c_green(verdict) if egress_ok else c_red(verdict)}")
|
||||
if not egress_ok:
|
||||
print(c_dim(" Tunnel still down after restart — if it persists, the Warp identity "
|
||||
f"likely needs regenerating (human: netvm-new-identity.sh {node})"))
|
||||
print()
|
||||
sys.exit(0 if egress_ok else 1)
|
||||
|
||||
|
||||
def _unit_state(unit):
|
||||
"""(enabled, active) booleans for a systemd unit."""
|
||||
rc_e, _ = _sh(["systemctl", "is-enabled", unit], timeout=15)
|
||||
rc_a, _ = _sh(["systemctl", "is-active", unit], timeout=15)
|
||||
return rc_e == 0, rc_a == 0
|
||||
|
||||
|
||||
def cmd_watchdog_status(args):
|
||||
as_json = getattr(args, "json", False)
|
||||
try:
|
||||
import host_evidence
|
||||
ev = host_evidence.collect()
|
||||
except Exception:
|
||||
ev = {}
|
||||
data = []
|
||||
for node in VALID_NODES:
|
||||
timer = "chromebox-watchdog-%s.timer" % node
|
||||
enabled, active = _unit_state(timer)
|
||||
node_ev = ev.get(node, {})
|
||||
data.append({
|
||||
"node": node,
|
||||
"timer": timer,
|
||||
"enabled": enabled,
|
||||
"active": active,
|
||||
"browser": node_ev.get("browser", "unknown"),
|
||||
"browser_detail": node_ev.get("browser_detail", ""),
|
||||
"cdp": node_ev.get("cdp", "unknown"),
|
||||
"cdp_detail": node_ev.get("cdp_detail", ""),
|
||||
})
|
||||
relay_enabled, relay_active = _unit_state("cdp-relay-watchdog.timer")
|
||||
relay = {"timer": "cdp-relay-watchdog.timer",
|
||||
"enabled": relay_enabled, "active": relay_active}
|
||||
if as_json:
|
||||
print(json.dumps({"ok": True, "nodes": data, "relay": relay}, indent=2))
|
||||
return
|
||||
|
||||
print("\n" + c_bold("=== WATCHDOG STATUS ===") + c_dim(f" ({datetime.now().strftime('%H:%M:%S')} local)\n"))
|
||||
headers = ["NODE", "TIMER", "BROWSER", "CDP"]
|
||||
rows = []
|
||||
for item in data:
|
||||
timer = badge_ok("active") if item["active"] else (
|
||||
badge_warn("ENABLED") if item["enabled"] else badge_dim("missing"))
|
||||
rows.append([c_bold(item["node"]), timer,
|
||||
item["browser"], item["cdp"]])
|
||||
print_table(headers, rows)
|
||||
rmark = badge_ok("active") if relay["active"] else badge_dim("missing")
|
||||
print(f" relay: {rmark} {c_dim('cdp-relay-watchdog.timer')}\n")
|
||||
|
||||
|
||||
def cmd_watchdog_run(args):
|
||||
target = args.target
|
||||
as_json = getattr(args, "json", False)
|
||||
if target == "relay":
|
||||
unit = "cdp-relay-watchdog.service"
|
||||
elif target in VALID_NODES:
|
||||
unit = "chromebox-watchdog@%s.service" % target
|
||||
else:
|
||||
print("Error: Specify node or 'relay' (e.g. box watchdog run dev)", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
rc, out = _sh(["sudo", "systemctl", "start", unit], timeout=180)
|
||||
tail = out[-500:] if out else ""
|
||||
if as_json:
|
||||
print(json.dumps({"ok": rc == 0, "unit": unit, "detail": tail}, indent=2))
|
||||
else:
|
||||
if rc == 0:
|
||||
print(c_green(f"Watchdog run triggered: {unit}"))
|
||||
else:
|
||||
print(c_red(f"Failed to start {unit}: {tail}"))
|
||||
sys.exit(0 if rc == 0 else 1)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Domain: APPROVALS
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -729,6 +930,136 @@ def cmd_approvals(args):
|
||||
print(f" Reason: {c_cyan(reason)}")
|
||||
print(c_dim(f" Operator can approve with: box approvals allow {node}"))
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Domain: MUSE-CHOICES (Muse TUI A/B/C auto-answer daemon)
|
||||
# ---------------------------------------------------------------------------
|
||||
def cmd_muse_choices(args):
|
||||
import muse_choice_watcher as mcw
|
||||
action = getattr(args, "mc_action", None) or "status"
|
||||
as_json = getattr(args, "json", False)
|
||||
caller = os.environ.get("BOX_CALLER") or getattr(args, "from_agent", None) or "super"
|
||||
|
||||
if action == "on":
|
||||
dry_run = getattr(args, "dry_run", False)
|
||||
state = mcw.set_enabled(True, dry_run=dry_run, by=caller)
|
||||
res = mcw.reconcile()
|
||||
if as_json:
|
||||
print(json.dumps({"ok": True, "desired": state, "reconcile": res}, indent=2))
|
||||
return
|
||||
mode = "DRY-RUN (logging only)" if dry_run else c_yellow("LIVE (typing A into Muse panes)")
|
||||
print(c_green("\n✔ Muse choice auto-answers ON.") + f" [{mode}]\n")
|
||||
for s in res["started"]:
|
||||
print(f" {badge_ok('STARTED')} {c_cyan(s)}")
|
||||
for s in res["already"]:
|
||||
print(f" {badge_dim('RUNNING')} {s}")
|
||||
for s in res.get("failed") or []:
|
||||
print(f" {badge_err('FAILED')} {s}")
|
||||
if not res["started"] and not res["already"] and not res.get("failed"):
|
||||
print(c_dim(" No Muse panes found on known tmux sockets."))
|
||||
print()
|
||||
|
||||
elif action == "off":
|
||||
state = mcw.set_enabled(False, by=caller)
|
||||
stopped = mcw.stop_all()
|
||||
if as_json:
|
||||
print(json.dumps({"ok": True, "desired": state, "stopped": stopped}, indent=2))
|
||||
return
|
||||
print(c_green("\n✔ Muse choice auto-answers OFF."))
|
||||
for r in stopped:
|
||||
if r.get("status") == "stopped-orphan":
|
||||
label = "(orphan %s:%s)" % (r.get("socket"), r.get("pane"))
|
||||
else:
|
||||
label = r.get("pidfile")
|
||||
print(f" {badge_dim('STOPPED')} {label} (pid {r.get('pid')})")
|
||||
if not stopped:
|
||||
print(c_dim(" No watchers were running."))
|
||||
print()
|
||||
|
||||
elif action == "reconcile":
|
||||
res = mcw.reconcile()
|
||||
if as_json:
|
||||
print(json.dumps({"ok": True, "reconcile": res}, indent=2))
|
||||
return
|
||||
print(f"\n Enabled: {c_bold(str(res['enabled']))} Dry-run: {res['dry_run']}")
|
||||
for s in res["started"]:
|
||||
print(f" {badge_ok('STARTED')} {c_cyan(s)}")
|
||||
for s in res["stopped"]:
|
||||
print(f" {badge_dim('STOPPED')} {s}")
|
||||
for s in res["pruned"]:
|
||||
print(f" {badge_dim('PRUNED')} {s}")
|
||||
for s in res.get("failed") or []:
|
||||
print(f" {badge_err('FAILED')} {s}")
|
||||
if not res["started"] and not res["stopped"] and not res["pruned"] and not res.get("failed"):
|
||||
print(c_dim(" Already in desired state."))
|
||||
print()
|
||||
|
||||
elif action == "logs":
|
||||
sock = getattr(args, "socket", None)
|
||||
pane = getattr(args, "pane", None)
|
||||
n = getattr(args, "n", 20)
|
||||
path = mcw.logfile_for(sock, pane)
|
||||
try:
|
||||
with open(path) as f:
|
||||
lines = f.readlines()
|
||||
except OSError:
|
||||
print(c_red(f"Error: no log at {path}"), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if as_json:
|
||||
recs = []
|
||||
for line in lines[-n:]:
|
||||
try:
|
||||
recs.append(json.loads(line))
|
||||
except Exception:
|
||||
recs.append({"raw": line.rstrip("\n")})
|
||||
print(json.dumps({"ok": True, "log": path, "records": recs}, indent=2))
|
||||
return
|
||||
print(c_bold(f"\n=== {path} (last {n}) ===\n"))
|
||||
for line in lines[-n:]:
|
||||
print(line.rstrip("\n"))
|
||||
print()
|
||||
|
||||
else: # status
|
||||
desired = mcw.get_desired()
|
||||
watchers = mcw.status_all()
|
||||
answers = mcw.recent_answers(5)
|
||||
if as_json:
|
||||
print(json.dumps({"ok": True, "desired": desired,
|
||||
"watchers": watchers,
|
||||
"recent_answers": answers}, indent=2))
|
||||
return
|
||||
state_badge = badge_ok("ON") if desired["enabled"] else badge_dim("OFF")
|
||||
dry = " (dry-run)" if desired.get("dry_run") else ""
|
||||
print("\n" + c_bold("=== MUSE CHOICE AUTO-ANSWERS ===") + f" {state_badge}{dry}")
|
||||
upd = desired.get("updated_at") or "-"
|
||||
by = desired.get("updated_by") or "-"
|
||||
print(c_dim(f" Desired state: enabled={desired['enabled']} dry_run={desired.get('dry_run')} updated={upd} by={by}\n"))
|
||||
if watchers:
|
||||
headers = ["WATCHER", "STATUS", "PID", "LOG BYTES"]
|
||||
rows = []
|
||||
for w in watchers:
|
||||
if w.get("orphan"):
|
||||
badge = badge_warn("ORPHAN")
|
||||
name = "(orphan %s:%s)" % (w.get("socket"), w.get("pane"))
|
||||
else:
|
||||
badge = badge_ok("ALIVE") if w["alive"] else badge_err("DEAD")
|
||||
name = w["pidfile"]
|
||||
rows.append([name, badge, str(w["pid"] or "-"),
|
||||
str(w["log_bytes"] if w["log_bytes"] is not None else "-")])
|
||||
print_table(headers, rows)
|
||||
else:
|
||||
print(c_dim(" No watcher state files."))
|
||||
if answers:
|
||||
print(c_bold("\n Recent answers (box audit feed):"))
|
||||
for a in answers:
|
||||
opts = "; ".join(a.get("options") or [])[:80]
|
||||
kind = a.get("kind") or "-"
|
||||
key = a.get("key") or "-"
|
||||
print(f" • {c_dim(a.get('ts', ''))} {c_bold(a.get('name', ''))} {kind}/{key} sig={a.get('sig')} ok={a.get('ok')}")
|
||||
print(f" {c_dim(opts)}")
|
||||
else:
|
||||
print(c_dim("\n No answers recorded in box audit feed."))
|
||||
print()
|
||||
|
||||
def resolve_sender(args) -> str:
|
||||
explicit = getattr(args, "from_agent", None)
|
||||
if explicit and explicit != DEFAULT_SENDER:
|
||||
@@ -768,6 +1099,359 @@ def sign_message(sender: str, message: str, key_path: Path = None, msg_id: str =
|
||||
signed_wire = f"[from:{sender}] [id:{mid}]\n\n{message}\n\n{sigblock}"
|
||||
return signed_wire, mid
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Domain: INVITE
|
||||
# ---------------------------------------------------------------------------
|
||||
def cmd_invite(args):
|
||||
import invite
|
||||
from invite_handler import salvage_blocked_node
|
||||
action = getattr(args, "invite_action", None) or "status"
|
||||
node = getattr(args, "node", None)
|
||||
as_json = getattr(args, "json", False)
|
||||
|
||||
if action in ("status", "list"):
|
||||
nodes = [node] if node else VALID_NODES
|
||||
res = invite.fleet_invite_status(nodes)
|
||||
if as_json:
|
||||
print(json.dumps({"ok": True, "agents": res}, indent=2))
|
||||
return
|
||||
print("\n" + c_bold("=== FLEET INVITE STATUS ===") + "\n")
|
||||
headers = ["NODE", "CODE", "REDEEMED", "USES LEFT", "USED", "REWARD"]
|
||||
rows = []
|
||||
for n in nodes:
|
||||
it = res.get(n, {})
|
||||
if not it.get("ok"):
|
||||
rows.append([c_bold(n), c_red("ERROR"), "-", "-", "-",
|
||||
(it.get("error") or "")[:40]])
|
||||
continue
|
||||
rows.append([c_bold(n), c_cyan(it.get("code") or "-"),
|
||||
"yes" if it.get("has_redeemed") else c_green("no"),
|
||||
str(it.get("uses_remaining")
|
||||
if it.get("uses_remaining") is not None else "-"),
|
||||
str(it.get("use_count")
|
||||
if it.get("use_count") is not None else "-"),
|
||||
((it.get("reward") or {}).get("title") or "-")[:40]])
|
||||
print_table(headers, rows)
|
||||
print()
|
||||
elif action in ("code", "find"):
|
||||
try:
|
||||
res = invite.get_invite(node)
|
||||
except invite.InviteError as e:
|
||||
print(c_red(f" Error: {e}"), file=sys.stderr)
|
||||
sys.exit(2)
|
||||
if as_json:
|
||||
print(json.dumps(res, indent=2))
|
||||
elif res.get("ok"):
|
||||
print(f" {c_bold(node)} invite code: {c_cyan(res['code'])}")
|
||||
if res.get("uses_remaining") is not None:
|
||||
print(f" Uses remaining: {res['uses_remaining']}")
|
||||
print(f" Source: {res.get('source', 'api')}")
|
||||
else:
|
||||
print(c_red(f" Error: {res.get('error')}"), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif action == "redeem":
|
||||
code = getattr(args, "code", None)
|
||||
try:
|
||||
res = invite.redeem_invite(node, code)
|
||||
except invite.InviteError as e:
|
||||
print(c_red(f" Error: {e}"), file=sys.stderr)
|
||||
sys.exit(2)
|
||||
if as_json:
|
||||
print(json.dumps(res, indent=2))
|
||||
elif res.get("ok"):
|
||||
msg = f" ✔ Redeemed {res['code']} on {node}."
|
||||
if res.get("detail"):
|
||||
msg += f" {res['detail']}"
|
||||
print(c_green(msg))
|
||||
else:
|
||||
print(c_red(f" ✘ Redeem failed on {node}: "
|
||||
f"{res.get('reason') or res.get('error')}"),
|
||||
file=sys.stderr)
|
||||
if res.get("detail"):
|
||||
print(c_dim(f" {res['detail']}"), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif action == "salvage":
|
||||
target = getattr(args, "node", "646") or "646"
|
||||
res = salvage_blocked_node(target, helper_node=getattr(args, "helper", None))
|
||||
if as_json:
|
||||
print(json.dumps(res, indent=2))
|
||||
return
|
||||
print(f"\n{c_bold('=== SALVAGE REPORT FOR ' + target.upper() + ' ===')}")
|
||||
code = res.get("invite_code") or res.get("code_redeemed")
|
||||
print(f" Invite Code to Credit: {c_cyan(str(code))}")
|
||||
if res.get("success"):
|
||||
print(c_green(f" Status: SUCCESS! Redeemed on {res.get('helper_node')}. 1 billion tokens credited to {target}!"))
|
||||
else:
|
||||
diag = res.get("error") or (res.get("redemption_result") or {}).get("detail") or "Redemption failed on helper peer"
|
||||
print(c_yellow(f" Diagnosis: {diag}"))
|
||||
action_hint = res.get("share_instruction") or f"Redeem code '{code}' via onboarding pipeline to grant 1B tokens to {target}."
|
||||
print(f" Action: {action_hint}")
|
||||
print(f" Command: {c_cyan('box onboard start <new_node> --email <client_email> --for ' + target)}")
|
||||
print()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Domain: USAGE
|
||||
# ---------------------------------------------------------------------------
|
||||
def cmd_usage(args):
|
||||
import invite
|
||||
node = getattr(args, "node", None) or getattr(args, "node_pos", None)
|
||||
as_json = getattr(args, "json", False)
|
||||
nodes = [node] if node else VALID_NODES
|
||||
res = invite.fleet_usage(nodes)
|
||||
if as_json:
|
||||
print(json.dumps({"ok": True, "agents": res}, indent=2))
|
||||
return
|
||||
print("\n" + c_bold("=== FLEET USAGE LIMITS ===") + "\n")
|
||||
headers = ["NODE", "WEEKLY RESET", "WEEKLY USED", "ADDITIONAL",
|
||||
"ADD LEFT", "REDEEMED"]
|
||||
rows = []
|
||||
has_blocked = False
|
||||
for n in nodes:
|
||||
it = res.get(n, {})
|
||||
if not it.get("ok"):
|
||||
rows.append([c_bold(n), c_red("ERROR"), "-", "-",
|
||||
(it.get("error") or "")[:30], "-"])
|
||||
continue
|
||||
wu_val = it.get("weekly_used_pct")
|
||||
wu = ("%d%%" % wu_val) if wu_val is not None else "-"
|
||||
au_val = it.get("additional_used_pct")
|
||||
au = ("%d%% used" % au_val) if au_val is not None else "-"
|
||||
add_left = it.get("additional_left") or "-"
|
||||
redeemed_val = it.get("has_redeemed")
|
||||
redeemed_str = "yes" if redeemed_val else (c_green("no") if redeemed_val is False else "-")
|
||||
if (wu_val is not None and wu_val >= 100 and au_val is not None and au_val >= 100) or ("0 tokens left" in add_left):
|
||||
has_blocked = True
|
||||
n_disp = c_red(f"● {n}")
|
||||
else:
|
||||
n_disp = c_bold(n)
|
||||
rows.append([n_disp, it.get("weekly_reset") or "-",
|
||||
wu, "%s / %s" % (it.get("additional_expires") or "-",
|
||||
au),
|
||||
add_left, redeemed_str])
|
||||
print_table(headers, rows)
|
||||
if has_blocked:
|
||||
print("\n" + c_yellow(" ⚠ One or more agents have reached usage limits. Run 'box invite salvage <node>' to resolve.") + "\n")
|
||||
else:
|
||||
print()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Domain: SETTINGS
|
||||
# ---------------------------------------------------------------------------
|
||||
def cmd_settings(args):
|
||||
from settings_rpa import SettingsRPA
|
||||
action = getattr(args, "settings_action", "usage") or "usage"
|
||||
node = getattr(args, "node", "646") or "646"
|
||||
as_json = getattr(args, "json", False)
|
||||
|
||||
with SettingsRPA(node) as rpa:
|
||||
if action == "usage":
|
||||
usage = rpa.read_usage()
|
||||
if as_json:
|
||||
print(json.dumps(usage.to_dict(), indent=2))
|
||||
return
|
||||
status_badge = badge_err("BLOCKED") if usage.is_blocked else badge_ok("ACTIVE")
|
||||
redeemed_badge = c_green("No (Eligible to redeem)") if not usage.has_redeemed else "Yes (Already redeemed)"
|
||||
print(f"\n{c_bold('=== Settings RPA Usage: ' + node + ' ===')}")
|
||||
print(f" Plan: {usage.plan}")
|
||||
print(f" Weekly Limit: {usage.weekly_reset_text} ({c_bold(str(usage.weekly_percent_used) + '%')} used)")
|
||||
print(f" Extra Tokens: {usage.extra_tokens_remaining} ({c_bold(str(usage.extra_percent_used) + '%')} used)")
|
||||
print(f" Has Redeemed: {redeemed_badge}")
|
||||
print(f" Status: {status_badge}\n")
|
||||
elif action == "check-redeem":
|
||||
info = rpa.check_redeem_entrypoint()
|
||||
print(json.dumps(info, indent=2))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Domain: ONBOARD (Agent-driven Onboarding & Invite Salvage Pipeline)
|
||||
# ---------------------------------------------------------------------------
|
||||
def cmd_onboard(args):
|
||||
import onboard_pipeline
|
||||
action = getattr(args, "onboard_action", "start") or "start"
|
||||
as_json = getattr(args, "json", False)
|
||||
|
||||
if action == "start":
|
||||
node = args.node
|
||||
email = args.email
|
||||
for_agent = getattr(args, "for_agent", None)
|
||||
code = getattr(args, "code", None)
|
||||
acct_name = getattr(args, "account_name", None)
|
||||
|
||||
res = onboard_pipeline.start_onboarding(node, email, beneficiary_node=for_agent, invite_code=code, account_name=acct_name)
|
||||
if as_json:
|
||||
print(json.dumps(res, indent=2))
|
||||
return
|
||||
if res.get("ok"):
|
||||
print(f"\n{c_bold('=== ONBOARDING PIPELINE STARTED: ' + node.upper() + ' ===')}")
|
||||
print(f" Target Node: {c_bold(node)}")
|
||||
print(f" Client Email: {email}")
|
||||
print(f" Beneficiary: @{res.get('beneficiary_node')} ({c_cyan(str(res.get('invite_code_queued')))})")
|
||||
print(f" Status: {c_yellow(res.get('status', 'in_progress').upper())}")
|
||||
print(f" Next Step: {res.get('message')}\n")
|
||||
else:
|
||||
print(c_red(f"\n✘ Onboarding failed: {res.get('error')}\n"), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
elif action == "submit-otp":
|
||||
node = args.node
|
||||
otp = args.otp
|
||||
email = getattr(args, "email", None)
|
||||
|
||||
res = onboard_pipeline.submit_onboarding_otp(node, otp, email=email)
|
||||
if as_json:
|
||||
print(json.dumps(res, indent=2))
|
||||
return
|
||||
if res.get("ok"):
|
||||
print(f"\n{c_green('=== ONBOARDING & REDEMPTION COMPLETE ===')}")
|
||||
print(f" Node: {c_bold(node)}")
|
||||
print(f" Beneficiary: @{res.get('beneficiary_node')}")
|
||||
print(f" Token Grant: +1,000,000,000 Muse tokens each")
|
||||
print(f" Message: {c_green(res.get('message'))}\n")
|
||||
else:
|
||||
print(c_red(f"\n✘ OTP submission failed: {res.get('error')}\n"), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
elif action == "status":
|
||||
node = args.node
|
||||
state = onboard_pipeline.OnboardState.load(node)
|
||||
if not state:
|
||||
print(c_red(f"No onboarding record found for node '{node}'"), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if as_json:
|
||||
print(json.dumps(onboard_pipeline.asdict(state), indent=2))
|
||||
return
|
||||
print(f"\n{c_bold('=== ONBOARDING STATUS: @' + state.node.upper() + ' ===')}")
|
||||
print(f" Email: {state.email}")
|
||||
print(f" Current Stage: {c_cyan(state.stage)}")
|
||||
print(f" Beneficiary Agent:@{state.beneficiary_node or '-'} (Code: {c_cyan(str(state.invite_code or '-'))})")
|
||||
if state.detail:
|
||||
print(f" Detail: {state.detail}")
|
||||
if state.redemption_result:
|
||||
rr = state.redemption_result
|
||||
r_st = c_green("SUCCESS") if rr.get("ok") else c_red("FAILED")
|
||||
print(f" Redemption: {r_st}")
|
||||
print()
|
||||
|
||||
elif action == "salvage-wo":
|
||||
node = getattr(args, "node", "646") or "646"
|
||||
target_chat = getattr(args, "target", "646 tasks") or "646 tasks"
|
||||
res = onboard_pipeline.issue_salvage_work_order(node, to_sidechat=target_chat)
|
||||
if as_json:
|
||||
print(json.dumps(res, indent=2))
|
||||
return
|
||||
if res.get("ok"):
|
||||
print(c_green(f"\n✔ Cryptographically signed salvage work order dispatched for @{node} -> {target_chat}\n"))
|
||||
else:
|
||||
print(c_red(f"\n✘ Failed to dispatch work order: {res}\n"), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
elif action == "feed-matrix":
|
||||
matrix = onboard_pipeline.calculate_feeding_weights()
|
||||
if as_json:
|
||||
print(json.dumps({"ok": True, "feed_matrix": matrix}, indent=2))
|
||||
return
|
||||
print("\n" + c_bold("=== FLEET FEEDING & TOKEN ALLOCATION MATRIX ==="))
|
||||
print(c_dim(" (Ranked by: Urgency + Work Done Over Time + Job Amount + Role Criticality)\n"))
|
||||
headers = ["RANK", "AGENT", "ROLE", "FEED SCORE", "STATUS", "JOBS", "WORK DONE", "CODE"]
|
||||
rows = []
|
||||
for idx, r in enumerate(matrix, 1):
|
||||
st = c_red("BLOCKED") if r["is_blocked"] else (c_yellow("LIMIT") if r["status"] == "LIMIT_REACHED" else c_green("ACTIVE"))
|
||||
rows.append([
|
||||
f"#{idx}",
|
||||
c_bold(r["node"]),
|
||||
r["role"],
|
||||
c_cyan(str(r["feeding_weight"]) + " pts"),
|
||||
st,
|
||||
str(r["jobs_count"]),
|
||||
f"{r['work_done_msgs']} msgs",
|
||||
c_bold(r["code"]),
|
||||
])
|
||||
print_table(headers, rows)
|
||||
print()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Domain: CHROMEBOX (agent browser settings-menu toggles)
|
||||
# ---------------------------------------------------------------------------
|
||||
def cmd_chromebox(args):
|
||||
from hatch_menu import toggles as menu_toggles
|
||||
action = getattr(args, "chrome_action", None)
|
||||
if action != "permissions":
|
||||
print(c_red(" Error: expected 'permissions' (box chromebox "
|
||||
"permissions <node> list|get|set|describe)"),
|
||||
file=sys.stderr)
|
||||
sys.exit(2)
|
||||
node = getattr(args, "node", None)
|
||||
as_json = getattr(args, "json", False)
|
||||
perm_action = getattr(args, "perm_action", None) or "list"
|
||||
|
||||
if perm_action == "list":
|
||||
tab = getattr(args, "tab", None)
|
||||
try:
|
||||
res = menu_toggles.list_toggles(node, tab=tab)
|
||||
except menu_toggles.MenuError as e:
|
||||
print(c_red(f" Error: {e}"), file=sys.stderr)
|
||||
sys.exit(2)
|
||||
if as_json:
|
||||
print(json.dumps(res, indent=2))
|
||||
return
|
||||
if not res.get("ok"):
|
||||
print(c_red(f" Error: {res.get('error')}"), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
print("\n" + c_bold(f"=== CHROMEBOX TOGGLES: {node} ===") + "\n")
|
||||
rows = []
|
||||
for name in sorted(res.get("toggles", {})):
|
||||
val = res["toggles"][name]
|
||||
rows.append([name, c_red("-") if val is None else str(val)])
|
||||
print_table(["TOGGLE", "VALUE"], rows)
|
||||
print()
|
||||
elif perm_action == "get":
|
||||
name = getattr(args, "toggle", None)
|
||||
try:
|
||||
res = menu_toggles.get_toggle(node, name)
|
||||
except menu_toggles.MenuError as e:
|
||||
print(c_red(f" Error: {e}"), file=sys.stderr)
|
||||
sys.exit(2)
|
||||
if as_json:
|
||||
print(json.dumps(res, indent=2))
|
||||
elif res.get("ok"):
|
||||
print(f" {c_bold(name)} = {c_cyan(res['value'])}")
|
||||
else:
|
||||
print(c_red(f" Error: {res.get('error')}"), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif perm_action == "set":
|
||||
name = getattr(args, "toggle", None)
|
||||
value = getattr(args, "value", None)
|
||||
try:
|
||||
res = menu_toggles.set_toggle(node, name, value)
|
||||
except menu_toggles.MenuError as e:
|
||||
print(c_red(f" Error: {e}"), file=sys.stderr)
|
||||
sys.exit(2)
|
||||
if as_json:
|
||||
print(json.dumps(res, indent=2))
|
||||
elif res.get("ok"):
|
||||
print(c_green(f" ✔ {name} = {res['value']} on {node} "
|
||||
f"(readback verified)."))
|
||||
else:
|
||||
print(c_red(f" ✘ Set failed on {node}: {res.get('error')}"),
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif perm_action == "describe":
|
||||
tab = getattr(args, "tab", None)
|
||||
try:
|
||||
res = menu_toggles.describe_tab(node, tab)
|
||||
except menu_toggles.MenuError as e:
|
||||
print(c_red(f" Error: {e}"), file=sys.stderr)
|
||||
sys.exit(2)
|
||||
print(json.dumps(res, indent=2))
|
||||
else:
|
||||
print(c_red(f" Error: unknown action {perm_action!r}"),
|
||||
file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Domain: DM (Direct Messaging & Work Orders)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -4275,11 +4959,16 @@ def build_parser():
|
||||
subparsers = parser.add_subparsers(dest="domain", help="Orchestration Domain")
|
||||
|
||||
# Domain: FLEET
|
||||
p_fleet = subparsers.add_parser("fleet", parents=[common], help="Node health, CDP status, active tabs, watch, restart")
|
||||
p_fleet.add_argument("action", nargs="?", default="status", choices=["status", "watch", "restart", "cdp"])
|
||||
p_fleet.add_argument("node", nargs="?", default=None, help="Target node (for restart / cdp)")
|
||||
p_fleet = subparsers.add_parser("fleet", parents=[common], help="Node health, CDP status, active tabs, watch, restart, heal")
|
||||
p_fleet.add_argument("action", nargs="?", default="status", choices=["status", "watch", "restart", "cdp", "heal"])
|
||||
p_fleet.add_argument("node", nargs="?", default=None, help="Target node (for restart / cdp / heal)")
|
||||
p_fleet.add_argument("--interval", type=int, default=2, help="Watch refresh interval in seconds")
|
||||
|
||||
# Domain: WATCHDOG
|
||||
p_watchdog = subparsers.add_parser("watchdog", parents=[common], help="Watchdog timers: status, trigger runs")
|
||||
p_watchdog.add_argument("action", nargs="?", default="status", choices=["status", "run"])
|
||||
p_watchdog.add_argument("target", nargs="?", default=None, help="Target node (or 'relay') for run")
|
||||
|
||||
# Domain: APPROVALS
|
||||
p_approvals = subparsers.add_parser("approvals", parents=[common], help="Inspect and handle agent browser & gateway approvals")
|
||||
p_approval = subparsers.add_parser("approval", parents=[common], help="Alias for 'approvals'")
|
||||
@@ -4343,6 +5032,107 @@ def build_parser():
|
||||
p_app_req_key.add_argument("node", choices=VALID_NODES, help="Target node requesting key")
|
||||
p_app_req_key.add_argument("--reason", default="Passkey authentication required", help="Reason for key request")
|
||||
|
||||
# Domain: MUSE-CHOICES
|
||||
p_mc = subparsers.add_parser("muse-choices", parents=[common], help="Muse TUI A/B/C choice auto-answer daemon (on/off/status/logs)")
|
||||
mc_sub = p_mc.add_subparsers(dest="mc_action")
|
||||
|
||||
p_mc_on = mc_sub.add_parser("on", parents=[common], help="Enable auto-answers on all Muse panes")
|
||||
p_mc_on.add_argument("--dry-run", action="store_true", help="Log answers instead of sending keys")
|
||||
|
||||
p_mc_off = mc_sub.add_parser("off", parents=[common], help="Disable auto-answers and stop all watchers")
|
||||
|
||||
p_mc_status = mc_sub.add_parser("status", parents=[common], help="Show desired state, watchers, recent answers")
|
||||
|
||||
p_mc_logs = mc_sub.add_parser("logs", parents=[common], help="Tail one pane watcher log")
|
||||
p_mc_logs.add_argument("--socket", required=True, help="Tmux socket path (e.g. /tmp/tmux-1000/default)")
|
||||
p_mc_logs.add_argument("--pane", required=True, help="Pane id (e.g. %%37)")
|
||||
p_mc_logs.add_argument("-n", type=int, default=20, help="Lines to show (default: 20)")
|
||||
|
||||
p_mc_rec = mc_sub.add_parser("reconcile", parents=[common], help="Enforce desired state now (start missing / stop excess)")
|
||||
|
||||
# Domain: INVITE
|
||||
p_invite = subparsers.add_parser("invite", parents=[common], help="Muse.ai invite codes: find per-agent codes and redeem")
|
||||
p_invite.add_argument("--node", choices=VALID_NODES, default=None, help="Filter by node (status)")
|
||||
inv_sub = p_invite.add_subparsers(dest="invite_action")
|
||||
|
||||
p_inv_status = inv_sub.add_parser("status", parents=[common], help="Invite code matrix across fleet (default)")
|
||||
p_inv_status.add_argument("--node", choices=VALID_NODES, default=None, help="Filter by node")
|
||||
|
||||
p_inv_list = inv_sub.add_parser("list", parents=[common], help="Alias for 'status'")
|
||||
p_inv_list.add_argument("--node", choices=VALID_NODES, default=None, help="Filter by node")
|
||||
|
||||
p_inv_code = inv_sub.add_parser("code", parents=[common], help="Show one agent's invite code")
|
||||
p_inv_code.add_argument("node", choices=VALID_NODES, help="Target node")
|
||||
|
||||
p_inv_find = inv_sub.add_parser("find", parents=[common], help="Alias for 'code'")
|
||||
p_inv_find.add_argument("node", choices=VALID_NODES, help="Target node")
|
||||
|
||||
p_inv_redeem = inv_sub.add_parser("redeem", parents=[common], help="Redeem an invite code on a node")
|
||||
p_inv_redeem.add_argument("node", choices=VALID_NODES, help="Target node")
|
||||
p_inv_redeem.add_argument("code", help="6-char invite code")
|
||||
|
||||
p_inv_salvage = inv_sub.add_parser("salvage", parents=[common], help="Salvage an out-of-tokens agent (defaults to 646)")
|
||||
p_inv_salvage.add_argument("node", nargs="?", default="646", choices=VALID_NODES, help="Blocked agent node (default: 646)")
|
||||
p_inv_salvage.add_argument("--helper", choices=VALID_NODES, help="Specific helper agent to redeem code")
|
||||
|
||||
# Domain: USAGE
|
||||
p_usage = subparsers.add_parser("usage", parents=[common], help="Muse.ai usage limits per agent")
|
||||
p_usage.add_argument("--node", choices=VALID_NODES, default=None, help="Filter by node")
|
||||
p_usage.add_argument("node_pos", nargs="?", default=None, choices=VALID_NODES, help="Optional node positional argument")
|
||||
|
||||
# Domain: SETTINGS
|
||||
p_settings = subparsers.add_parser("settings", parents=[common], help="Settings menu RPA automation")
|
||||
settings_sub = p_settings.add_subparsers(dest="settings_action")
|
||||
p_set_usage = settings_sub.add_parser("usage", parents=[common], help="Extract usage via Settings RPA")
|
||||
p_set_usage.add_argument("node", choices=VALID_NODES, default="646", nargs="?", help="Target node (default: 646)")
|
||||
p_set_check = settings_sub.add_parser("check-redeem", parents=[common], help="Check if Redeem Invite Code is in General Settings")
|
||||
p_set_check.add_argument("node", choices=VALID_NODES, default="646", nargs="?", help="Target node (default: 646)")
|
||||
|
||||
# Domain: ONBOARD
|
||||
p_onboard = subparsers.add_parser("onboard", parents=[common], help="Agent onboarding & automated invite code salvage pipeline")
|
||||
onboard_sub = p_onboard.add_subparsers(dest="onboard_action")
|
||||
|
||||
p_onb_start = onboard_sub.add_parser("start", parents=[common], help="Start onboarding pipeline (provisions infra + initiates auth)")
|
||||
p_onb_start.add_argument("node", help="Target new node label (e.g. dev2, client1)")
|
||||
p_onb_start.add_argument("--email", required=True, help="Client login email")
|
||||
p_onb_start.add_argument("--for", dest="for_agent", choices=VALID_NODES, help="Beneficiary agent to unblock (defaults to most urgent)")
|
||||
p_onb_start.add_argument("--code", help="Explicit 6-character invite code to redeem")
|
||||
p_onb_start.add_argument("--account-name", help="Display name hint for multi-account selection")
|
||||
|
||||
p_onb_otp = onboard_sub.add_parser("submit-otp", parents=[common], help="Submit transient OTP and complete auto-redemption")
|
||||
p_onb_otp.add_argument("node", help="Target node label")
|
||||
p_onb_otp.add_argument("otp", help="6-digit verification code")
|
||||
p_onb_otp.add_argument("--email", help="Client email (optional)")
|
||||
|
||||
p_onb_stat = onboard_sub.add_parser("status", parents=[common], help="Query onboarding pipeline state for a node")
|
||||
p_onb_stat.add_argument("node", help="Target node label")
|
||||
|
||||
p_onb_wo = onboard_sub.add_parser("salvage-wo", parents=[common], help="Dispatch cryptographically signed salvage work order to sidechat")
|
||||
p_onb_wo.add_argument("node", nargs="?", default="646", choices=VALID_NODES, help="Blocked agent node (default: 646)")
|
||||
p_onb_wo.add_argument("--target", default="646 tasks", help="Target sidechat (default: 646 tasks)")
|
||||
|
||||
onboard_sub.add_parser("feed-matrix", parents=[common], help="Display all agents ranked by feeding weight, job volume, role, and work done over time")
|
||||
|
||||
# Domain: CHROMEBOX
|
||||
p_chromebox = subparsers.add_parser("chromebox", parents=[common], help="Agent browser settings-menu toggles (chromebox RPA)")
|
||||
chrome_sub = p_chromebox.add_subparsers(dest="chrome_action")
|
||||
p_chrome_perm = chrome_sub.add_parser("permissions", parents=[common], help="Settings menu toggles (permissions + related tabs)")
|
||||
p_chrome_perm.add_argument("node", choices=VALID_NODES, help="Target node")
|
||||
perm_sub = p_chrome_perm.add_subparsers(dest="perm_action")
|
||||
|
||||
p_perm_list = perm_sub.add_parser("list", parents=[common], help="List toggle states (default)")
|
||||
p_perm_list.add_argument("--tab", default=None, help="Only this settings tab")
|
||||
|
||||
p_perm_get = perm_sub.add_parser("get", parents=[common], help="Read one toggle")
|
||||
p_perm_get.add_argument("toggle", help="Toggle address (e.g. permissions.web_access, permissions.websites:example.com)")
|
||||
|
||||
p_perm_set = perm_sub.add_parser("set", parents=[common], help="Set one toggle (verified with readback)")
|
||||
p_perm_set.add_argument("toggle", help="Toggle address")
|
||||
p_perm_set.add_argument("value", help="Value (on/off, Allow/Ask/Deny, auto_allow/always_ask, theme name)")
|
||||
|
||||
p_perm_desc = perm_sub.add_parser("describe", parents=[common], help="Full inventory of one settings tab (JSON)")
|
||||
p_perm_desc.add_argument("tab", help="Settings tab name (e.g. Permissions)")
|
||||
|
||||
# Domain: DM
|
||||
p_dm = subparsers.add_parser("dm", parents=[common], help="Inter-agent DMs, work orders ([WO]), acks, live log tail")
|
||||
dm_sub = p_dm.add_subparsers(dest="action")
|
||||
@@ -4817,11 +5607,20 @@ def build_parser():
|
||||
p_docs = subparsers.add_parser("docs", aliases=["doc"], parents=[common], help="Agent lookups, surfaces for box.muse-dev.online, sentence grammar & regex")
|
||||
p_docs.add_argument("docs_args", nargs=argparse.REMAINDER, help="Arguments passed directly to docs-lookup.py")
|
||||
|
||||
# Domain: TUI (Interactive full-screen Muse TUI & Box fleet console)
|
||||
p_tui = subparsers.add_parser("tui", parents=[common], help="Interactive full-screen Muse TUI & Box fleet console")
|
||||
p_tui.add_argument("tui_args", nargs=argparse.REMAINDER, help="Arguments passed directly to muse-tui.py")
|
||||
|
||||
return parser
|
||||
|
||||
def main():
|
||||
if len(sys.argv) > 1:
|
||||
if sys.argv[1] == "tmux":
|
||||
if sys.argv[1] == "tui":
|
||||
tui_args = sys.argv[2:]
|
||||
cmd = [sys.executable, str(BIN_DIR / "muse-tui.py"), "--mode", "box"] + tui_args
|
||||
res = subprocess.run(cmd)
|
||||
sys.exit(res.returncode)
|
||||
elif sys.argv[1] == "tmux":
|
||||
cmd = [sys.executable, str(BIN_DIR / "muse-tmux.py")] + (sys.argv[2:] or ["list"])
|
||||
res = subprocess.run(cmd)
|
||||
sys.exit(res.returncode)
|
||||
@@ -4891,6 +5690,20 @@ def main():
|
||||
print("Error: Specify node (e.g. super fleet cdp muse)", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
cmd_fleet_cdp(args)
|
||||
elif act == "heal":
|
||||
if not getattr(args, "node", None):
|
||||
print("Error: Specify node to heal (e.g. super fleet heal dev)", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
cmd_fleet_heal(args)
|
||||
elif args.domain == "watchdog":
|
||||
act = getattr(args, "action", "status") or "status"
|
||||
if act == "status":
|
||||
cmd_watchdog_status(args)
|
||||
elif act == "run":
|
||||
if not getattr(args, "target", None):
|
||||
print("Error: Specify node or 'relay' (e.g. box watchdog run dev)", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
cmd_watchdog_run(args)
|
||||
elif args.domain == "dm":
|
||||
act = getattr(args, "action", None)
|
||||
if not act or act == "log":
|
||||
@@ -5083,8 +5896,20 @@ def main():
|
||||
cmd_swarm_prune(args)
|
||||
else:
|
||||
p_swarm.print_help()
|
||||
elif args.domain in ("approvals", "approval"):
|
||||
elif args.domain in ("approvals", "approval", "blocked"):
|
||||
cmd_approvals(args)
|
||||
elif args.domain == "muse-choices":
|
||||
cmd_muse_choices(args)
|
||||
elif args.domain == "invite":
|
||||
cmd_invite(args)
|
||||
elif args.domain == "usage":
|
||||
cmd_usage(args)
|
||||
elif args.domain == "settings":
|
||||
cmd_settings(args)
|
||||
elif args.domain == "onboard":
|
||||
cmd_onboard(args)
|
||||
elif args.domain == "chromebox":
|
||||
cmd_chromebox(args)
|
||||
elif args.domain in ("deploy", "subagent"):
|
||||
if args.domain == "subagent":
|
||||
args.action = "subagent"
|
||||
|
||||
Reference in New Issue
Block a user