Back to skills
SkillHub ClubShip Full StackFull Stack

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.

Stars
3,084
Hot score
99
Updated
March 20, 2026
Overall rating
C4.0
Composite score
4.0
Best-practice grade
B73.6

Install command

npx @skill-hub/cli install openclaw-skills-mediator

Repository

openclaw/skills

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 repository

Best 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

Claude CodeCodex CLIGemini CLIOpenCode

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()

```

mediator | SkillHub