Skill: austindixson/scribe (version 1.0.0)

--- name: scribe displayName: Scribe | OpenClaw Skill description: Scans OpenClaw logs, config files, chat history, cursor history, behavior, desires, tastes, and drafts to take comprehensive daily and weekly notes with summaries. version: 1.0.0 --- # Scribe | OpenClaw Skill Comprehensive note-taking system that scans multiple OpenClaw data sources to generate daily and weekly notes with summaries. ## Description Scribe automatically scans and analyzes: - **OpenClaw logs** - Errors, warnings, gateway events, subagent activity - **Config files** - Model preferences, gateway settings, agent configurations - **Chat history** - Cursor IDE chat messages from SQLite databases - **Cursor history** - Workspace-specific conversation history - **Memory files** - Daily notes and long-term memory (MEMORY.md) - **Behavior files** - BEHAVIOR.md, DESIRES.md, TASTES.md, PREFERENCES.md - **Drafts** - Blog posts, tweets, and other draft files Generates structured markdown notes with summaries at the top of each file. ## Installation ```bash clawhub install scribe ``` Or clone into your skills directory: ```bash git clone https://github.com/Org/scribe.git workspace/skills/scribe ``` ## Usage ### Daily Notes Generate a daily note covering the last 24 hours: ```bash python3 workspace/skills/scribe/scripts/scribe.py --mode daily ``` ### Weekly Notes Generate a weekly note covering the last 7 days: ```bash python3 workspace/skills/scribe/scripts/scribe.py --mode weekly ``` ### Both Daily and Weekly Generate both notes at once: ```bash python3 workspace/skills/scribe/scripts/scribe.py --mode both ``` ### JSON Output Get results in JSON format: ```bash python3 workspace/skills/scribe/scripts/scribe.py --mode daily --json ``` ## Examples **Example 1: Daily Note Generation** *Scenario:* You want a daily summary of OpenClaw activity. *Action:* Run `python3 workspace/skills/scribe/scripts/scribe.py --mode daily`. *Outcome:* A markdown file `workspace/Notes/daily/YYYY-MM-DD.md` with a summary at the top, followed by detailed sections for logs, chat history, memory, drafts, behavior, and config. **Example 2: Weekly Summary** *Scenario:* You want a weekly overview of patterns and trends. *Action:* Run `python3 workspace/skills/scribe/scripts/scribe.py --mode weekly`. *Outcome:* A markdown file `workspace/Notes/weekly/YYYY-MM-DD.md` with weekly statistics, trends, and activity summaries. **Example 3: Cron Job Integration** *Scenario:* Automate daily note generation. *Action:* Add a cron job that runs `scribe.py --mode daily` every day at midnight. *Outcome:* Daily notes are automatically generated and saved to `workspace/Notes/daily/`. ## Commands ```bash python3 workspace/skills/scribe/scripts/scribe.py --mode daily # Generate daily note python3 workspace/skills/scribe/scripts/scribe.py --mode weekly # Generate weekly note python3 workspace/skills/scribe/scripts/scribe.py --mode both # Generate both python3 workspace/skills/scribe/scripts/scribe.py --mode daily --json # JSON output python3 workspace/skills/scribe/scripts/scribe.py --openclaw-home /path/to/openclaw # Custom home directory ``` - **--mode** — Choose `daily`, `weekly`, or `both` (default: `daily`) - **--json** — Output results in JSON format instead of markdown files - **--openclaw-home** — Specify OpenClaw home directory (default: `~/.openclaw`) ## What this skill does 1. **Scans logs** - Analyzes all `.log` files in `logs/` directory for errors, warnings, gateway events, and subagent spawns 2. **Reads config** - Extracts model preferences, gateway settings, and agent configurations from `openclaw.json` 3. **Extracts chat history** - Queries Cursor's SQLite databases (`state.vscdb`) for recent chat messages 4. **Scans memory files** - Reads daily notes (`memory/YYYY-MM-DD.md`) and long-term memory (`MEMORY.md`) 5. **Finds drafts** - Searches for draft files matching patterns like `*draft*.txt`, `*draft*.md`, `blog/**/*.md`, `tweet*.txt` 6. **Reads behavior files** - Looks for `BEHAVIOR.md`, `DESIRES.md`, `TASTES.md`, `PREFERENCES.md` 7. **Generates notes** - Creates structured markdown files with summaries at the top 8. **Saves to Notes** - Writes daily notes to `workspace/Notes/daily/YYYY-MM-DD.md` and weekly notes to `workspace/Notes/weekly/YYYY-MM-DD.md` ## Output Format ### Daily Note Structure ```markdown # Daily Summary - YYYY-MM-DD HH:MM:SS - **Logs**: X errors, Y warnings - **Gateway Events**: Z events - **Subagent Activity**: N spawns - **Chat Messages**: M messages - **Daily Notes**: K files - **Drafts**: L draft files - **Config**: Loaded successfully --- # Daily Note ## Logs ### Errors (X) - [error details] ### Warnings (Y) - [warning details] ## Chat History Found M messages in the last 24 hours. ### User (X messages) - [message previews] ## Memory Files ### Daily Notes (K) - **YYYY-MM-DD**: [content preview] ## Drafts ### [draft path] - Size: X bytes - Modified: [timestamp] - Preview: [content preview] ## Behavior & Preferences ### Behavior Files - **BEHAVIOR.md**: [content preview] ## Configuration ### Model Preferences - Default: [model] - Aliases: X configured ``` ### Weekly Note Structure ```markdown # Weekly Summary - YYYY-MM-DD HH:MM:SS [Summary statistics] --- # Weekly Note - YYYY-MM-DD to YYYY-MM-DD ## Weekly Summary - **Total Errors**: X - **Total Warnings**: Y - **Gateway Events**: Z - **Subagent Spawns**: N ## Chat Activity (M messages) - **User**: X messages - **Assistant**: Y messages ## Memory Activity (K daily notes) - **YYYY-MM-DD**: X bytes ## Drafts (L files) - **[path]**: X bytes (modified: [timestamp]) ## Trends & Patterns - ⚠️ **Error Rate**: X errors this week - 🤖 **Subagent Activity**: N spawns this week - 💬 **Chat Activity**: M messages this week ``` ## Requirements - Python 3.7+ - OpenClaw installation with `~/.openclaw` directory structure - Cursor IDE installed (for chat history scanning) - SQLite3 available (usually pre-installed on macOS) - Write access to `workspace/Notes/` directory ## Integration as a Cron Job **Example Cron Job Configuration:** ```json { "payload": { "kind": "agentTurn", "message": "Run scribe.py --mode daily to generate daily notes.", "model": "openrouter/google/gemini-2.5-flash", "thinking": "low", "timeoutSeconds": 300 }, "schedule": { "kind": "cron", "cron": "0 0 * * *" }, "delivery": { "mode": "announce" }, "sessionTarget": "isolated", "name": "Daily Scribe Note" } ``` **Or run directly via shell script:** ```bash # Add to crontab (crontab -e) # Run daily at midnight 0 0 * * * /Users/ghost/.openclaw/workspace/skills/scribe/scripts/scribe.py --mode daily >> /Users/ghost/.openclaw/logs/scribe.log 2>&1 ``` ## Security & Privacy - **File access**: Reads `openclaw.json` (config only, no secrets), `logs/*.log`, `memory/*.md`, and Cursor SQLite databases - **No data exfiltration**: All data stays local; notes are saved to `workspace/Notes/` directory - **Safe execution**: Uses read-only access to config and logs; only writes to `workspace/Notes/daily/` and `workspace/Notes/weekly/` directories ## Limitations - Chat history extraction depends on Cursor's database schema (may need updates if Cursor changes storage format) - Draft file detection uses pattern matching; may miss files with non-standard naming - Behavior file detection looks for common filenames; custom locations may not be found - Large log files may take time to process

