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:
+42
-4
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user