mediator
Intercept and filter communications from difficult contacts. Strips emotion, extracts facts, drafts neutral responses. Use when setting up communication filtering for specific contacts, configuring the mediator, or processing intercepted messages. Triggers on "mediator", "intercept messages", "filter communications", "difficult contact", or requests to handle messages from someone the user doesn't want to deal with directly.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install openclaw-skills-mediator
Repository
Skill path: skills/dylntrnr/mediator
Intercept and filter communications from difficult contacts. Strips emotion, extracts facts, drafts neutral responses. Use when setting up communication filtering for specific contacts, configuring the mediator, or processing intercepted messages. Triggers on "mediator", "intercept messages", "filter communications", "difficult contact", or requests to handle messages from someone the user doesn't want to deal with directly.
Open repositoryBest for
Primary workflow: Ship Full Stack.
Technical facets: Full Stack.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: openclaw.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install mediator into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/openclaw/skills before adding mediator to shared team environments
- Use mediator for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
--- name: mediator description: Intercept and filter communications from difficult contacts. Strips emotion, extracts facts, drafts neutral responses. Use when setting up communication filtering for specific contacts, configuring the mediator, or processing intercepted messages. Triggers on "mediator", "intercept messages", "filter communications", "difficult contact", or requests to handle messages from someone the user doesn't want to deal with directly. --- # Mediator Skill Emotional firewall for difficult relationships. Intercepts messages from configured contacts, strips out emotional content, presents just the facts, and helps draft measured responses. ## Quick Start ```bash # Initialize config (creates mediator.yaml if missing) ~/clawd/skills/mediator/scripts/mediator.sh init # Add a contact to mediate ~/clawd/skills/mediator/scripts/mediator.sh add "Ex Partner" \ --email "[email protected]" \ --phone "+15551234567" \ --channels email,imessage # Process incoming (usually called by cron/heartbeat) ~/clawd/skills/mediator/scripts/mediator.sh check # List configured contacts ~/clawd/skills/mediator/scripts/mediator.sh list # Remove a contact ~/clawd/skills/mediator/scripts/mediator.sh remove "Ex Partner" ``` ## Configuration Config lives at `~/.clawdbot/mediator.yaml`: ```yaml mediator: # Global settings archive_originals: true # Archive raw messages after processing notify_channel: telegram # Where to send summaries (telegram|slack|imessage) contacts: - name: "Ex Partner" email: "[email protected]" phone: "+15551234567" channels: [email, imessage] mode: intercept # intercept | assist summarize: facts-only # facts-only | neutral | full respond: draft # draft | auto (dangerous) - name: "Difficult Client" email: "[email protected]" channels: [email] mode: assist # Don't hide originals, just help respond summarize: neutral respond: draft ``` ### Modes - **intercept**: Archive/hide original, only show summary. User never sees raw emotional content. - **assist**: Show original but also provide summary and response suggestions. ### Summarize Options - **facts-only**: Extract only actionable items, requests, deadlines. No emotion. - **neutral**: Rewrite the message in neutral tone, preserving all content. - **full**: Show everything but flag emotional/manipulative language. ### Respond Options - **draft**: Generate suggested response, wait for approval before sending. - **auto**: Automatically respond (use with extreme caution). ## How It Works ### Email Flow 1. Gmail Pub/Sub notification arrives (real-time) 2. Check if sender matches any configured contact 3. If match: - Fetch full email content - Process through LLM to extract facts/strip emotion - Archive original (apply "Mediator/Raw" label, mark read) - Send summary to configured notify channel - If response needed, draft one ### iMessage Flow 1. `imsg watch` monitors for new messages 2. Check if sender matches configured contact 3. If match: - Process message content - Send summary to notify channel - Draft response if requested ## Scripts - `mediator.sh` - Main CLI wrapper - `process-email.py` - Email processing logic - `process-imessage.py` - iMessage processing logic - `summarize.py` - LLM-based content analysis and summarization ## Integration ### Heartbeat Check Add to `HEARTBEAT.md`: ``` ## Mediator Check ~/clawd/skills/mediator/scripts/mediator.sh check ``` ### Cron (for more frequent checking) ```bash # Check every 5 minutes during business hours */5 9-18 * * 1-5 ~/clawd/skills/mediator/scripts/mediator.sh check ``` ## Safety Notes - **Never auto-respond** to legal, financial, or child-related messages - Original messages are archived, not deleted (recoverable) - All actions logged to `~/.clawdbot/logs/mediator.log` - Review and adjust prompts if summaries miss important context ## Example Output **Original email:** > I can't BELIEVE you would do this to me AGAIN. After everything I've done for you!!! You NEVER think about anyone but yourself. I need you to pick up the kids at 3pm on Saturday and if you can't even do THAT then I don't know what to say anymore. **Mediator summary:** > **From:** Ex Partner > **Channel:** Email > **Action Required:** Yes > > **Request:** Pick up kids at 3pm Saturday > > **Suggested response:** > "Confirmed. I'll pick up the kids at 3pm on Saturday." --- See `references/prompts.md` for the LLM prompts used in processing. --- ## Referenced Files > The following files are referenced in this skill and included for context. ### references/prompts.md ```markdown # Mediator Prompts Reference These prompts are used by `summarize.py` to process messages. ## facts-only (default) Extracts only actionable content. Best for high-conflict situations where you don't want any emotional content. **Removes:** Anger, guilt, accusations, passive aggression, manipulation, blame, criticism, unnecessary history **Extracts:** Specific requests, dates/times/deadlines, names/places/amounts, questions needing answers ## neutral Rewrites the full message in professional tone. Preserves all information but removes emotional charge. Good for situations where you need the full context but can't deal with the tone. ## full Shows everything but flags concerning patterns. Identifies manipulation tactics, distinguishes reasonable vs unreasonable asks, notes hidden implications. Best when you need to understand the full picture including the manipulation. --- ## Customizing Prompts To customize prompts, edit `scripts/summarize.py` and modify the `PROMPTS` dictionary. ### Adding a new mode ```python PROMPTS["custom-mode"] = """Your custom prompt here...""" ``` Then use with: ```bash mediator.sh add "Contact" --summarize custom-mode ``` ## Response Generation All modes generate `suggested_response` fields. These are designed to be: - Neutral and non-escalating - Factual and direct - Brief (no unnecessary words) - Focused only on actionable items Example transformations: | Original | Response | |----------|----------| | "I can't BELIEVE you forgot AGAIN!!!" | (No response needed - no ask) | | "Pick up kids at 3pm Saturday OR ELSE" | "Confirmed. I'll pick up the kids at 3pm Saturday." | | "Why do you always do this to me? Can you at least send me the documents?" | "Sending the documents now." | ``` --- ## Skill Companion Files > Additional files collected from the skill directory layout. ### _meta.json ```json { "owner": "dylntrnr", "slug": "mediator", "displayName": "Mediator", "latest": { "version": "1.0.0", "publishedAt": 1770855906167, "commit": "https://github.com/openclaw/skills/commit/b63f28d71bd5e42f4fd525254d402bd12b4bfe79" }, "history": [] } ``` ### scripts/config-helper.py ```python #!/usr/bin/env python3 """Config helper for mediator - safely manages YAML config.""" import argparse import os import sys from pathlib import Path try: import yaml except ImportError: print("Installing PyYAML...") os.system(f"{sys.executable} -m pip install -q pyyaml") import yaml CONFIG_FILE = Path.home() / ".clawdbot" / "mediator.yaml" def load_config(): if not CONFIG_FILE.exists(): return {"mediator": {"contacts": []}} with open(CONFIG_FILE) as f: return yaml.safe_load(f) or {"mediator": {"contacts": []}} def save_config(config): CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True) with open(CONFIG_FILE, "w") as f: yaml.dump(config, f, default_flow_style=False, sort_keys=False) def cmd_add(args): config = load_config() if "contacts" not in config.get("mediator", {}): config.setdefault("mediator", {})["contacts"] = [] # Check if contact already exists contacts = config["mediator"]["contacts"] for c in contacts: if c.get("name", "").lower() == args.name.lower(): print(f"Contact '{args.name}' already exists. Remove first to update.") sys.exit(1) # Build contact entry contact = { "name": args.name, "channels": args.channels.split(","), "mode": args.mode, "summarize": args.summarize, "respond": args.respond, } if args.email: contact["email"] = args.email if args.phone: contact["phone"] = args.phone contacts.append(contact) save_config(config) def cmd_remove(args): config = load_config() contacts = config.get("mediator", {}).get("contacts", []) original_len = len(contacts) contacts = [c for c in contacts if c.get("name", "").lower() != args.name.lower()] if len(contacts) == original_len: print(f"Contact '{args.name}' not found.") sys.exit(1) config["mediator"]["contacts"] = contacts save_config(config) def cmd_list(args): config = load_config() contacts = config.get("mediator", {}).get("contacts", []) if not contacts: print("No contacts configured.") print("Add one with: mediator.sh add <name> --email <addr> --channels email,imessage") return print(f"Configured contacts ({len(contacts)}):") print("") for c in contacts: name = c.get("name", "Unknown") email = c.get("email", "-") phone = c.get("phone", "-") channels = ", ".join(c.get("channels", [])) mode = c.get("mode", "intercept") summarize = c.get("summarize", "facts-only") print(f" {name}") print(f" Email: {email}") print(f" Phone: {phone}") print(f" Channels: {channels}") print(f" Mode: {mode} | Summarize: {summarize}") print("") def main(): parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(dest="command") # Add command add_parser = subparsers.add_parser("add") add_parser.add_argument("--name", required=True) add_parser.add_argument("--email", default="") add_parser.add_argument("--phone", default="") add_parser.add_argument("--channels", default="email") add_parser.add_argument("--mode", default="intercept") add_parser.add_argument("--summarize", default="facts-only") add_parser.add_argument("--respond", default="draft") # Remove command remove_parser = subparsers.add_parser("remove") remove_parser.add_argument("--name", required=True) # List command subparsers.add_parser("list") args = parser.parse_args() if args.command == "add": cmd_add(args) elif args.command == "remove": cmd_remove(args) elif args.command == "list": cmd_list(args) else: parser.print_help() if __name__ == "__main__": main() ``` ### scripts/mediator.sh ```bash #!/bin/bash # Mediator CLI - Emotional firewall for difficult contacts set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CONFIG_FILE="${HOME}/.clawdbot/mediator.yaml" LOG_FILE="${HOME}/.clawdbot/logs/mediator.log" # Ensure log directory exists mkdir -p "$(dirname "$LOG_FILE")" log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE" } usage() { cat <<EOF Mediator - Emotional firewall for difficult contacts Usage: mediator.sh <command> [options] Commands: init Initialize config file add <name> Add a contact to mediate --email <addr> Contact's email address --phone <num> Contact's phone number --channels <list> Comma-separated: email,imessage --mode <mode> intercept | assist (default: intercept) --summarize <type> facts-only | neutral | full (default: facts-only) --respond <type> draft | auto (default: draft) remove <name> Remove a contact list List configured contacts check Process any new messages from configured contacts process <type> <id> Process a specific message (email|imessage) status Show mediator status Examples: mediator.sh init mediator.sh add "Ex Partner" --email [email protected] --phone +15551234567 --channels email,imessage mediator.sh check mediator.sh list EOF } cmd_init() { if [[ -f "$CONFIG_FILE" ]]; then echo "Config already exists at $CONFIG_FILE" exit 0 fi mkdir -p "$(dirname "$CONFIG_FILE")" cat > "$CONFIG_FILE" <<'YAML' mediator: # Global settings archive_originals: true notify_channel: telegram # Where to send summaries # Gmail accounts to monitor gmail_accounts: - [email protected] - [email protected] contacts: [] # Example contact: # - name: "Difficult Person" # email: "[email protected]" # phone: "+15551234567" # channels: [email, imessage] # mode: intercept # summarize: facts-only # respond: draft YAML echo "Created config at $CONFIG_FILE" log "Initialized mediator config" } cmd_add() { local name="$1" shift local email="" phone="" channels="email" mode="intercept" summarize="facts-only" respond="draft" while [[ $# -gt 0 ]]; do case "$1" in --email) email="$2"; shift 2 ;; --phone) phone="$2"; shift 2 ;; --channels) channels="$2"; shift 2 ;; --mode) mode="$2"; shift 2 ;; --summarize) summarize="$2"; shift 2 ;; --respond) respond="$2"; shift 2 ;; *) echo "Unknown option: $1"; exit 1 ;; esac done if [[ -z "$name" ]]; then echo "Error: Contact name required" exit 1 fi if [[ -z "$email" && -z "$phone" ]]; then echo "Error: At least one of --email or --phone required" exit 1 fi # Use Python to safely add to YAML python3 "$SCRIPT_DIR/config-helper.py" add \ --name "$name" \ --email "$email" \ --phone "$phone" \ --channels "$channels" \ --mode "$mode" \ --summarize "$summarize" \ --respond "$respond" log "Added contact: $name (email=$email, phone=$phone, channels=$channels)" echo "Added contact: $name" } cmd_remove() { local name="$1" if [[ -z "$name" ]]; then echo "Error: Contact name required" exit 1 fi python3 "$SCRIPT_DIR/config-helper.py" remove --name "$name" log "Removed contact: $name" echo "Removed contact: $name" } cmd_list() { python3 "$SCRIPT_DIR/config-helper.py" list } cmd_check() { log "Running mediator check" # Check emails python3 "$SCRIPT_DIR/process-email.py" check # Check iMessages (if any contacts have imessage channel) python3 "$SCRIPT_DIR/process-imessage.py" check log "Mediator check complete" } cmd_status() { echo "=== Mediator Status ===" echo "" if [[ ! -f "$CONFIG_FILE" ]]; then echo "Not initialized. Run: mediator.sh init" exit 0 fi echo "Config: $CONFIG_FILE" echo "Log: $LOG_FILE" echo "" cmd_list echo "" echo "Recent activity:" tail -5 "$LOG_FILE" 2>/dev/null || echo "(no recent activity)" } # Main case "${1:-}" in init) cmd_init ;; add) shift; cmd_add "$@" ;; remove) shift; cmd_remove "$@" ;; list) cmd_list ;; check) cmd_check ;; status) cmd_status ;; -h|--help|"") usage ;; *) echo "Unknown command: $1"; usage; exit 1 ;; esac ``` ### scripts/process-email.py ```python #!/usr/bin/env python3 """ Process emails from mediated contacts. Checks for new emails, processes them, archives originals, sends summaries. """ import json import os import subprocess import sys from datetime import datetime from pathlib import Path try: import yaml except ImportError: os.system(f"{sys.executable} -m pip install -q pyyaml") import yaml CONFIG_FILE = Path.home() / ".clawdbot" / "mediator.yaml" STATE_FILE = Path.home() / ".clawdbot" / "mediator-state.json" LOG_FILE = Path.home() / ".clawdbot" / "logs" / "mediator.log" GOG_SCRIPT = Path.home() / "clawd" / "scripts" / "gog-read.sh" SUMMARIZE_SCRIPT = Path(__file__).parent / "summarize.py" def log(msg): LOG_FILE.parent.mkdir(parents=True, exist_ok=True) timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") with open(LOG_FILE, "a") as f: f.write(f"[{timestamp}] [email] {msg}\n") def load_config(): if not CONFIG_FILE.exists(): return None with open(CONFIG_FILE) as f: return yaml.safe_load(f) def load_state(): if not STATE_FILE.exists(): return {"last_check": {}, "processed_ids": []} with open(STATE_FILE) as f: return json.load(f) def save_state(state): STATE_FILE.parent.mkdir(parents=True, exist_ok=True) with open(STATE_FILE, "w") as f: json.dump(state, f, indent=2) def get_email_contacts(config): """Get contacts that have email channel enabled.""" contacts = config.get("mediator", {}).get("contacts", []) return [c for c in contacts if "email" in c.get("channels", [])] def search_emails(account: str, sender_email: str) -> list: """Search for unread emails from a specific sender.""" query = f"is:unread from:{sender_email}" # Determine account flag account_flag = "--work" if "doxy" in account else "" cmd = [str(GOG_SCRIPT), "gmail", "search", query] if account_flag: cmd.insert(1, account_flag) cmd.extend(["--json", "--limit", "10"]) try: result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) if result.returncode != 0: log(f"Email search failed: {result.stderr}") return [] # Parse JSON output output = result.stdout.strip() if not output or output == "[]": return [] return json.loads(output) except Exception as e: log(f"Email search error: {e}") return [] def get_email_content(account: str, message_id: str) -> dict: """Get full email content.""" account_flag = "--work" if "doxy" in account else "" cmd = [str(GOG_SCRIPT), "gmail", "get", message_id] if account_flag: cmd.insert(1, account_flag) cmd.append("--json") try: result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) if result.returncode != 0: log(f"Email get failed: {result.stderr}") return {} return json.loads(result.stdout) except Exception as e: log(f"Email get error: {e}") return {} def archive_email(account: str, message_id: str, thread_id: str): """Archive email (add Mediator/Raw label, mark read).""" # This needs to go through SAL for write operations # For now, log that we would archive log(f"Would archive email {message_id} (requires SAL for write ops)") # TODO: Implement via SAL request # sal_request = { # "action": "email.modify", # "account": account, # "params": { # "message_id": message_id, # "add_labels": ["Mediator/Raw"], # "mark_read": True # } # } def summarize_content(contact: dict, content: str) -> dict: """Use LLM to summarize/neutralize content.""" try: result = subprocess.run( [sys.executable, str(SUMMARIZE_SCRIPT), "--mode", contact.get("summarize", "facts-only"), "--content", content], capture_output=True, text=True, timeout=60 ) if result.returncode == 0: return json.loads(result.stdout) else: log(f"Summarize failed: {result.stderr}") return {"summary": content, "action_required": False, "suggested_response": ""} except Exception as e: log(f"Summarize error: {e}") return {"summary": content, "action_required": False, "suggested_response": ""} def send_summary(contact: dict, email_data: dict, summary: dict, notify_channel: str): """Send summary to notification channel.""" contact_name = contact.get("name", "Unknown") subject = email_data.get("subject", "(no subject)") message_parts = [ f"📨 **Mediated Email from {contact_name}**", f"Subject: {subject}", "", "**Summary:**", summary.get("summary", "(no summary)"), ] if summary.get("action_required"): message_parts.append("") message_parts.append("⚡ **Action Required:** Yes") if summary.get("suggested_response"): message_parts.append("") message_parts.append("**Suggested Response:**") message_parts.append(f"> {summary['suggested_response']}") message = "\n".join(message_parts) # For now, print to stdout (caller can route appropriately) print(f"=== MEDIATOR SUMMARY ===\n{message}\n========================") log(f"Generated summary for email from {contact_name}") def check_emails(): """Main check routine - scan for emails from mediated contacts.""" config = load_config() if not config: log("No config found") return state = load_state() email_contacts = get_email_contacts(config) if not email_contacts: return accounts = config.get("mediator", {}).get("gmail_accounts", [ "[email protected]", "[email protected]" ]) notify_channel = config.get("mediator", {}).get("notify_channel", "telegram") processed_ids = set(state.get("processed_ids", [])) new_processed = [] for contact in email_contacts: email = contact.get("email") if not email: continue for account in accounts: emails = search_emails(account, email) for email_data in emails: msg_id = email_data.get("id") if not msg_id or msg_id in processed_ids: continue log(f"Processing email {msg_id} from {contact.get('name')}") # Get full content full_email = get_email_content(account, msg_id) if not full_email: continue # Summarize body = full_email.get("body", full_email.get("snippet", "")) summary = summarize_content(contact, body) # Archive if intercept mode if contact.get("mode") == "intercept": archive_email(account, msg_id, full_email.get("thread_id", "")) # Send summary send_summary(contact, full_email, summary, notify_channel) new_processed.append(msg_id) # Update state if new_processed: state["processed_ids"] = list(processed_ids | set(new_processed))[-500:] # Keep last 500 state["last_check"]["email"] = datetime.now().isoformat() save_state(state) if __name__ == "__main__": if len(sys.argv) > 1 and sys.argv[1] == "check": check_emails() else: print("Usage: process-email.py check") ``` ### scripts/process-imessage.py ```python #!/usr/bin/env python3 """ Process iMessages from mediated contacts. Checks for new messages, processes them, sends summaries. """ import json import os import subprocess import sys from datetime import datetime from pathlib import Path try: import yaml except ImportError: os.system(f"{sys.executable} -m pip install -q pyyaml") import yaml CONFIG_FILE = Path.home() / ".clawdbot" / "mediator.yaml" STATE_FILE = Path.home() / ".clawdbot" / "mediator-state.json" LOG_FILE = Path.home() / ".clawdbot" / "logs" / "mediator.log" SUMMARIZE_SCRIPT = Path(__file__).parent / "summarize.py" def log(msg): LOG_FILE.parent.mkdir(parents=True, exist_ok=True) timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") with open(LOG_FILE, "a") as f: f.write(f"[{timestamp}] [imessage] {msg}\n") def load_config(): if not CONFIG_FILE.exists(): return None with open(CONFIG_FILE) as f: return yaml.safe_load(f) def load_state(): if not STATE_FILE.exists(): return {"last_check": {}, "processed_ids": [], "imessage_last_ts": {}} with open(STATE_FILE) as f: return json.load(f) def save_state(state): STATE_FILE.parent.mkdir(parents=True, exist_ok=True) with open(STATE_FILE, "w") as f: json.dump(state, f, indent=2) def get_imessage_contacts(config): """Get contacts that have imessage channel enabled.""" contacts = config.get("mediator", {}).get("contacts", []) return [c for c in contacts if "imessage" in c.get("channels", [])] def normalize_phone(phone: str) -> str: """Normalize phone number for comparison.""" return "".join(c for c in phone if c.isdigit())[-10:] def get_recent_messages(phone: str, limit: int = 10) -> list: """Get recent messages from a phone number using imsg CLI.""" try: # Use imsg history command result = subprocess.run( ["imsg", "history", phone, "--limit", str(limit), "--json"], capture_output=True, text=True, timeout=30 ) if result.returncode != 0: log(f"imsg history failed: {result.stderr}") return [] output = result.stdout.strip() if not output: return [] return json.loads(output) except FileNotFoundError: log("imsg command not found") return [] except Exception as e: log(f"imsg error: {e}") return [] def summarize_content(contact: dict, content: str) -> dict: """Use LLM to summarize/neutralize content.""" try: result = subprocess.run( [sys.executable, str(SUMMARIZE_SCRIPT), "--mode", contact.get("summarize", "facts-only"), "--content", content], capture_output=True, text=True, timeout=60 ) if result.returncode == 0: return json.loads(result.stdout) else: log(f"Summarize failed: {result.stderr}") return {"summary": content, "action_required": False, "suggested_response": ""} except Exception as e: log(f"Summarize error: {e}") return {"summary": content, "action_required": False, "suggested_response": ""} def send_summary(contact: dict, messages: list, summary: dict, notify_channel: str): """Send summary to notification channel.""" contact_name = contact.get("name", "Unknown") msg_count = len(messages) message_parts = [ f"💬 **Mediated iMessage from {contact_name}**", f"({msg_count} message{'s' if msg_count != 1 else ''})", "", "**Summary:**", summary.get("summary", "(no summary)"), ] if summary.get("action_required"): message_parts.append("") message_parts.append("⚡ **Action Required:** Yes") if summary.get("suggested_response"): message_parts.append("") message_parts.append("**Suggested Response:**") message_parts.append(f"> {summary['suggested_response']}") message = "\n".join(message_parts) # For now, print to stdout (caller can route appropriately) print(f"=== MEDIATOR SUMMARY ===\n{message}\n========================") log(f"Generated summary for iMessage from {contact_name}") def check_imessages(): """Main check routine - scan for iMessages from mediated contacts.""" config = load_config() if not config: log("No config found") return state = load_state() imessage_contacts = get_imessage_contacts(config) if not imessage_contacts: return notify_channel = config.get("mediator", {}).get("notify_channel", "telegram") last_timestamps = state.get("imessage_last_ts", {}) for contact in imessage_contacts: phone = contact.get("phone") if not phone: continue normalized = normalize_phone(phone) last_ts = last_timestamps.get(normalized, 0) messages = get_recent_messages(phone) if not messages: continue # Filter to new incoming messages only new_messages = [] max_ts = last_ts for msg in messages: msg_ts = msg.get("timestamp", 0) is_from_them = not msg.get("is_from_me", True) if is_from_them and msg_ts > last_ts: new_messages.append(msg) max_ts = max(max_ts, msg_ts) if not new_messages: continue log(f"Processing {len(new_messages)} new iMessage(s) from {contact.get('name')}") # Combine message texts for summarization combined_text = "\n".join(m.get("text", "") for m in new_messages) # Summarize summary = summarize_content(contact, combined_text) # Send summary send_summary(contact, new_messages, summary, notify_channel) # Update timestamp last_timestamps[normalized] = max_ts # Update state state["imessage_last_ts"] = last_timestamps state["last_check"]["imessage"] = datetime.now().isoformat() save_state(state) if __name__ == "__main__": if len(sys.argv) > 1 and sys.argv[1] == "check": check_imessages() else: print("Usage: process-imessage.py check") ``` ### scripts/summarize.py ```python #!/usr/bin/env python3 """ LLM-based content summarization for mediator skill. Strips emotion, extracts facts, drafts neutral responses. """ import argparse import json import os import subprocess import sys from pathlib import Path # Prompts for different summarization modes PROMPTS = { "facts-only": """You are an emotional firewall. Your job is to extract ONLY the factual, actionable content from a message, completely removing: - Emotional language (anger, guilt, accusations, passive aggression) - Manipulation tactics - Blame or criticism - Unnecessary context or history Extract: - Specific requests or asks - Dates, times, deadlines - Names, places, amounts - Yes/no questions that need answers If the message is purely emotional with no actionable content, say "No action required - emotional venting only." Respond in JSON format: { "summary": "Brief factual summary of what they need/want", "action_required": true/false, "deadline": "date/time if mentioned, null otherwise", "suggested_response": "Brief, neutral response if action required" }""", "neutral": """You are a diplomatic translator. Rewrite the following message in completely neutral, professional tone while preserving ALL information. Remove emotional charge but keep the meaning intact. Respond in JSON format: { "summary": "Neutral rewrite of the full message", "action_required": true/false, "original_tone": "brief description of original emotional tone", "suggested_response": "Appropriate neutral response" }""", "full": """You are a communication analyst. Analyze this message and flag any concerning patterns while presenting the full content. Identify: - Main message/request - Emotional manipulation tactics (guilt, threats, gaslighting) - Reasonable vs unreasonable asks - Hidden implications Respond in JSON format: { "summary": "Full message content", "action_required": true/false, "flags": ["list of concerning patterns identified"], "reasonable_parts": "what's fair/reasonable in the message", "unreasonable_parts": "what's unfair/unreasonable", "suggested_response": "Balanced response addressing reasonable parts" }""" } def summarize_with_llm(content: str, mode: str) -> dict: """Call LLM to summarize content.""" prompt = PROMPTS.get(mode, PROMPTS["facts-only"]) full_prompt = f"""{prompt} MESSAGE TO PROCESS: \"\"\" {content} \"\"\" Respond with ONLY valid JSON, no other text.""" # Try using the llm CLI if available try: result = subprocess.run( ["llm", "-m", "gpt-4o-mini", prompt, "--no-stream"], input=content, capture_output=True, text=True, timeout=60 ) if result.returncode == 0: response = result.stdout.strip() # Try to parse as JSON try: # Handle markdown code blocks if response.startswith("```"): response = response.split("```")[1] if response.startswith("json"): response = response[4:] return json.loads(response) except json.JSONDecodeError: return { "summary": response, "action_required": False, "suggested_response": "" } except FileNotFoundError: pass except Exception as e: pass # Fallback: simple extraction without LLM return fallback_summarize(content, mode) def fallback_summarize(content: str, mode: str) -> dict: """Simple fallback summarization without LLM.""" # Very basic heuristics lines = content.strip().split("\n") # Look for question marks (questions need answers) questions = [l.strip() for l in lines if "?" in l] # Look for time/date patterns import re time_patterns = re.findall( r'\b\d{1,2}(?::\d{2})?\s*(?:am|pm|AM|PM)?\b|\b(?:monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b|\b\d{1,2}/\d{1,2}\b', content, re.IGNORECASE ) action_required = bool(questions) or bool(time_patterns) # Truncate if too long summary = content[:500] + "..." if len(content) > 500 else content return { "summary": summary, "action_required": action_required, "questions": questions[:3] if questions else [], "suggested_response": "Please review and respond." if action_required else "" } def main(): parser = argparse.ArgumentParser(description="Summarize message content") parser.add_argument("--mode", choices=["facts-only", "neutral", "full"], default="facts-only") parser.add_argument("--content", required=True, help="Content to summarize") args = parser.parse_args() result = summarize_with_llm(args.content, args.mode) print(json.dumps(result)) if __name__ == "__main__": main() ```