path: README.md
content: # Scribe | OpenClaw Skill

Comprehensive note-taking system for OpenClaw that scans logs, configs, chat history, memory files, drafts, and behavior files to generate daily and weekly notes with summaries.

## Quick Start

```bash
# Generate daily note
python3 workspace/skills/scribe/scripts/scribe.py --mode daily

# Generate weekly note
python3 workspace/skills/scribe/scripts/scribe.py --mode weekly

# Generate both
python3 workspace/skills/scribe/scripts/scribe.py --mode both
```

## Features

- ✅ **Log Analysis** - Scans OpenClaw logs for errors, warnings, gateway events, and subagent activity
- ✅ **Config Scanning** - Reads model preferences and settings from `openclaw.json`
- ✅ **Chat History** - Extracts messages from Cursor IDE SQLite databases
- ✅ **Memory Tracking** - Scans daily notes and long-term memory files
- ✅ **Draft Detection** - Finds blog posts, tweets, and other draft files
- ✅ **Behavior Files** - Reads BEHAVIOR.md, DESIRES.md, TASTES.md, PREFERENCES.md
- ✅ **Summary Generation** - Creates summaries at the top of each note file
- ✅ **Daily & Weekly Notes** - Generates both daily and weekly reports

## Output Location

Notes are saved to:
- Daily: `workspace/Notes/daily/YYYY-MM-DD.md`
- Weekly: `workspace/Notes/weekly/YYYY-MM-DD.md`

