feat: unified fleet CLI, Main Chat preservation policy, sidechat routing, and file transfers

- Added CHAT_POLICY.md and README.md banner enforcing sidechat-first and file-transfer-first rules.
- Added strict Main Chat block to super dm send and super dm wo with --allow-main-chat override.
- Implemented file transfer staging and metadata registry in super dm send-file and super dm files (with clean subcommand).
- Added full job lifecycle management (show, create, enable, disable, delete, run --follow) to super-cli.py and box-ctl.py.
- Audited all jobs in jobs/*.json and redirected automated dispatches away from Main Chat.
- Hardened chromebox-watchdog.sh with systemd user session environment exports and stale singleton cleanup.
- Added compose_check command choice to muse-chat-api.py.
This commit is contained in:
operator
2026-10-04 16:34:25 +00:00
parent 0b7c75739b
commit 7a35b684c4
12 changed files with 608 additions and 141 deletions
+42 -4
View File
@@ -178,12 +178,31 @@ def cmd_send(ws, message):
def cmd_messages(ws, n=5, width=200):
# Check approvals first (non-blocking)
check_approvals(ws)
# Exclude the compose box subtree: a failed send leaves the draft text
# (including the [id:...] tag) in the composer, and scraping it would
# produce a false "verified" (2026-10-04 dm.py false-confirmation bug).
result = ev(ws, f"""(() => {{
const ps = [...document.querySelectorAll('p')].slice(-{n*2}).map(p=>p.innerText.slice(0,{width}));
const composer = document.querySelector('[contenteditable="true"]') ||
document.querySelector('textarea[placeholder*="Message"]');
const ps = [...document.querySelectorAll('p')]
.filter(p => !(composer && composer.contains(p)))
.slice(-{n*2}).map(p=>p.innerText.slice(0,{width}));
return ps.join('\\n---\\n');
}})()""")
print(result)
def cmd_compose_check(ws):
"""Print the current compose-box text (empty string if clear).
Used by dm.py to confirm a send actually left the composer."""
result = ev(ws, """(() => {
const input = document.querySelector('[contenteditable="true"]') ||
document.querySelector('textarea[placeholder*="Message"]') ||
[...document.querySelectorAll('div[role="textbox"]')][0];
if (!input) return 'NOCOMPOSE';
return input.innerText || '';
})()""")
print(result if result is not None else '')
def cmd_wait(ws, timeout=30):
print(f"Waiting {timeout}s for response...")
# Check approvals periodically during wait
@@ -197,8 +216,25 @@ def cmd_wait(ws, timeout=30):
cmd_messages(ws, 2)
def cmd_sidechat_use(ws, chat_id):
"""Open sidebar, click the box with the chat name."""
import base64
"""Open a sidechat by name (sidebar text search) or by thread UUID
(direct navigation). The sidebar shows titles, not UUIDs, so the
text search below can never match a UUID."""
import base64, re
cid = chat_id.strip()
if re.fullmatch(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", cid.lower()):
url = "https://muse.ai/thread/" + cid.lower()
want_uuid = cid.lower()
# Use ev1 (skips CDP chatter) and confirm the URL actually changed.
# A stale read here used to report the previous thread's URL (2026-10-04).
cur = None
for _try in range(3):
ev1(ws, "window.location.href=" + json.dumps(url), True)
time.sleep(4)
cur = ev1(ws, "window.location.href", True)
if cur and want_uuid in cur:
break
print(f"Navigated to: {cur}")
return
b64 = base64.b64encode(chat_id.encode()).decode()
js = (
"(async()=>{"
@@ -488,7 +524,7 @@ def main():
p = argparse.ArgumentParser()
p.add_argument('--account', required=True, choices=list(ACCOUNTS.keys()),
help='Agent name (matches node, profile, ACCOUNTS.md)')
p.add_argument('command', choices=['send', 'messages', 'wait', 'approvals', 'sidechat', 'upload', 'url'])
p.add_argument('command', choices=['send', 'messages', 'wait', 'approvals', 'sidechat', 'upload', 'url', 'compose_check'])
p.add_argument('arg', nargs='*', default=[])
p.add_argument('--dry-run', action='store_true',
help='upload: stage attachment without sending')
@@ -523,6 +559,8 @@ def main():
n = int(args.arg[0]) if args.arg else 5
w = int(args.arg[1]) if len(args.arg) > 1 else 200
cmd_messages(ws, n, w)
elif args.command == 'compose_check':
cmd_compose_check(ws)
elif args.command == 'wait':
t = int(args.arg[0]) if args.arg else 30
cmd_wait(ws, t)