#!/usr/bin/env python3 """ btmsg — Group Agent Messenger for BTerminal Mission Control. Hierarchical communication system for multi-agent orchestration. Agents communicate through messages stored in SQLite. Agent identity set via BTMSG_AGENT_ID environment variable. Usage: btmsg [args] Commands: inbox Show unread messages (--all for all) read Read message and mark as read send Send message to agent (must be in contacts) reply Reply to a message contacts List allowed contacts for current agent history Show conversation history with agent status Show all agents and their status register [--tier N] [--model M] Register an agent allow Add contact permission whoami Show current agent identity graph Visual hierarchy with status dots and links """ import sqlite3 import sys import os import uuid import json from pathlib import Path from datetime import datetime, timezone DB_PATH = Path.home() / ".local" / "share" / "bterminal" / "btmsg.db" # Agent roles and their tiers ROLES = { 'admin': 0, 'manager': 1, 'architect': 1, 'tester': 1, 'reviewer': 1, 'project': 2, } # Colors for terminal output C_RESET = "\033[0m" C_BOLD = "\033[1m" C_DIM = "\033[2m" C_RED = "\033[31m" C_GREEN = "\033[32m" C_YELLOW = "\033[33m" C_BLUE = "\033[34m" C_MAGENTA = "\033[35m" C_CYAN = "\033[36m" ROLE_COLORS = { 'admin': C_CYAN, 'manager': C_MAGENTA, 'architect': C_BLUE, 'tester': C_GREEN, 'reviewer': C_CYAN, 'project': C_YELLOW, } def get_db(): DB_PATH.parent.mkdir(parents=True, exist_ok=True) db = sqlite3.connect(str(DB_PATH)) db.row_factory = sqlite3.Row db.execute("PRAGMA journal_mode=WAL") return db def init_db(): db = get_db() db.executescript(""" CREATE TABLE IF NOT EXISTS agents ( id TEXT PRIMARY KEY, name TEXT NOT NULL, role TEXT NOT NULL, group_id TEXT NOT NULL, tier INTEGER NOT NULL DEFAULT 2, model TEXT, cwd TEXT, system_prompt TEXT, status TEXT DEFAULT 'stopped', last_active_at TEXT, created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS contacts ( agent_id TEXT NOT NULL, contact_id TEXT NOT NULL, PRIMARY KEY (agent_id, contact_id), FOREIGN KEY (agent_id) REFERENCES agents(id), FOREIGN KEY (contact_id) REFERENCES agents(id) ); CREATE TABLE IF NOT EXISTS messages ( id TEXT PRIMARY KEY, from_agent TEXT NOT NULL, to_agent TEXT NOT NULL, content TEXT NOT NULL, read INTEGER DEFAULT 0, reply_to TEXT, group_id TEXT NOT NULL, created_at TEXT DEFAULT (datetime('now')), FOREIGN KEY (from_agent) REFERENCES agents(id), FOREIGN KEY (to_agent) REFERENCES agents(id) ); CREATE INDEX IF NOT EXISTS idx_messages_to ON messages(to_agent, read); CREATE INDEX IF NOT EXISTS idx_messages_from ON messages(from_agent); CREATE INDEX IF NOT EXISTS idx_messages_group ON messages(group_id); CREATE INDEX IF NOT EXISTS idx_messages_reply ON messages(reply_to); CREATE TABLE IF NOT EXISTS channels ( id TEXT PRIMARY KEY, name TEXT NOT NULL, group_id TEXT NOT NULL, created_by TEXT NOT NULL, created_at TEXT DEFAULT (datetime('now')), FOREIGN KEY (created_by) REFERENCES agents(id) ); CREATE TABLE IF NOT EXISTS channel_members ( channel_id TEXT NOT NULL, agent_id TEXT NOT NULL, joined_at TEXT DEFAULT (datetime('now')), PRIMARY KEY (channel_id, agent_id), FOREIGN KEY (channel_id) REFERENCES channels(id), FOREIGN KEY (agent_id) REFERENCES agents(id) ); CREATE TABLE IF NOT EXISTS channel_messages ( id TEXT PRIMARY KEY, channel_id TEXT NOT NULL, from_agent TEXT NOT NULL, content TEXT NOT NULL, created_at TEXT DEFAULT (datetime('now')), FOREIGN KEY (channel_id) REFERENCES channels(id), FOREIGN KEY (from_agent) REFERENCES agents(id) ); CREATE INDEX IF NOT EXISTS idx_channel_messages ON channel_messages(channel_id, created_at); """) db.commit() db.close() def get_agent_id(): """Get current agent ID from environment.""" agent_id = os.environ.get("BTMSG_AGENT_ID") if not agent_id: print(f"{C_RED}Error: BTMSG_AGENT_ID not set.{C_RESET}") print(f"{C_DIM}Set it with: export BTMSG_AGENT_ID={C_RESET}") sys.exit(1) return agent_id def get_agent(db, agent_id): """Get agent record, or None.""" row = db.execute("SELECT * FROM agents WHERE id = ?", (agent_id,)).fetchone() return row def can_message(db, from_id, to_id): """Check if from_agent is allowed to message to_agent.""" # Admin (tier 0) can message anyone sender = get_agent(db, from_id) if sender and sender['tier'] == 0: return True row = db.execute( "SELECT 1 FROM contacts WHERE agent_id = ? AND contact_id = ?", (from_id, to_id) ).fetchone() return row is not None def format_time(ts_str): """Format timestamp for display.""" if not ts_str: return "?" try: dt = datetime.fromisoformat(ts_str) return dt.strftime("%H:%M:%S") except (ValueError, TypeError): return ts_str[:19] def format_role(role): """Colorize role name.""" color = ROLE_COLORS.get(role, C_RESET) return f"{color}{role}{C_RESET}" def short_id(msg_id): """Show first 8 chars of message ID.""" return msg_id[:8] if msg_id else "?" # ─── Commands ──────────────────────────────────────────────── def cmd_inbox(args): """Show unread messages (or all with --all).""" agent_id = get_agent_id() show_all = "--all" in args db = get_db() agent = get_agent(db, agent_id) if not agent: print(f"{C_RED}Agent '{agent_id}' not registered.{C_RESET}") return if show_all: rows = db.execute( "SELECT m.*, a.name as sender_name, a.role as sender_role " "FROM messages m JOIN agents a ON m.from_agent = a.id " "WHERE m.to_agent = ? ORDER BY m.created_at DESC LIMIT 50", (agent_id,) ).fetchall() else: rows = db.execute( "SELECT m.*, a.name as sender_name, a.role as sender_role " "FROM messages m JOIN agents a ON m.from_agent = a.id " "WHERE m.to_agent = ? AND m.read = 0 ORDER BY m.created_at ASC", (agent_id,) ).fetchall() if not rows: label = "messages" if show_all else "unread messages" print(f"{C_DIM}No {label}.{C_RESET}") return label = "All messages" if show_all else f"Unread messages ({len(rows)})" print(f"\n{C_BOLD}📬 {label} for {agent['name']}:{C_RESET}\n") for row in rows: read_mark = " " if row['read'] else f"{C_GREEN}●{C_RESET}" role_str = format_role(row['sender_role']) time_str = format_time(row['created_at']) reply_str = f" {C_DIM}↩ {short_id(row['reply_to'])}{C_RESET}" if row['reply_to'] else "" print(f" {read_mark} [{short_id(row['id'])}] {C_DIM}{time_str}{C_RESET} " f"{C_BOLD}{row['sender_name']}{C_RESET} ({role_str}){reply_str}") # Show first 120 chars of content preview = row['content'][:120].replace('\n', ' ') if len(row['content']) > 120: preview += "..." print(f" {preview}\n") db.close() def cmd_read(args): """Read a specific message and mark as read.""" if not args: print(f"{C_RED}Usage: btmsg read {C_RESET}") return agent_id = get_agent_id() msg_id_prefix = args[0] db = get_db() # Support short IDs (prefix match) row = db.execute( "SELECT m.*, a.name as sender_name, a.role as sender_role " "FROM messages m JOIN agents a ON m.from_agent = a.id " "WHERE m.id LIKE ? AND m.to_agent = ?", (msg_id_prefix + "%", agent_id) ).fetchone() if not row: print(f"{C_RED}Message not found.{C_RESET}") db.close() return role_str = format_role(row['sender_role']) time_str = format_time(row['created_at']) print(f"\n{C_BOLD}{'─' * 60}{C_RESET}") print(f" {C_DIM}ID:{C_RESET} {row['id']}") print(f" {C_DIM}From:{C_RESET} {C_BOLD}{row['sender_name']}{C_RESET} ({role_str})") print(f" {C_DIM}Time:{C_RESET} {time_str}") if row['reply_to']: print(f" {C_DIM}Re:{C_RESET} {short_id(row['reply_to'])}") print(f"{C_BOLD}{'─' * 60}{C_RESET}\n") print(f" {row['content']}\n") print(f"{C_BOLD}{'─' * 60}{C_RESET}") print(f"{C_DIM}Reply: btmsg reply {short_id(row['id'])} \"your message\"{C_RESET}\n") # Mark as read db.execute("UPDATE messages SET read = 1 WHERE id = ?", (row['id'],)) db.commit() db.close() def cmd_send(args): """Send a message to an agent.""" if len(args) < 2: print(f"{C_RED}Usage: btmsg send {C_RESET}") return agent_id = get_agent_id() to_id = args[0] content = " ".join(args[1:]) db = get_db() # Verify sender exists sender = get_agent(db, agent_id) if not sender: print(f"{C_RED}Agent '{agent_id}' not registered.{C_RESET}") db.close() return # Verify recipient exists (support prefix match) recipient = get_agent(db, to_id) if not recipient: # Try prefix match row = db.execute("SELECT * FROM agents WHERE id LIKE ?", (to_id + "%",)).fetchone() if row: recipient = row to_id = row['id'] else: print(f"{C_RED}Agent '{to_id}' not found.{C_RESET}") db.close() return # Check contact permission if not can_message(db, agent_id, to_id): print(f"{C_RED}Not allowed to message '{recipient['name']}' ({recipient['role']}).{C_RESET}") print(f"{C_DIM}Check your contacts: btmsg contacts{C_RESET}") db.close() return msg_id = str(uuid.uuid4()) db.execute( "INSERT INTO messages (id, from_agent, to_agent, content, group_id) " "VALUES (?, ?, ?, ?, ?)", (msg_id, agent_id, to_id, content, sender['group_id']) ) db.commit() db.close() print(f"{C_GREEN}✓ Sent to {recipient['name']} ({recipient['role']}){C_RESET} [{short_id(msg_id)}]") def cmd_reply(args): """Reply to a message.""" if len(args) < 2: print(f"{C_RED}Usage: btmsg reply {C_RESET}") return agent_id = get_agent_id() msg_id_prefix = args[0] content = " ".join(args[1:]) db = get_db() # Find original message (prefix match) original = db.execute( "SELECT * FROM messages WHERE id LIKE ? AND to_agent = ?", (msg_id_prefix + "%", agent_id) ).fetchone() if not original: print(f"{C_RED}Message not found.{C_RESET}") db.close() return # Reply goes back to sender sender = get_agent(db, agent_id) if not sender: print(f"{C_RED}Agent '{agent_id}' not registered.{C_RESET}") db.close() return to_id = original['from_agent'] # Check contact permission if not can_message(db, agent_id, to_id): recipient = get_agent(db, to_id) rname = recipient['name'] if recipient else to_id print(f"{C_RED}Not allowed to reply to '{rname}'.{C_RESET}") db.close() return reply_id = str(uuid.uuid4()) db.execute( "INSERT INTO messages (id, from_agent, to_agent, content, reply_to, group_id) " "VALUES (?, ?, ?, ?, ?, ?)", (reply_id, agent_id, to_id, content, original['id'], sender['group_id']) ) db.commit() recipient = get_agent(db, to_id) rname = recipient['name'] if recipient else to_id print(f"{C_GREEN}✓ Replied to {rname}{C_RESET} [{short_id(reply_id)}]") db.close() def cmd_contacts(args): """List allowed contacts for current agent.""" agent_id = get_agent_id() db = get_db() agent = get_agent(db, agent_id) if not agent: print(f"{C_RED}Agent '{agent_id}' not registered.{C_RESET}") db.close() return rows = db.execute( "SELECT a.* FROM contacts c JOIN agents a ON c.contact_id = a.id " "WHERE c.agent_id = ? ORDER BY a.tier, a.role, a.name", (agent_id,) ).fetchall() print(f"\n{C_BOLD}📋 Contacts for {agent['name']} ({format_role(agent['role'])}):{C_RESET}\n") if not rows: print(f" {C_DIM}No contacts configured.{C_RESET}") db.close() return current_tier = None for row in rows: if row['tier'] != current_tier: current_tier = row['tier'] tier_label = "Management" if current_tier == 1 else "Execution" print(f" {C_DIM}── Tier {current_tier}: {tier_label} ──{C_RESET}") status_dot = f"{C_GREEN}●{C_RESET}" if row['status'] == 'active' else f"{C_DIM}○{C_RESET}" print(f" {status_dot} {C_BOLD}{row['id']}{C_RESET} — {row['name']} ({format_role(row['role'])})") print() db.close() def cmd_history(args): """Show conversation history with another agent.""" if not args: print(f"{C_RED}Usage: btmsg history [--limit N]{C_RESET}") return agent_id = get_agent_id() other_id = args[0] limit = 20 if "--limit" in args: idx = args.index("--limit") if idx + 1 < len(args): try: limit = int(args[idx + 1]) except ValueError: pass db = get_db() # Support prefix match for other_id other = get_agent(db, other_id) if not other: row = db.execute("SELECT * FROM agents WHERE id LIKE ?", (other_id + "%",)).fetchone() if row: other = row other_id = row['id'] else: print(f"{C_RED}Agent '{other_id}' not found.{C_RESET}") db.close() return agent = get_agent(db, agent_id) if not agent: print(f"{C_RED}Agent '{agent_id}' not registered.{C_RESET}") db.close() return rows = db.execute( "SELECT m.*, a.name as sender_name, a.role as sender_role " "FROM messages m JOIN agents a ON m.from_agent = a.id " "WHERE (m.from_agent = ? AND m.to_agent = ?) " " OR (m.from_agent = ? AND m.to_agent = ?) " "ORDER BY m.created_at ASC LIMIT ?", (agent_id, other_id, other_id, agent_id, limit) ).fetchall() print(f"\n{C_BOLD}💬 History: {agent['name']} ↔ {other['name']}{C_RESET}\n") if not rows: print(f" {C_DIM}No messages yet.{C_RESET}\n") db.close() return for row in rows: time_str = format_time(row['created_at']) is_me = row['from_agent'] == agent_id arrow = f"{C_CYAN}→{C_RESET}" if is_me else f"{C_YELLOW}←{C_RESET}" name = "You" if is_me else row['sender_name'] print(f" {C_DIM}{time_str}{C_RESET} {arrow} {C_BOLD}{name}{C_RESET}: {row['content']}") print() db.close() def cmd_status(args): """Show all agents in the group and their status.""" db = get_db() # Get current agent's group, or show all agent_id = os.environ.get("BTMSG_AGENT_ID") if agent_id: agent = get_agent(db, agent_id) if agent: group_id = agent['group_id'] rows = db.execute( "SELECT * FROM agents WHERE group_id = ? ORDER BY tier, role, name", (group_id,) ).fetchall() else: rows = db.execute("SELECT * FROM agents ORDER BY group_id, tier, role, name").fetchall() else: rows = db.execute("SELECT * FROM agents ORDER BY group_id, tier, role, name").fetchall() if not rows: print(f"{C_DIM}No agents registered.{C_RESET}") db.close() return # Count unread per agent unread_counts = {} for row in rows: count = db.execute( "SELECT COUNT(*) FROM messages WHERE to_agent = ? AND read = 0", (row['id'],) ).fetchone()[0] unread_counts[row['id']] = count print(f"\n{C_BOLD}🤖 Agent Status{C_RESET}\n") current_tier = None for row in rows: if row['tier'] != current_tier: current_tier = row['tier'] tier_label = "TIER 1 — Management" if current_tier == 1 else "TIER 2 — Execution" print(f" {C_BOLD}{tier_label}{C_RESET}") status = row['status'] or 'stopped' if status == 'active': status_str = f"{C_GREEN}● active{C_RESET}" elif status == 'sleeping': status_str = f"{C_YELLOW}◐ sleeping{C_RESET}" else: status_str = f"{C_DIM}○ stopped{C_RESET}" unread = unread_counts.get(row['id'], 0) unread_str = f" {C_RED}({unread} unread){C_RESET}" if unread > 0 else "" model_str = f" {C_DIM}[{row['model']}]{C_RESET}" if row['model'] else "" print(f" {status_str} {C_BOLD}{row['name']}{C_RESET} " f"({format_role(row['role'])}) — {row['id']}{model_str}{unread_str}") print() db.close() def cmd_register(args): """Register a new agent.""" if len(args) < 4: print(f"{C_RED}Usage: btmsg register [--tier N] [--model M] [--cwd PATH]{C_RESET}") print(f"{C_DIM}Roles: {', '.join(ROLES.keys())}{C_RESET}") return agent_id = args[0] name = args[1] role = args[2] group_id = args[3] tier = ROLES.get(role, 2) model = None cwd = None # Parse optional flags i = 4 while i < len(args): if args[i] == "--tier" and i + 1 < len(args): tier = int(args[i + 1]) i += 2 elif args[i] == "--model" and i + 1 < len(args): model = args[i + 1] i += 2 elif args[i] == "--cwd" and i + 1 < len(args): cwd = args[i + 1] i += 2 else: i += 1 db = get_db() # Check if agent already exists existing = get_agent(db, agent_id) if existing: print(f"{C_YELLOW}Agent '{agent_id}' already exists. Updating...{C_RESET}") db.execute( "UPDATE agents SET name=?, role=?, group_id=?, tier=?, model=?, cwd=? WHERE id=?", (name, role, group_id, tier, model, cwd, agent_id) ) else: db.execute( "INSERT INTO agents (id, name, role, group_id, tier, model, cwd) " "VALUES (?, ?, ?, ?, ?, ?, ?)", (agent_id, name, role, group_id, tier, model, cwd) ) db.commit() db.close() print(f"{C_GREEN}✓ Registered: {name} ({role}, tier {tier}){C_RESET}") def cmd_allow(args): """Add contact permission between agents.""" if len(args) < 2: print(f"{C_RED}Usage: btmsg allow [--bidirectional]{C_RESET}") return from_id = args[0] to_id = args[1] bidir = "--bidirectional" in args or "-b" in args db = get_db() # Verify both agents exist for aid in [from_id, to_id]: if not get_agent(db, aid): print(f"{C_RED}Agent '{aid}' not found.{C_RESET}") db.close() return db.execute( "INSERT OR IGNORE INTO contacts (agent_id, contact_id) VALUES (?, ?)", (from_id, to_id) ) if bidir: db.execute( "INSERT OR IGNORE INTO contacts (agent_id, contact_id) VALUES (?, ?)", (to_id, from_id) ) db.commit() db.close() direction = "↔" if bidir else "→" print(f"{C_GREEN}✓ Contact: {from_id} {direction} {to_id}{C_RESET}") def cmd_whoami(args): """Show current agent identity.""" agent_id = get_agent_id() db = get_db() agent = get_agent(db, agent_id) if not agent: print(f"{C_YELLOW}Agent ID: {agent_id} (not registered){C_RESET}") db.close() return unread = db.execute( "SELECT COUNT(*) FROM messages WHERE to_agent = ? AND read = 0", (agent_id,) ).fetchone()[0] print(f"\n{C_BOLD}🤖 {agent['name']}{C_RESET}") print(f" {C_DIM}ID:{C_RESET} {agent['id']}") print(f" {C_DIM}Role:{C_RESET} {format_role(agent['role'])} (Tier {agent['tier']})") print(f" {C_DIM}Group:{C_RESET} {agent['group_id']}") if agent['model']: print(f" {C_DIM}Model:{C_RESET} {agent['model']}") if agent['cwd']: print(f" {C_DIM}CWD:{C_RESET} {agent['cwd']}") print(f" {C_DIM}Unread:{C_RESET} {unread} messages") print() db.close() def cmd_unread_count(args): """Print just the unread count (for notifications).""" agent_id = os.environ.get("BTMSG_AGENT_ID") if not agent_id: print("0") return db = get_db() count = db.execute( "SELECT COUNT(*) FROM messages WHERE to_agent = ? AND read = 0", (agent_id,) ).fetchone()[0] db.close() print(count) def cmd_notify(args): """Print notification line if there are unread messages (for agent injection).""" agent_id = os.environ.get("BTMSG_AGENT_ID") if not agent_id: return db = get_db() count = db.execute( "SELECT COUNT(*) FROM messages WHERE to_agent = ? AND read = 0", (agent_id,) ).fetchone()[0] if count > 0: # Get latest unread sender latest = db.execute( "SELECT a.name, a.role FROM messages m JOIN agents a ON m.from_agent = a.id " "WHERE m.to_agent = ? AND m.read = 0 ORDER BY m.created_at DESC LIMIT 1", (agent_id,) ).fetchone() sender_info = f" (latest from {latest['name']})" if latest else "" print(f"📬 You have {count} unread message{'s' if count != 1 else ''}!{sender_info}") print(f" Use: btmsg inbox") db.close() def cmd_graph(args): """Show visual hierarchy graph with agent status.""" db = get_db() # Get current group agent_id = os.environ.get("BTMSG_AGENT_ID") group_id = None if agent_id: agent = get_agent(db, agent_id) if agent: group_id = agent['group_id'] if group_id: agents = db.execute( "SELECT * FROM agents WHERE group_id = ? ORDER BY tier, role, name", (group_id,) ).fetchall() else: agents = db.execute("SELECT * FROM agents ORDER BY group_id, tier, role, name").fetchall() if not agents: print(f"{C_DIM}No agents registered.{C_RESET}") db.close() return # Build contact map contacts_map = {} for a in agents: rows = db.execute( "SELECT contact_id FROM contacts WHERE agent_id = ?", (a['id'],) ).fetchall() contacts_map[a['id']] = [r['contact_id'] for r in rows] # Unread counts unread = {} for a in agents: count = db.execute( "SELECT COUNT(*) FROM messages WHERE to_agent = ? AND read = 0", (a['id'],) ).fetchone()[0] unread[a['id']] = count # Status dot def dot(agent): s = agent['status'] or 'stopped' if s == 'active': return f"{C_GREEN}●{C_RESET}" elif s == 'sleeping': return f"{C_YELLOW}●{C_RESET}" else: return f"{C_RED}●{C_RESET}" # Agent label with status def label(agent): d = dot(agent) name = agent['name'] role_c = ROLE_COLORS.get(agent['role'], C_RESET) unread_str = f" {C_RED}✉{unread[agent['id']]}{C_RESET}" if unread.get(agent['id'], 0) > 0 else "" model_str = f" {C_DIM}[{agent['model']}]{C_RESET}" if agent['model'] else "" return f"{d} {C_BOLD}{name}{C_RESET} {role_c}({agent['role']}){C_RESET}{model_str}{unread_str}" # Separate by tier tier1 = [a for a in agents if a['tier'] == 1] tier2 = [a for a in agents if a['tier'] == 2] # Find manager, architect, tester manager = next((a for a in tier1 if a['role'] == 'manager'), None) architect = next((a for a in tier1 if a['role'] == 'architect'), None) tester = next((a for a in tier1 if a['role'] == 'tester'), None) other_tier1 = [a for a in tier1 if a['role'] not in ('manager', 'architect', 'tester')] # Draw graph group_name = group_id or "all" print(f"\n{C_BOLD} ╔══════════════════════════════════════════════════════╗{C_RESET}") print(f"{C_BOLD} ║ Agent Hierarchy — {group_name:<35s}║{C_RESET}") print(f"{C_BOLD} ╚══════════════════════════════════════════════════════╝{C_RESET}") # USER at top print(f"\n {C_BOLD}{C_CYAN}👤 USER{C_RESET}") print(f" {C_DIM} │{C_RESET}") # TIER 1 print(f" {C_DIM} │ ┌─────────────────────────────────────────────┐{C_RESET}") print(f" {C_DIM} │ │{C_RESET} {C_BOLD}TIER 1 — Management{C_RESET} {C_DIM}│{C_RESET}") print(f" {C_DIM} │ │{C_RESET} {C_DIM}│{C_RESET}") if manager: print(f" {C_DIM} ├──│{C_RESET} {label(manager)}") if architect: print(f" {C_DIM} │ ├── {C_RESET}{label(architect)}") if tester: print(f" {C_DIM} │ │ └── {C_RESET}{label(tester)}") elif tester: print(f" {C_DIM} │ └── {C_RESET}{label(tester)}") for a in other_tier1: print(f" {C_DIM} │ ├── {C_RESET}{label(a)}") else: for a in tier1: print(f" {C_DIM} │ {C_RESET} {label(a)}") print(f" {C_DIM} │{C_RESET} {C_DIM}│{C_RESET}") print(f" {C_DIM} └─────────────────────────────────────────────┘{C_RESET}") if tier2: # Connection lines from manager to tier 2 print(f" {C_DIM} │{C_RESET}") print(f" {C_DIM} │ ┌─────────────────────────────────────────────┐{C_RESET}") print(f" {C_DIM} │ │{C_RESET} {C_BOLD}TIER 2 — Execution{C_RESET} {C_DIM}│{C_RESET}") print(f" {C_DIM} │ │{C_RESET} {C_DIM}│{C_RESET}") for i, a in enumerate(tier2): is_last = i == len(tier2) - 1 connector = "└" if is_last else "├" print(f" {C_DIM} ├──│{C_RESET} {connector}── {label(a)}") print(f" {C_DIM} │{C_RESET} {C_DIM}│{C_RESET}") print(f" {C_DIM} └─────────────────────────────────────────────┘{C_RESET}") # Legend print(f"\n {C_DIM}Legend: {C_GREEN}●{C_RESET}{C_DIM} active {C_YELLOW}●{C_RESET}{C_DIM} sleeping {C_RED}●{C_RESET}{C_DIM} stopped {C_RED}✉{C_RESET}{C_DIM} unread{C_RESET}") # Communication lines summary print(f"\n {C_BOLD}Communication links:{C_RESET}") for a in agents: targets = contacts_map.get(a['id'], []) if targets: target_names = [] for t_id in targets: t = get_agent(db, t_id) if t: target_names.append(t['name']) if target_names: role_c = ROLE_COLORS.get(a['role'], C_RESET) print(f" {role_c}{a['name']}{C_RESET} → {', '.join(target_names)}") print() db.close() def cmd_feed(args): """Show all messages in the group (admin only).""" agent_id = get_agent_id() db = get_db() agent = get_agent(db, agent_id) if not agent or agent['tier'] != 0: print(f"{C_RED}Admin access required (tier 0).{C_RESET}") db.close() return limit = 50 if "--limit" in args: idx = args.index("--limit") if idx + 1 < len(args): try: limit = int(args[idx + 1]) except ValueError: pass rows = db.execute( "SELECT m.*, a1.name as sender_name, a1.role as sender_role, " "a2.name as recipient_name, a2.role as recipient_role " "FROM messages m " "JOIN agents a1 ON m.from_agent = a1.id " "JOIN agents a2 ON m.to_agent = a2.id " "WHERE m.group_id = ? ORDER BY m.created_at DESC LIMIT ?", (agent['group_id'], limit) ).fetchall() print(f"\n{C_BOLD}📡 Activity Feed — All Messages{C_RESET}\n") if not rows: print(f" {C_DIM}No messages yet.{C_RESET}\n") db.close() return for row in reversed(rows): time_str = format_time(row['created_at']) sender_role = format_role(row['sender_role']) recipient_role = format_role(row['recipient_role']) print(f" {C_DIM}{time_str}{C_RESET} {C_BOLD}{row['sender_name']}{C_RESET} ({sender_role}) " f"→ {C_BOLD}{row['recipient_name']}{C_RESET} ({recipient_role})") preview = row['content'][:200].replace('\n', ' ') if len(row['content']) > 200: preview += "..." print(f" {preview}\n") db.close() def cmd_channel(args): """Channel management commands.""" if not args: print(f"{C_RED}Usage: btmsg channel [args]{C_RESET}") return subcmd = args[0] subargs = args[1:] handlers = { 'create': cmd_channel_create, 'list': cmd_channel_list, 'send': cmd_channel_send, 'history': cmd_channel_history, 'add': cmd_channel_add, } handler = handlers.get(subcmd) if handler: handler(subargs) else: print(f"{C_RED}Unknown channel command: {subcmd}{C_RESET}") def cmd_channel_create(args): """Create a new channel.""" if not args: print(f"{C_RED}Usage: btmsg channel create {C_RESET}") return agent_id = get_agent_id() name = args[0] db = get_db() agent = get_agent(db, agent_id) if not agent: print(f"{C_RED}Agent '{agent_id}' not registered.{C_RESET}") db.close() return channel_id = str(uuid.uuid4())[:8] db.execute( "INSERT INTO channels (id, name, group_id, created_by) VALUES (?, ?, ?, ?)", (channel_id, name, agent['group_id'], agent_id) ) db.execute( "INSERT INTO channel_members (channel_id, agent_id) VALUES (?, ?)", (channel_id, agent_id) ) db.commit() db.close() print(f"{C_GREEN}✓ Channel #{name} created [{channel_id}]{C_RESET}") def cmd_channel_list(args): """List channels.""" agent_id = get_agent_id() db = get_db() agent = get_agent(db, agent_id) if not agent: print(f"{C_RED}Agent '{agent_id}' not registered.{C_RESET}") db.close() return rows = db.execute( "SELECT c.*, " "(SELECT COUNT(*) FROM channel_members cm WHERE cm.channel_id = c.id) as member_count, " "(SELECT content FROM channel_messages cm2 WHERE cm2.channel_id = c.id " "ORDER BY cm2.created_at DESC LIMIT 1) as last_msg " "FROM channels c WHERE c.group_id = ? ORDER BY c.name", (agent['group_id'],) ).fetchall() print(f"\n{C_BOLD}📢 Channels{C_RESET}\n") if not rows: print(f" {C_DIM}No channels yet. Create one: btmsg channel create {C_RESET}\n") db.close() return for row in rows: last = row['last_msg'] or "" if len(last) > 60: last = last[:60] + "..." print(f" {C_CYAN}#{row['name']}{C_RESET} ({row['member_count']} members) [{row['id']}]") if last: print(f" {C_DIM}{last}{C_RESET}") print() db.close() def cmd_channel_send(args): """Send message to a channel.""" if len(args) < 2: print(f"{C_RED}Usage: btmsg channel send {C_RESET}") return agent_id = get_agent_id() channel_ref = args[0] content = " ".join(args[1:]) db = get_db() agent = get_agent(db, agent_id) if not agent: print(f"{C_RED}Agent '{agent_id}' not registered.{C_RESET}") db.close() return channel = db.execute( "SELECT * FROM channels WHERE id = ? OR name = ?", (channel_ref, channel_ref) ).fetchone() if not channel: print(f"{C_RED}Channel '{channel_ref}' not found.{C_RESET}") db.close() return if agent['tier'] > 0: is_member = db.execute( "SELECT 1 FROM channel_members WHERE channel_id = ? AND agent_id = ?", (channel['id'], agent_id) ).fetchone() if not is_member: print(f"{C_RED}Not a member of #{channel['name']}.{C_RESET}") db.close() return msg_id = str(uuid.uuid4()) db.execute( "INSERT INTO channel_messages (id, channel_id, from_agent, content) VALUES (?, ?, ?, ?)", (msg_id, channel['id'], agent_id, content) ) db.commit() db.close() print(f"{C_GREEN}✓ Sent to #{channel['name']}{C_RESET} [{short_id(msg_id)}]") def cmd_channel_history(args): """Show channel message history.""" if not args: print(f"{C_RED}Usage: btmsg channel history [--limit N]{C_RESET}") return channel_ref = args[0] limit = 30 if "--limit" in args: idx = args.index("--limit") if idx + 1 < len(args): try: limit = int(args[idx + 1]) except ValueError: pass db = get_db() channel = db.execute( "SELECT * FROM channels WHERE id = ? OR name = ?", (channel_ref, channel_ref) ).fetchone() if not channel: print(f"{C_RED}Channel '{channel_ref}' not found.{C_RESET}") db.close() return rows = db.execute( "SELECT cm.*, a.name as sender_name, a.role as sender_role " "FROM channel_messages cm JOIN agents a ON cm.from_agent = a.id " "WHERE cm.channel_id = ? ORDER BY cm.created_at ASC LIMIT ?", (channel['id'], limit) ).fetchall() print(f"\n{C_BOLD}📢 #{channel['name']}{C_RESET}\n") if not rows: print(f" {C_DIM}No messages yet.{C_RESET}\n") db.close() return for row in rows: time_str = format_time(row['created_at']) role_str = format_role(row['sender_role']) print(f" {C_DIM}{time_str}{C_RESET} {C_BOLD}{row['sender_name']}{C_RESET} ({role_str}): {row['content']}") print() db.close() def cmd_channel_add(args): """Add member to a channel.""" if len(args) < 2: print(f"{C_RED}Usage: btmsg channel add {C_RESET}") return channel_ref = args[0] member_id = args[1] db = get_db() channel = db.execute( "SELECT * FROM channels WHERE id = ? OR name = ?", (channel_ref, channel_ref) ).fetchone() if not channel: print(f"{C_RED}Channel '{channel_ref}' not found.{C_RESET}") db.close() return member = get_agent(db, member_id) if not member: print(f"{C_RED}Agent '{member_id}' not found.{C_RESET}") db.close() return db.execute( "INSERT OR IGNORE INTO channel_members (channel_id, agent_id) VALUES (?, ?)", (channel['id'], member_id) ) db.commit() db.close() print(f"{C_GREEN}✓ Added {member['name']} to #{channel['name']}{C_RESET}") def cmd_help(args=None): """Show help.""" print(__doc__) # ─── Main dispatch ─────────────────────────────────────────── COMMANDS = { 'inbox': cmd_inbox, 'read': cmd_read, 'send': cmd_send, 'reply': cmd_reply, 'contacts': cmd_contacts, 'history': cmd_history, 'status': cmd_status, 'register': cmd_register, 'allow': cmd_allow, 'whoami': cmd_whoami, 'graph': cmd_graph, 'unread': cmd_unread_count, 'notify': cmd_notify, 'feed': cmd_feed, 'channel': cmd_channel, 'help': cmd_help, '--help': cmd_help, '-h': cmd_help, } def main(): init_db() if len(sys.argv) < 2: cmd_help() sys.exit(0) command = sys.argv[1] args = sys.argv[2:] handler = COMMANDS.get(command) if not handler: print(f"{C_RED}Unknown command: {command}{C_RESET}") cmd_help() sys.exit(1) handler(args) if __name__ == "__main__": main()