## Example Output

### Daily Note Summary

```markdown
# Daily Summary - 2026-02-17 10:30:00

- **Logs**: 3 errors, 5 warnings
- **Gateway Events**: 12 events
- **Subagent Activity**: 8 spawns
- **Chat Messages**: 45 messages
- **Daily Notes**: 2 files
- **Drafts**: 1 draft files
- **Config**: Loaded successfully
```

### Weekly Note Summary

```markdown
# Weekly Summary - 2026-02-17 10:30:00

- **Total Errors**: 15
- **Total Warnings**: 28
- **Gateway Events**: 89
- **Subagent Spawns**: 42
```

## Usage Examples

### Manual Execution

```bash
# Daily note only
python3 workspace/skills/scribe/scripts/scribe.py --mode daily

# Weekly note only
python3 workspace/skills/scribe/scripts/scribe.py --mode weekly

# Both daily and weekly
python3 workspace/skills/scribe/scripts/scribe.py --mode both

# JSON output
python3 workspace/skills/scribe/scripts/scribe.py --mode daily --json
```

### Cron Job Integration

Add to your OpenClaw cron jobs:

```json
{
  "payload": {
    "kind": "agentTurn",
    "message": "Run scribe.py --mode daily",
    "model": "openrouter/google/gemini-2.5-flash",
    "thinking": "low",
    "timeoutSeconds": 300
  },
  "schedule": {
    "kind": "cron",
    "cron": "0 0 * * *"
  },
  "delivery": {
    "mode": "announce"
  },
  "sessionTarget": "isolated",
  "name": "Daily Scribe Note"
}
```

## Data Sources

Scribe scans the following locations:

- **Logs**: `~/.openclaw/logs/*.log`
- **Config**: `~/.openclaw/openclaw.json`
- **Chat History**: Cursor SQLite databases (`~/Library/Application Support/Cursor/User/globalStorage/state.vscdb`)
- **Memory**: `~/.openclaw/workspace/memory/*.md`
- **Drafts**: `~/.openclaw/workspace/**/*draft*.txt`, `**/*draft*.md`, `blog/**/*.md`, `tweet*.txt`
- **Behavior**: `~/.openclaw/workspace/BEHAVIOR.md`, `DESIRES.md`, `TASTES.md`, `PREFERENCES.md`

## Requirements

- Python 3.7+
- OpenClaw installation
- Cursor IDE (for chat history)
- SQLite3 (usually pre-installed on macOS)

## License

OpenClaw Skill - See main OpenClaw repository for license information.


path: _meta.json
content: {
  "schema": "openclaw.skill.v1",
  "name": "scribe",
  "version": "1.0.0",
  "displayName": "Scribe",
  "description": "Scans OpenClaw logs, config files, chat history, cursor history, behavior, desires, tastes, and drafts to take comprehensive daily and weekly notes with summaries.",
  "author": "OpenClaw",
  "tags": ["notes", "journal", "logs", "openclaw"],
  "platform": "openclaw"
}


path: scripts/scribe.py
content: #!/usr/bin/env python3
"""
Scribe - Comprehensive note-taking for OpenClaw

Scans OpenClaw logs, config files, chat history, cursor history, behavior,
desires, tastes, drafts and takes daily/weekly notes with summaries.
"""

import argparse
import json
import sqlite3
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import List, Dict, Any, Optional, Tuple
import re
import os


class Scribe:
    def __init__(self, openclaw_home: Path):
        self.openclaw_home = openclaw_home
        self.workspace_dir = openclaw_home / "workspace"
        self.logs_dir = openclaw_home / "logs"
        self.notes_dir = self.workspace_dir / "Notes"
        self.daily_notes_dir = self.notes_dir / "daily"
        self.weekly_notes_dir = self.notes_dir / "weekly"
        self.memory_dir = self.workspace_dir / "memory"
        self.config_path = openclaw_home / "openclaw.json"
        self.cursor_storage = Path.home() / "Library" / "Application Support" / "Cursor" / "User"
        self.global_db = self.cursor_storage / "globalStorage" / "state.vscdb"
        self.workspace_storage = self.cursor_storage / "workspaceStorage"
        
        # Ensure directories exist
        self.notes_dir.mkdir(parents=True, exist_ok=True)
        self.daily_notes_dir.mkdir(parents=True, exist_ok=True)
        self.weekly_notes_dir.mkdir(parents=True, exist_ok=True)
        self.memory_dir.mkdir(parents=True, exist_ok=True)
        
    def scan_logs(self, days_back: int = 1) -> Dict[str, Any]:
        """Scan OpenClaw logs for activity patterns."""
        log_data = {
            "errors": [],
            "warnings": [],
            "activities": [],
            "gateway_events": [],
            "subagent_spawns": []
        }
        
        if not self.logs_dir.exists():
            return log_data
        
        cutoff_time = datetime.now() - timedelta(days=days_back)
        
        for log_file in self.logs_dir.glob("*.log"):
            try:
                with open(log_file, 'r', encoding='utf-8', errors='ignore') as f:
                    for line in f:
                        # Try to extract timestamp if present
                        timestamp_match = re.search(r'(\d{4}-\d{2}-\d{2}[\sT]\d{2}:\d{2}:\d{2})', line)
                        if timestamp_match:
                            try:
                                log_time = datetime.strptime(timestamp_match.group(1), '%Y-%m-%d %H:%M:%S')
                            except:
                                try:
                                    log_time = datetime.strptime(timestamp_match.group(1), '%Y-%m-%dT%H:%M:%S')
                                except:
                                    log_time = None
                        else:
                            log_time = None
                        
                        # Categorize log entries
                        line_lower = line.lower()
                        if 'error' in line_lower or 'exception' in line_lower or 'traceback' in line_lower:
                            log_data["errors"].append({
                                "file": log_file.name,
                                "line": line.strip(),
                                "timestamp": log_time.isoformat() if log_time else None
                            })
                        elif 'warning' in line_lower or 'warn' in line_lower:
                            log_data["warnings"].append({
                                "file": log_file.name,
                                "line": line.strip(),
                                "timestamp": log_time.isoformat() if log_time else None
                            })
                        elif 'gateway' in line_lower:
                            log_data["gateway_events"].append({
                                "file": log_file.name,
                                "line": line.strip(),
                                "timestamp": log_time.isoformat() if log_time else None
                            })
                        elif 'spawn' in line_lower or 'subagent' in line_lower:
                            log_data["subagent_spawns"].append({
                                "file": log_file.name,
                                "line": line.strip(),
                                "timestamp": log_time.isoformat() if log_time else None
                            })
            except Exception as e:
                log_data["errors"].append({
                    "file": log_file.name,
                    "error": f"Failed to read log file: {e}"
                })
        
        return log_data
    
    def scan_config(self) -> Dict[str, Any]:
        """Scan openclaw.json config file for settings and preferences."""
        config_data = {
            "exists": False,
            "model_preferences": {},
            "gateway_settings": {},
            "agent_settings": {},
            "tools_settings": {}
        }
        
        if not self.config_path.exists():
            return config_data
        
        config_data["exists"] = True
        
        try:
            with open(self.config_path, 'r') as f:
                config = json.load(f)
            
            # Extract model preferences
            if "agents" in config and "defaults" in config["agents"]:
                defaults = config["agents"]["defaults"]
                if "model" in defaults:
                    config_data["model_preferences"]["default"] = defaults["model"]
                if "models" in defaults:
                    config_data["model_preferences"]["aliases"] = defaults["models"]
            
            # Extract gateway settings
            if "gateway" in config:
                config_data["gateway_settings"] = config["gateway"]
            
            # Extract agent settings
            if "agents" in config:
                config_data["agent_settings"] = config["agents"]
            
            # Extract tools settings
            if "tools" in config:
                config_data["tools_settings"] = config["tools"]
                
        except Exception as e:
            config_data["error"] = str(e)
        
        return config_data
    
    def scan_chat_history(self, hours_back: float = 24.0) -> List[Dict[str, Any]]:
        """Extract chat history from Cursor's SQLite databases."""
        cutoff_time = datetime.now(timezone.utc) - timedelta(hours=hours_back)
        all_messages = []
        
        # Try global storage
        if self.global_db.exists():
            try:
                conn = sqlite3.connect(str(self.global_db))
                conn.row_factory = sqlite3.Row
                cursor = conn.cursor()
                
                cursor.execute("SELECT key, value FROM ItemTable WHERE key LIKE '%chat%' OR key LIKE '%ai%' OR key LIKE '%history%' OR key LIKE '%conversation%'")
                rows = cursor.fetchall()
                
                for row in rows:
                    try:
                        value = json.loads(row['value'])
                        if isinstance(value, list):
                            for item in value:
                                if isinstance(item, dict) and 'messages' in item:
                                    all_messages.extend(self._extract_messages_from_conversation(item, cutoff_time))
                                elif isinstance(item, dict) and ('role' in item or 'message' in item):
                                    msg = self._normalize_message(item, cutoff_time)
                                    if msg:
                                        all_messages.append(msg)
                        elif isinstance(value, dict) and 'messages' in value:
                            all_messages.extend(self._extract_messages_from_conversation(value, cutoff_time))
                    except:
                        pass
                
                conn.close()
            except Exception as e:
                pass
        
        # Try workspace-specific databases
        if self.workspace_storage.exists():
            for workspace_dir in self.workspace_storage.iterdir():
                db_path = workspace_dir / "state.vscdb"
                if db_path.exists():
                    try:
                        conn = sqlite3.connect(str(db_path))
                        conn.row_factory = sqlite3.Row
                        cursor = conn.cursor()
                        
                        cursor.execute("SELECT key, value FROM ItemTable WHERE key LIKE '%chat%' OR key LIKE '%ai%' OR key LIKE '%history%'")
                        rows = cursor.fetchall()
                        
                        for row in rows:
                            try:
                                value = json.loads(row['value'])
                                if isinstance(value, dict) and 'messages' in value:
                                    all_messages.extend(self._extract_messages_from_conversation(value, cutoff_time))
                            except:
                                pass
                        
                        conn.close()
                    except:
                        pass
        
        return all_messages
    
    def _extract_messages_from_conversation(self, conversation: Dict, cutoff_time: datetime) -> List[Dict]:
        """Extract messages from a conversation object."""
        messages = []
        if 'messages' in conversation:
            for msg in conversation['messages']:
                normalized = self._normalize_message(msg, cutoff_time)
                if normalized:
                    messages.append(normalized)
        return messages
    
    def _normalize_message(self, msg: Any, cutoff_time: datetime) -> Optional[Dict]:
        """Normalize a message object to a standard format."""
        if not isinstance(msg, dict):
            return None
        
        # Extract timestamp
        timestamp = None
        if 'timestamp' in msg:
            timestamp = msg['timestamp']
        elif 'createdAt' in msg:
            timestamp = msg['createdAt']
        elif 'time' in msg:
            timestamp = msg['time']
        
        if timestamp:
            try:
                if isinstance(timestamp, (int, float)):
                    msg_time = datetime.fromtimestamp(timestamp, tz=timezone.utc)
                else:
                    msg_time = datetime.fromisoformat(str(timestamp).replace('Z', '+00:00'))
                if msg_time < cutoff_time:
                    return None
            except:
                pass
        
        # Extract role and content
        role = msg.get('role', msg.get('author', 'unknown'))
        content = msg.get('content', msg.get('message', msg.get('text', '')))
        
        if not content:
            return None
        
        return {
            "role": role,
            "content": str(content)[:500],  # Truncate long messages
            "timestamp": timestamp
        }
    
    def scan_memory_files(self, days_back: int = 7) -> Dict[str, Any]:
        """Scan memory files for behavior patterns and context."""
        memory_data = {
            "daily_notes": [],
            "memories": []
        }
        
        if not self.memory_dir.exists():
            return memory_data
        
        cutoff_date = datetime.now() - timedelta(days=days_back)
        
        # Scan daily memory files
        for memory_file in self.memory_dir.glob("*.md"):
            if memory_file.name == "MEMORY.md":
                # Read long-term memory
                try:
                    with open(memory_file, 'r', encoding='utf-8') as f:
                        content = f.read()
                        memory_data["memories"].append({
                            "file": memory_file.name,
                            "content": content[:2000],  # Truncate for summary
                            "size": len(content)
                        })
                except Exception as e:
                    pass
            else:
                # Daily note file (YYYY-MM-DD.md)
                try:
                    file_date_match = re.match(r'(\d{4}-\d{2}-\d{2})', memory_file.name)
                    if file_date_match:
                        file_date = datetime.strptime(file_date_match.group(1), '%Y-%m-%d')
                        if file_date >= cutoff_date:
                            with open(memory_file, 'r', encoding='utf-8') as f:
                                content = f.read()
                                memory_data["daily_notes"].append({
                                    "date": file_date_match.group(1),
                                    "content": content[:2000],
                                    "size": len(content)
                                })
                except Exception as e:
                    pass
        
        return memory_data
    
    def scan_drafts(self) -> List[Dict[str, Any]]:
        """Scan for draft files (tweets, blog posts, etc.)."""
        drafts = []
        
        # Check workspace for draft files
        draft_patterns = [
            "**/*draft*.txt",
            "**/*draft*.md",
            "**/blog/**/*.md",
            "**/tweet*.txt"
        ]
        
        for pattern in draft_patterns:
            for draft_file in self.workspace_dir.glob(pattern):
                try:
                    with open(draft_file, 'r', encoding='utf-8') as f:
                        content = f.read()
                        drafts.append({
                            "path": str(draft_file.relative_to(self.workspace_dir)),
                            "content": content[:1000],  # Truncate
                            "size": len(content),
                            "modified": datetime.fromtimestamp(draft_file.stat().st_mtime).isoformat()
                        })
                except:
                    pass
        
        return drafts
    
    def scan_behavior_files(self) -> Dict[str, Any]:
        """Scan for behavior, desires, and taste files."""
        behavior_data = {
            "behavior": {},
            "desires": {},
            "tastes": {}
        }
        
        # Look for common behavior/desire files
        behavior_files = {
            "behavior": ["BEHAVIOR.md", "behavior.md", "workspace/BEHAVIOR.md"],
            "desires": ["DESIRES.md", "desires.md", "workspace/DESIRES.md"],
            "tastes": ["TASTES.md", "tastes.md", "workspace/TASTES.md", "PREFERENCES.md"]
        }
        
        for category, file_patterns in behavior_files.items():
            for pattern in file_patterns:
                file_path = self.workspace_dir / pattern if pattern.startswith("workspace/") else self.openclaw_home / pattern
                if file_path.exists():
                    try:
                        with open(file_path, 'r', encoding='utf-8') as f:
                            content = f.read()
                            behavior_data[category][pattern] = {
                                "content": content[:2000],
                                "size": len(content)
                            }
                    except:
                        pass
        
        return behavior_data
    
    def generate_summary(self, data: Dict[str, Any], period: str = "daily") -> str:
        """Generate a summary at the top of the note file."""
        summary_lines = []
        summary_lines.append(f"# {period.capitalize()} Summary - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
        summary_lines.append("")
        
        # Log summary
        if "logs" in data:
            logs = data["logs"]
            summary_lines.append(f"- **Logs**: {len(logs.get('errors', []))} errors, {len(logs.get('warnings', []))} warnings")
            summary_lines.append(f"- **Gateway Events**: {len(logs.get('gateway_events', []))} events")
            summary_lines.append(f"- **Subagent Activity**: {len(logs.get('subagent_spawns', []))} spawns")
        
        # Chat history summary
        if "chat_history" in data:
            chat_count = len(data["chat_history"])
            summary_lines.append(f"- **Chat Messages**: {chat_count} messages")
        
        # Memory summary
        if "memory" in data:
            memory = data["memory"]
            summary_lines.append(f"- **Daily Notes**: {len(memory.get('daily_notes', []))} files")
            summary_lines.append(f"- **Memory Files**: {len(memory.get('memories', []))} files")
        
        # Drafts summary
        if "drafts" in data:
            draft_count = len(data["drafts"])
            summary_lines.append(f"- **Drafts**: {draft_count} draft files")
        
        # Config summary
        if "config" in data and data["config"].get("exists"):
            summary_lines.append("- **Config**: Loaded successfully")
        
        summary_lines.append("")
        summary_lines.append("---")
        summary_lines.append("")
        
        return "\n".join(summary_lines)
    
    def generate_daily_note(self) -> Path:
        """Generate a daily note file."""
        today = datetime.now().strftime('%Y-%m-%d')
        note_file = self.daily_notes_dir / f"{today}.md"
        
        # Collect all data
        data = {
            "logs": self.scan_logs(days_back=1),
            "config": self.scan_config(),
            "chat_history": self.scan_chat_history(hours_back=24),
            "memory": self.scan_memory_files(days_back=1),
            "drafts": self.scan_drafts(),
            "behavior": self.scan_behavior_files()
        }
        
        # Generate note content
        content_lines = []
        content_lines.append(self.generate_summary(data, "daily"))
        content_lines.append("# Daily Note")
        content_lines.append("")
        
        # Logs section
        content_lines.append("## Logs")
        logs = data["logs"]
        if logs.get("errors"):
            content_lines.append(f"### Errors ({len(logs['errors'])})")
            for error in logs["errors"][:10]:  # Limit to 10
                content_lines.append(f"- {error.get('line', error.get('error', 'Unknown error'))[:200]}")
        if logs.get("warnings"):
            content_lines.append(f"### Warnings ({len(logs['warnings'])})")
            for warning in logs["warnings"][:10]:
                content_lines.append(f"- {warning.get('line', 'Unknown warning')[:200]}")
        content_lines.append("")
        
        # Chat history section
        content_lines.append("## Chat History")
        chat_history = data["chat_history"]
        if chat_history:
            content_lines.append(f"Found {len(chat_history)} messages in the last 24 hours.")
            # Group by role
            by_role = {}
            for msg in chat_history:
                role = msg.get("role", "unknown")
                if role not in by_role:
                    by_role[role] = []
                by_role[role].append(msg)
            for role, messages in by_role.items():
                content_lines.append(f"### {role.capitalize()} ({len(messages)} messages)")
                for msg in messages[:5]:  # Show first 5
                    content_lines.append(f"- {msg.get('content', '')[:150]}")
        else:
            content_lines.append("No recent chat history found.")
        content_lines.append("")
        
        # Memory section
        content_lines.append("## Memory Files")
        memory = data["memory"]
        if memory.get("daily_notes"):
            content_lines.append(f"### Daily Notes ({len(memory['daily_notes'])})")
            for note in memory["daily_notes"]:
                content_lines.append(f"- **{note['date']}**: {note['content'][:200]}...")
        content_lines.append("")
        
        # Drafts section
        content_lines.append("## Drafts")
        drafts = data["drafts"]
        if drafts:
            for draft in drafts:
                content_lines.append(f"### {draft['path']}")
                content_lines.append(f"- Size: {draft['size']} bytes")
                content_lines.append(f"- Modified: {draft['modified']}")
                content_lines.append(f"- Preview: {draft['content'][:200]}...")
        else:
            content_lines.append("No drafts found.")
        content_lines.append("")
        
        # Behavior section
        content_lines.append("## Behavior & Preferences")
        behavior = data["behavior"]
        if behavior.get("behavior"):
            content_lines.append("### Behavior Files")
            for file_path, file_data in behavior["behavior"].items():
                content_lines.append(f"- **{file_path}**: {file_data['content'][:200]}...")
        if behavior.get("desires"):
            content_lines.append("### Desires")
            for file_path, file_data in behavior["desires"].items():
                content_lines.append(f"- **{file_path}**: {file_data['content'][:200]}...")
        if behavior.get("tastes"):
            content_lines.append("### Tastes")
            for file_path, file_data in behavior["tastes"].items():
                content_lines.append(f"- **{file_path}**: {file_data['content'][:200]}...")
        content_lines.append("")
        
        # Config section
        content_lines.append("## Configuration")
        config = data["config"]
        if config.get("exists"):
            if config.get("model_preferences"):
                content_lines.append("### Model Preferences")
                if "default" in config["model_preferences"]:
                    content_lines.append(f"- Default: {config['model_preferences']['default']}")
                if "aliases" in config["model_preferences"]:
                    content_lines.append(f"- Aliases: {len(config['model_preferences']['aliases'])} configured")
        else:
            content_lines.append("Config file not found or not readable.")
        content_lines.append("")
        
        # Write file
        content = "\n".join(content_lines)
        with open(note_file, 'w', encoding='utf-8') as f:
            f.write(content)
        
        return note_file
    
    def generate_weekly_note(self) -> Path:
        """Generate a weekly note file."""
        today = datetime.now()
        week_start = today - timedelta(days=today.weekday())
        week_end = week_start + timedelta(days=6)
        
        note_file = self.weekly_notes_dir / f"{week_start.strftime('%Y-%m-%d')}.md"
        
        # Collect all data for the week
        data = {
            "logs": self.scan_logs(days_back=7),
            "config": self.scan_config(),
            "chat_history": self.scan_chat_history(hours_back=168),  # 7 days
            "memory": self.scan_memory_files(days_back=7),
            "drafts": self.scan_drafts(),
            "behavior": self.scan_behavior_files()
        }
        
        # Generate note content
        content_lines = []
        content_lines.append(self.generate_summary(data, "weekly"))
        content_lines.append(f"# Weekly Note - {week_start.strftime('%Y-%m-%d')} to {week_end.strftime('%Y-%m-%d')}")
        content_lines.append("")
        
        # Weekly summary
        content_lines.append("## Weekly Summary")
        content_lines.append("")
        
        # Logs summary
        logs = data["logs"]
        total_errors = len(logs.get("errors", []))
        total_warnings = len(logs.get("warnings", []))
        total_gateway_events = len(logs.get("gateway_events", []))
        total_spawns = len(logs.get("subagent_spawns", []))
        
        content_lines.append(f"- **Total Errors**: {total_errors}")
        content_lines.append(f"- **Total Warnings**: {total_warnings}")
        content_lines.append(f"- **Gateway Events**: {total_gateway_events}")
        content_lines.append(f"- **Subagent Spawns**: {total_spawns}")
        content_lines.append("")
        
        # Chat history summary
        chat_history = data["chat_history"]
        content_lines.append(f"## Chat Activity ({len(chat_history)} messages)")
        if chat_history:
            by_role = {}
            for msg in chat_history:
                role = msg.get("role", "unknown")
                if role not in by_role:
                    by_role[role] = []
                by_role[role].append(msg)
            for role, messages in by_role.items():
                content_lines.append(f"- **{role.capitalize()}**: {len(messages)} messages")
        content_lines.append("")
        
        # Memory summary
        memory = data["memory"]
        content_lines.append(f"## Memory Activity ({len(memory.get('daily_notes', []))} daily notes)")
        for note in memory.get("daily_notes", []):
            content_lines.append(f"- **{note['date']}**: {note['size']} bytes")
        content_lines.append("")
        
        # Drafts summary
        drafts = data["drafts"]
        content_lines.append(f"## Drafts ({len(drafts)} files)")
        for draft in drafts:
            content_lines.append(f"- **{draft['path']}**: {draft['size']} bytes (modified: {draft['modified']})")
        content_lines.append("")
        
        # Trends and patterns
        content_lines.append("## Trends & Patterns")
        content_lines.append("")
        if total_errors > 0:
            content_lines.append(f"- ⚠️ **Error Rate**: {total_errors} errors this week")
        if total_spawns > 0:
            content_lines.append(f"- 🤖 **Subagent Activity**: {total_spawns} spawns this week")
        if len(chat_history) > 0:
            content_lines.append(f"- 💬 **Chat Activity**: {len(chat_history)} messages this week")
        content_lines.append("")
        
        # Write file
        content = "\n".join(content_lines)
        with open(note_file, 'w', encoding='utf-8') as f:
            f.write(content)
        
        return note_file


def main():
    parser = argparse.ArgumentParser(description="Scribe - Comprehensive note-taking for OpenClaw")
    parser.add_argument("--mode", choices=["daily", "weekly", "both"], default="daily",
                       help="Generate daily, weekly, or both notes")
    parser.add_argument("--openclaw-home", type=str,
                       help="OpenClaw home directory (default: ~/.openclaw)")
    parser.add_argument("--json", action="store_true",
                       help="Output results in JSON format")
    args = parser.parse_args()
    
    openclaw_home = Path(args.openclaw_home) if args.openclaw_home else Path.home() / ".openclaw"
    
    scribe = Scribe(openclaw_home)
    
    results = {
        "daily_note": None,
        "weekly_note": None,
        "errors": []
    }
    
    try:
        if args.mode in ["daily", "both"]:
            daily_file = scribe.generate_daily_note()
            results["daily_note"] = str(daily_file)
        
        if args.mode in ["weekly", "both"]:
            weekly_file = scribe.generate_weekly_note()
            results["weekly_note"] = str(weekly_file)
    except Exception as e:
        results["errors"].append(str(e))
        if not args.json:
            print(f"Error: {e}", file=sys.stderr)
    
    if args.json:
        print(json.dumps(results, indent=2))
    else:
        if results["daily_note"]:
            print(f"Daily note generated: {results['daily_note']}")
        if results["weekly_note"]:
            print(f"Weekly note generated: {results['weekly_note']}")
        if results["errors"]:
            print(f"Errors: {results['errors']}", file=sys.stderr)


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (CRITICAL)