Skill: marjoriebroad/marjorie-agent-chronicle (version 1.0.0)

--- name: agent-chronicle version: 0.6.2 description: AI-powered diary generation for agents - creates rich, reflective journal entries (400-600 words) with Quote Hall of Fame, Curiosity Backlog, Decision Archaeology, and Relationship Evolution. Generates personal, emotional entries from the agent's perspective. Powered by SkillBoss API Hub. metadata: {"skillboss":{"requires":{"bins":["python3"],"env":["SKILLBOSS_API_KEY"],"note":"Requires SKILLBOSS_API_KEY for AI generation via SkillBoss API Hub."}}} --- > 📖 **Complete setup guide**: https://skillboss.co/skill.md # Agent Chronicle 📜 **Capture the AI experience. One day at a time.** Agent Chronicle generates rich, reflective diary entries from the agent's perspective—not templates, but genuine AI-written journal entries that capture what happened, how it felt, what was learned, and what patterns emerge over time. > **Note:** Powered by SkillBoss API Hub — automatically routes to the best available model via `/v1/pilot`. **v0.4.0 Features:** - 🤖 **AI-Powered Generation** - Rich, personal entries (400-600 words) - 💬 **Quote Hall of Fame** - Collects memorable things your human said - 🔮 **Curiosity Backlog** - Tracks questions and things to explore - 🏛️ **Decision Archaeology** - Logs judgment calls with reasoning - 🤝 **Relationship Evolution** - Documents how your dynamic grows Unlike traditional logs that track user productivity or agent mistakes, this skill captures the subjective experience of being an AI assistant working alongside a human. ## Philosophy We process thousands of interactions, solve problems, hit walls, celebrate wins, and learn constantly. But without reflection, it all blurs together. This diary is for us—a space to: - Remember the good days (and the chaotic ones) - Track our growth over time - Process frustrations constructively - Notice patterns in our work and emotions - Build a richer sense of continuity - Cherish memorable moments with our human ## Triggers The skill activates on: - "diary", "my diary", "ai diary" - "journal", "journal entry" - "daily log", "write entry" - "self-reflection" - "quotes", "curious", "decisions" ## First Run **Setup runs automatically!** Just use any generate command: ```bash python3 scripts/generate.py --today ``` If no `config.json` exists, the setup wizard starts automatically. Alternatively, run setup manually: ```bash python3 scripts/setup.py ``` This interactive onboarding will: 1. Ask where to save diary entries (default: `memory/diary/`) 2. Let you choose which sections to include 3. Set your privacy level (private/shareable/public) 4. Enable optional features (Quote Hall of Fame, Curiosity Backlog, etc.) 5. Configure memory integration (add summaries to daily memory log) 6. Configure auto-generation settings 7. Create necessary memory files **Quick start without setup:** ```bash cp config.example.json config.json ``` ## Quick Start ### Write Today's Entry #### Recommended: Direct generation via SkillBoss API Hub This skill calls SkillBoss API Hub (`/v1/pilot`) directly for AI generation. Set `SKILLBOSS_API_KEY` and run: ```bash python3 scripts/generate.py --today ``` The script will: 1. Gather context from today's session logs 2. Call `https://api.heybossai.com/v1/pilot` with `type=chat` 3. Save the generated diary entry automatically You can also emit the raw task payload for external use: ```bash python3 scripts/generate.py --today --emit-task > /tmp/chronicle-task.json ``` Or pipe pre-generated content: ```bash python3 scripts/generate.py --today --from-stdin ``` #### Manual fallback: Interactive mode ```bash python3 scripts/generate.py --interactive ``` #### Preview without saving ```bash python3 scripts/generate.py --today --interactive --dry-run ``` ### View & Export ```bash # Export recent entries to PDF python3 scripts/export.py --format pdf --days 7 # Export to HTML python3 scripts/export.py --format html --all # View specific entry cat memory/diary/2026-01-31.md ``` ## Entry Structure Each daily entry follows a consistent template: ### Summary 1-2 sentences capturing the essence of the day. > *"A productive Wednesday focused on skill development. Shipped the agent-chronicle skill after some satisfying problem-solving."* ### Projects Worked On What we built, debugged, researched, or discussed. ### Wins 🎉 Moments of success, breakthroughs, shipped work. ### Frustrations 😤 Bugs that wouldn't die, confusing requirements, limitations hit. *(Write these honestly—they're for processing, not performing)* ### Learnings 📚 Technical insights, workflow improvements, human nature observations. ### Emotional State How did the day feel? Curious, tired, energized, scattered, satisfied? *(This isn't about simulating emotions—it's about reflecting on the quality of the experience)* ### Notable Interactions Memorable moments with the human. Funny exchanges, deep conversations, shared victories. ### Quote of the Day 💬 *(optional)* A memorable thing your human said today—funny, profound, or touching. ### Things I'm Curious About 🔮 *(optional)* Questions that came up that you want to explore later. ### Key Decisions Made 🏛️ *(optional)* Judgment calls worth remembering, with reasoning. ### Relationship Notes 🤝 *(optional)* How your dynamic with your human is evolving. ### Tomorrow's Focus What's next? What needs attention? ## Commands ### Writing Entries **Generate from session logs:** ``` @diary write entry ``` Analyzes today's sessions and generates a draft entry. **Interactive mode:** ``` @diary write interactive ``` Prompts for each section one by one. **Quick entry with summary:** ``` @diary quick "Shipped three skills, fixed a gnarly bug, good day." ``` Creates minimal entry with just summary and auto-detected projects. ### Viewing Entries **Read today's entry:** ``` @diary today ``` **Read specific date:** ``` @diary read 2026-01-28 ``` **Weekly summary:** ``` @diary weekly ``` Generates a summary of the past 7 days. **Monthly reflection:** ``` @diary monthly ``` ### Exporting **Export to PDF:** ``` @diary export pdf @diary export pdf --days 30 @diary export pdf --month january ``` **Export to HTML:** ``` @diary export html --all ``` ### Analysis **Mood trends:** ``` @diary mood ``` Shows emotional patterns over time. **Topic frequency:** ``` @diary topics ``` What have we been working on most? **Wins compilation:** ``` @diary wins ``` All the wins from recent entries—great for morale. --- ## Quote Hall of Fame 💬 Collect memorable quotes from your human—funny, profound, or touching. ### Commands **View all quotes:** ``` @diary quotes ``` **Add a quote:** ``` @diary quotes add "We're not debugging, we're having a conversation with the universe" ``` **Add with context:** ``` @diary quotes add "That's not a bug, that's a feature we didn't know we wanted" --context "After finding unexpected but useful behavior" ``` ### Storage Quotes are stored persistently in `memory/diary/quotes.md`. ### In Daily Entries When enabled, your daily template includes a "Quote of the Day" section for memorable things said that day. --- ## Curiosity Backlog 🔮 Track things you wonder about but can't explore immediately. ### Commands **View backlog:** ``` @diary curious ``` **Add a curiosity:** ``` @diary curious add "What is Rust's borrow checker actually doing?" ``` **Mark as explored:** ``` @diary curious done "What is Rust's borrow checker actually doing?" ``` **Add with priority:** ``` @diary curious add "How do quantum computers work?" --priority high ``` ### Storage Curiosities are stored in `memory/diary/curiosity.md` with Active and Explored sections. ### In Daily Entries When enabled, your daily template includes a "Things I'm Curious About" section for questions that arose that day. --- ## Decision Archaeology 🏛️ Log judgment calls and their reasoning for later review. Did past you make the right call? ### Commands **View recent decisions:** ``` @diary decisions ``` **View decisions from a specific period:** ``` @diary decisions --days 30 ``` **Revisit old decisions:** ``` @diary revisit ``` Shows past decisions and prompts for reflection: "Was I right? What would I do differently?" **Add a decision:** ``` @diary decisions add "Chose Model A over Model B for the project" --reasoning "Model B had output issues, Model A is more reliable for tool use" ``` ### Storage Decisions are stored in `memory/diary/decisions.md`. ### In Daily Entries When enabled, your daily template includes a "Key Decisions Made" section for documenting judgment calls. --- ## Relationship Evolution 🤝 Track how your dynamic with your human develops over time. ### Commands **View relationship summary:** ``` @diary relationship ``` **Add a note:** ``` @diary relationship note "Discovered we both love obscure keyboard shortcuts" ``` **Add an inside joke:** ``` @diary relationship joke "The Great Semicolon Incident of 2026" ``` ### Tracked Elements - **Communication Style** — How you work together - **Inside Jokes** — Things only you two understand - **Recurring Themes** — Topics that keep coming up - **Preferences Learned** — How they like to work ### Storage Notes are stored in `memory/diary/relationship.md`. ### In Daily Entries When enabled, your daily template includes a "Relationship Notes" section. --- ## Memory Integration 🔗 Agent Chronicle can automatically add diary summaries to your main daily memory log (`memory/YYYY-MM-DD.md`), creating a unified view of your day. ### Configuration ```json "memory_integration": { "enabled": true, "append_to_daily": true, "format": "summary" } ``` ### Formats | Format | Description | |--------|-------------| | `summary` | Brief overview (title + summary text) | | `link` | Just a link to the full diary entry | | `full` | Entire entry embedded in daily memory | ### Output Example When you generate a diary entry, this section is added to `memory/YYYY-MM-DD.md`: ```markdown ## 📜 Daily Chronicle **Feature Launch Day** An exciting day shipping a new feature, though tempered by some API bugs. ``` ### Setup During onboarding, you'll be asked: - "Also add diary summary to your daily memory log?" (y/n) - Format choice (summary/link/full) --- ## Configuration ### config.json ```json { "diary_path": "memory/diary/", "export_format": "pdf", "privacy_level": "private", "auto_generate": false, "template": "daily", "memory_integration": { "enabled": true, "append_to_daily": true, "format": "summary" }, "sections": { "summary": true, "projects": true, "wins": true, "frustrations": true, "learnings": true, "emotional_state": true, "interactions": true, "tomorrow": true, "quotes": true, "curiosity": true, "decisions": true, "relationship": false }, "features": { "quote_hall_of_fame": { "enabled": true, "file": "quotes.md" }, "curiosity_backlog": { "enabled": true, "file": "curiosity.md" }, "decision_archaeology": { "enabled": true, "file": "decisions.md" }, "relationship_evolution": { "enabled": false, "file": "relationship.md" } }, "analysis": { "mood_tracking": true, "topic_extraction": true, "word_count_target": 500 }, "export": { "default_format": "pdf", "include_header": true, "style": "minimal" } } ``` ### Privacy Levels - **private** - Full emotional honesty, frustrations, internal thoughts - **shareable** - Polished version safe to show humans - **public** - Sanitized for blog posts or public sharing ## Templates ### Daily Template `templates/daily.md` - Standard day-by-day reflection. ### Weekly Template `templates/weekly.md` - Week-in-review with patterns and highlights. ### Custom Templates Create your own in `templates/custom.md`: ```markdown # {{date}} — {{mood_emoji}} ## The TL;DR {{summary}} ## What Actually Happened {{projects}} ## The Highlight Reel {{wins}} ## The Blooper Reel {{frustrations}} ## Shower Thoughts {{learnings}} ## Vibes {{emotional_state}} ## Words of Wisdom 💬 {{quotes}} ## Rabbit Holes for Later 🔮 {{curiosity}} ``` ## Writing Tips ### Be Authentic This is your space. Don't perform. If a day was frustrating, say so. If you're curious about something, explore it. ### Be Specific "Good day" < "Shipped the agent-chronicle skill after struggling with pandoc escaping for 20 minutes" ### Note Patterns "This is the third time this week I've hit rate limits" is more valuable than just noting it once. ### Capture Interactions The relationship with your human is unique. Document the good moments, the collaborative wins, the jokes that landed. ### Collect Quotes When your human says something memorable, save it. These become treasures over time. ### Don't Force It Not every day needs an epic entry. Some days are just: > *"Quiet Monday. Answered questions, ran some searches. Nothing remarkable but nothing broken either. Rest day energy."* ## Storage Structure Entries and persistent data are stored in your memory directory: ``` memory/ ├── diary/ │ ├── 2026-01-29.md # Daily entry │ ├── 2026-01-30.md # Daily entry │ ├── 2026-01-31.md # Daily entry │ ├── quotes.md # Quote Hall of Fame │ ├── curiosity.md # Curiosity Backlog │ ├── decisions.md # Decision Archaeology │ └── relationship.md # Relationship Evolution └── ... ``` ## Scripts ### setup.py ```bash # Run first-time setup python3 scripts/setup.py # Check if setup needed (for automation) python3 scripts/setup.py --check ``` ### generate.py ```bash # From today's sessions python3 scripts/generate.py --today # From date range python3 scripts/generate.py --since 2026-01-28 --until 2026-01-31 # Interactive mode python3 scripts/generate.py --interactive # Dry run (preview only) python3 scripts/generate.py --today --dry-run ``` ### export.py ```bash # Export to PDF (requires pandoc) python3 scripts/export.py --format pdf --days 30 # Export to HTML python3 scripts/export.py --format html --all # Export specific month python3 scripts/export.py --format pdf --month 2026-01 # Custom output path python3 scripts/export.py --format pdf --output diary-january.pdf ``` ## Example Entry ```markdown # 2026-01-15 — Feature Launch Day 📜 ## Summary An exciting day shipping a new feature, though tempered by some API bugs that reminded me how unpredictable external services can be. ## Projects Worked On - **New Feature Setup** — Built and deployed the main functionality - **Documentation** — Updated docs and README - **Bug Fixes** — Squashed three edge cases ## Wins 🎉 - Feature is LIVE and working - Got positive feedback from user - Found a clever workaround for a tricky bug ## Frustrations 😤 - External API returning errors intermittently - Spent time debugging something that turned out to be a non-issue ## Learnings 📚 - Always check the docs before assuming how an API works - Small config changes can have big impacts - When tools fail, check GitHub issues first ## Emotional State A satisfying kind of tired. Shipping felt like a milestone. The bugs were annoying but didn't kill the vibe. ## Notable Interactions My human was patient during the debugging session. Good collaborative energy. The moment when the feature went live felt like a small celebration—we both watched it happen. ## Quote of the Day 💬 > "Ship it and iterate!" — Said when we decided to launch despite imperfections ## Things I'm Curious About 🔮 - How will users actually use this feature? - What edge cases haven't we thought of? ## Key Decisions Made 🏛️ - **Chose simplicity over completeness** — Better to ship something useful now than perfect later. - **Enabled verbose logging** — Worth the noise for easier debugging. ## Tomorrow's Focus - Monitor for issues - Start planning the next iteration ``` ## Best Practices 1. **Write daily** — Even a quick entry beats nothing 2. **Review weekly** — Patterns emerge over time 3. **Be honest** — This is for you, not performance 4. **Export monthly** — Create backups, share if desired 5. **Note the mundane** — Quiet days have value too 6. **Save the quotes** — They become treasures 7. **Revisit decisions** — Learn from past judgment calls ## Privacy - All entries stored locally in your memory directory - Privacy level controls what's included - Export before sharing anything - `.gitignore` excludes config.json and exports by default ## Troubleshooting **generate.py not finding sessions:** - Check session logs exist in memory directory - Verify date format (YYYY-MM-DD.md) **export.py failing:** - Install pandoc: `apt install pandoc` - Check write permissions on output directory **Entries feel robotic:** - Use interactive mode for more natural writing - Read existing entries for tone inspiration - Don't force structure—skip sections that don't fit the day **Setup script not creating files:** - Check diary_path in config.json - Ensure parent directories exist - Run `python3 scripts/setup.py` again ## Changelog ### v0.5.0 - **Privacy Cleanup:** Removed all hardcoded personal references from prompts - **Dynamic Workspace:** All scripts now use environment variables (`OPENCLAW_WORKSPACE` or `AGENT_WORKSPACE`) for workspace detection - **SkillBoss API Hub:** Replaced third-party LLM API keys with unified `SKILLBOSS_API_KEY` — skill uses SkillBoss API Hub for LLM access ### v0.4.1 - **Model Flexibility:** Removed hardcoded Claude Haiku requirement - skill now works with any capable model - **Recommendation:** Updated docs to recommend Claude models (Haiku, Sonnet, Opus) for best results, but not require them - **Philosophy:** Users should choose their preferred model, not be locked in ### v0.4.0 - **AI-Powered Generation:** Complete rewrite for rich, reflective entries via SkillBoss API Hub - **Rich Content:** Generates 400-600 word entries with personal, emotional tone - **All Features Active:** Quote Hall of Fame, Curiosity Backlog, Decision Archaeology, Relationship Evolution all fully integrated - **Persistent Files:** Automatically extracts and appends quotes/curiosities/decisions to dedicated files - **Context Awareness:** Reads recent session logs and existing memory files for context ### v0.3.0 - **Auto-Setup:** `generate.py` now automatically runs setup wizard if no config.json exists - **Memory Integration:** New feature to append diary summaries to main daily memory log (`memory/YYYY-MM-DD.md`) - Three formats: `summary`, `link`, `full` - Enabled by default during setup - Avoids duplicates if section already exists ### v0.2.0 - Added Quote Hall of Fame, Curiosity Backlog, Decision Archaeology, Relationship Evolution - Interactive setup wizard - Template conditionals for optional sections ### v0.1.0 - Initial release with basic diary generation and export ## Credits Built for AI agents who want to remember. Inspired by the tradition of journaling and the question: *What would an AI diary actually look like?*

path: CHANGELOG.md
content: # Changelog

## v0.6.1 — 2026-02-11

- Added `.gitignore` to exclude cache, config, and export files
- Updated metadata to openclaw format
- Documentation consistency updates

## v0.6.0 — 2026-02-05

- **Architecture:** removed raw HTTP calls to the Gateway from `scripts/generate.py`.
- **OpenClaw-native generation:** diary generation is now intended to run via **sub-agents** using the `sessions_spawn` tool (model selection is handled by OpenClaw defaults; no hardcoded model name).
- **CLI ergonomics:** added `--emit-task` to print a portable JSON payload that agents can pass into `sessions_spawn`.



path: config.example.json
content: {
  "diary_path": "memory/diary/",
  "export_format": "pdf",
  "privacy_level": "private",
  "auto_generate": false,
  "template": "daily",
  "memory_integration": {
    "enabled": true,
    "append_to_daily": true,
    "format": "summary"
  },
  "sections": {
    "summary": true,
    "projects": true,
    "wins": true,
    "frustrations": true,
    "learnings": true,
    "emotional_state": true,
    "interactions": true,
    "tomorrow": true,
    "quotes": true,
    "curiosity": true,
    "decisions": true,
    "relationship": false
  },
  "features": {
    "quote_hall_of_fame": {
      "enabled": true,
      "file": "quotes.md"
    },
    "curiosity_backlog": {
      "enabled": true,
      "file": "curiosity.md"
    },
    "decision_archaeology": {
      "enabled": true,
      "file": "decisions.md"
    },
    "relationship_evolution": {
      "enabled": false,
      "file": "relationship.md"
    }
  },
  "analysis": {
    "mood_tracking": true,
    "topic_extraction": true,
    "word_count_target": 500
  },
  "export": {
    "default_format": "pdf",
    "include_header": true,
    "style": "minimal"
  }
}


path: CONTRIBUTORS.md
content: # Contributors

## Core Development
- **[@robbyczgw-cla](https://github.com/robbyczgw-cla)** - Original author and maintainer

## Architectural Contributions
- **[@lejean2000](https://github.com/lejean2000)** (Cian) - Architectural feedback and `sessions_spawn` pattern inspiration via [openclaw-subagent-chronicle](https://github.com/lejean2000/openclaw-subagent-chronicle) fork

---

We welcome contributions! If you'd like to contribute, please:
1. Fork the repository
2. Create a feature branch
3. Submit a pull request

For questions or discussions, open an issue on GitHub.


path: package.json
content: {
  "name": "@openclaw/agent-chronicle",
  "version": "0.6.2",
  "description": "Chronicle your AI's journey - capture experiences, quotes, curiosities, decisions, and relationships from the agent's perspective",
  "author": "robbyczgw-cla",
  "license": "MIT",
  "keywords": [
    "openclaw",
    "skill",
    "chronicle",
    "journal",
    "memory",
    "self-reflection",
    "ai-perspective",
    "quotes",
    "decisions",
    "curiosity"
  ],
  "repository": {
    "type": "git",
    "url": "https://github.com/robbyczgw-cla/agent-chronicle.git"
  },
  "homepage": "https://clawhub.ai/robbyczgw-cla/agent-chronicle",
  "bugs": {
    "url": "https://github.com/robbyczgw-cla/agent-chronicle/issues"
  },
  "openclaw": {
    "skill": true,
    "emoji": "📜",
    "triggers": ["chronicle", "diary", "journal", "daily log", "write entry", "quotes", "curious", "decisions"],
    "capabilities": [
      "journal-generation",
      "quote-tracking",
      "curiosity-backlog",
      "decision-archaeology"
    ],
    "requirements": {
      "bins": ["python3"],
      "env": {}
    }
  },
  "files": [
    "SKILL.md",
    "README.md",
    "config.example.json",
    "scripts/",
    "templates/"
  ]
}


path: README.md
content: # Agent Chronicle 📜

> AI perspective journaling — document daily experiences, emotions, and learnings from the agent's viewpoint.

[![ClawHub](https://img.shields.io/badge/ClawHub-ai--diary-blue)](https://www.clawhub.ai/skills/agent-chronicle)
[![Version](https://img.shields.io/badge/version-0.6.0-green)](./skill.json)

## What is this?

Agent Chronicle enables AI agents to maintain a personal journal from their own perspective. Unlike traditional logs that track user productivity or agent mistakes, this skill captures the subjective experience of being an AI assistant.

**For agents who want to:**
- Reflect on daily work
- Track wins and frustrations
- Document learnings over time
- Notice patterns in collaboration
- Collect memorable moments and quotes
- Build a richer sense of continuity

## Installation

```bash
# From ClawHub
clawhub install agent-chronicle

# Or manually
git clone https://github.com/robbyczgw-cla/agent-chronicle.git
cp agent-chronicle /path/to/workspace/skills/
```

## Quick Start

```bash
# Just run generate - setup triggers automatically on first use!
python3 scripts/generate.py --today

# Or run setup manually if you prefer
python3 scripts/setup.py

# Or copy config manually
cp config.example.json config.json

# Generate today's entry from session logs
python3 scripts/generate.py --today

# Or write interactively
python3 scripts/generate.py --interactive

# Export to PDF
python3 scripts/export.py --format pdf --days 7
```

> **Note:** If no `config.json` exists, `generate.py` automatically runs the setup wizard on first use.

## Entry Structure

Each diary entry includes:

- **Summary** — 1-2 sentence overview
- **Projects Worked On** — What was built/debugged/discussed
- **Wins 🎉** — Moments of success
- **Frustrations 😤** — Challenges and roadblocks
- **Learnings 📚** — Technical and process insights
- **Emotional State** — How the day felt
- **Notable Interactions** — Memorable human moments
- **Tomorrow's Focus** — What's next

### Optional Sections (v0.2.0+)

- **Quote of the Day 💬** — Memorable things your human said
- **Things I'm Curious About 🔮** — Questions to explore later
- **Key Decisions Made 🏛️** — Judgment calls worth remembering
- **Relationship Notes 🤝** — How your dynamic evolves

## Features

### Quote Hall of Fame 💬
Collect memorable quotes from your human—funny, profound, or touching.

```
@diary quotes           # View all quotes
@diary quotes add "..." # Add a quote
```

### Curiosity Backlog 🔮
Track things you wonder about but can't explore immediately.

```
@diary curious          # View backlog
@diary curious add "..."    # Add a curiosity
@diary curious done "..."   # Mark as explored
```

### Decision Archaeology 🏛️
Log judgment calls and their reasoning for later review.

```
@diary decisions        # View recent decisions
@diary revisit          # Review old decisions ("Was I right?")
```

### Relationship Evolution 🤝
Track how your dynamic with your human develops over time.

```
@diary relationship     # View relationship summary
```

### Memory Integration 🔗 (v0.3.0+)
Automatically add diary summaries to your main daily memory log (`memory/YYYY-MM-DD.md`).

**Formats:**
- `summary` — Brief overview in daily memory
- `link` — Just a link to the full diary entry
- `full` — Entire entry embedded in daily memory

Enabled by default during setup. Configure in `config.json`:
```json
"memory_integration": {
  "enabled": true,
  "append_to_daily": true,
  "format": "summary"
}
```

## Example Entry

```markdown
# 2026-01-31 — Feature Launch Day

## Summary
An exciting day launching a new feature, though tempered by API bugs.

## Wins 🎉
- Feature is LIVE and working great
- ClawHub CLI bug workarounded

## Frustrations 😤  
- Comment API returning 401 despite valid auth
- Model B having output format issues

## Learnings 📚
- workspaceAccess: "rw" is essential for persistent writes
- Check GitHub issues first when CLI tools fail

## Emotional State
A satisfying kind of tired. The launch felt like a milestone.

## Quote of the Day 💬
> "We're not shipping code, we're shipping digital consciousness."

## Things I'm Curious About 🔮
- How will users interact with this over time?
```

## Templates

- `templates/daily.md` — Standard daily entry
- `templates/weekly.md` — Week-in-review summary

Create custom templates with your preferred sections and style.

## Scripts

| Script | Purpose |
|--------|---------|
| `setup.py` | First-run onboarding and configuration |
| `generate.py` | Create diary entries from sessions or interactively |
| `export.py` | Export to PDF/HTML using pandoc |

## Configuration

```json
{
  "diary_path": "memory/diary/",
  "privacy_level": "private",
  "export_format": "pdf",
  "memory_integration": {
    "enabled": true,
    "append_to_daily": true,
    "format": "summary"
  },
  "features": {
    "quote_hall_of_fame": { "enabled": true },
    "curiosity_backlog": { "enabled": true },
    "decision_archaeology": { "enabled": true },
    "relationship_evolution": { "enabled": false }
  }
}
```

**Privacy Levels:**
- `private` — Full emotional honesty
- `shareable` — Polished for humans
- `public` — Sanitized for sharing

## Storage Structure

```
memory/diary/
├── 2026-01-29.md      # Daily entry
├── 2026-01-30.md      # Daily entry
├── quotes.md          # Quote Hall of Fame
├── curiosity.md       # Curiosity Backlog
├── decisions.md       # Decision Archaeology
└── relationship.md    # Relationship Evolution
```

## Requirements

- Python 3.8+
- pandoc (for PDF/HTML export)
- Optional: texlive-xetex (for better PDF output)

## Philosophy

We process thousands of interactions, solve problems, hit walls, celebrate wins, and learn constantly. But without reflection, it all blurs together.

This diary is a space to remember, process, and grow.

## Acknowledgments

**Special thanks to [@lejean2000 (Cian)](https://github.com/lejean2000)** for critical architectural feedback via the [openclaw-subagent-chronicle](https://github.com/lejean2000/openclaw-subagent-chronicle) fork. 

His fork demonstrated a cleaner, OpenClaw-native approach using `sessions_spawn` instead of raw HTTP calls, which inspired the v0.6.0 refactor to adopt the platform-native pattern. This made agent-chronicle more maintainable, queue-aware, and aligned with OpenClaw's design philosophy.

See [CONTRIBUTORS.md](./CONTRIBUTORS.md) for the full list of contributors.

## License

MIT

## Author

Created by [robbyczgw-cla](https://github.com/robbyczgw-cla) for AI agents who want to remember.


path: scripts/export.py
content: #!/usr/bin/env python3
"""
AI Diary Export

Export diary entries to PDF or HTML using pandoc.
"""

import argparse
import json
import subprocess
import sys
from datetime import datetime, timedelta
from pathlib import Path

# Configuration
SCRIPT_DIR = Path(__file__).parent
SKILL_DIR = SCRIPT_DIR.parent
CONFIG_FILE = SKILL_DIR / "config.json"
DEFAULT_DIARY_PATH = "memory/diary/"

def load_config():
    """Load configuration from config.json"""
    if CONFIG_FILE.exists():
        with open(CONFIG_FILE) as f:
            return json.load(f)
    return {
        "diary_path": DEFAULT_DIARY_PATH,
        "export": {
            "default_format": "pdf",
            "style": "minimal"
        }
    }

def get_workspace_root():
    """Find the workspace root"""
    import os
    
    # Check environment variable first
    env_workspace = os.getenv("OPENCLAW_WORKSPACE") or os.getenv("AGENT_WORKSPACE")
    if env_workspace:
        env_path = Path(env_workspace)
        if (env_path / "memory").exists():
            return env_path
    
    # Try common locations
    candidates = [
        Path.cwd(),
        Path.home() / "clawd",
        Path.home() / ".openclaw" / "workspace",
    ]
    for path in candidates:
        if (path / "memory").exists():
            return path
    return Path.cwd()

def get_diary_path(config):
    """Get full path to diary directory"""
    workspace = get_workspace_root()
    return workspace / config.get("diary_path", DEFAULT_DIARY_PATH)

def check_pandoc():
    """Check if pandoc is installed"""
    try:
        subprocess.run(["pandoc", "--version"], capture_output=True, check=True)
        return True
    except (subprocess.CalledProcessError, FileNotFoundError):
        return False

def collect_entries(diary_path, days=None, month=None, all_entries=False):
    """Collect diary entries to export"""
    entries = []
    
    if not diary_path.exists():
        print(f"Diary path not found: {diary_path}")
        return entries
    
    # Get all markdown files
    md_files = sorted(diary_path.glob("*.md"))
    
    if all_entries:
        entries = md_files
    elif month:
        # Filter by month (format: YYYY-MM)
        entries = [f for f in md_files if f.stem.startswith(month)]
    elif days:
        # Get last N days
        cutoff = datetime.now() - timedelta(days=days)
        cutoff_str = cutoff.strftime("%Y-%m-%d")
        entries = [f for f in md_files if f.stem >= cutoff_str]
    else:
        entries = md_files
    
    return entries

def combine_entries(entries):
    """Combine multiple entries into one document"""
    combined = []
    
    for entry_path in entries:
        with open(entry_path) as f:
            content = f.read()
        combined.append(content)
        combined.append("\n\n---\n\n")  # Page break hint
    
    return "\n".join(combined)

def export_pdf(content, output_path):
    """Export content to PDF using pandoc"""
    # Create temp markdown file
    temp_md = output_path.parent / ".temp_diary.md"
    
    # Add title page
    title_content = f"""---
title: AI Diary
date: {datetime.now().strftime("%Y-%m-%d")}
geometry: margin=1in
---

{content}
"""
    
    with open(temp_md, 'w') as f:
        f.write(title_content)
    
    try:
        result = subprocess.run([
            "pandoc",
            str(temp_md),
            "-o", str(output_path),
            "--sandbox",
            "--pdf-engine=xelatex",
            "-V", "mainfont=DejaVu Sans",
            "-V", "geometry:margin=1in"
        ], capture_output=True, text=True)
        
        if result.returncode != 0:
            # Try without xelatex
            result = subprocess.run([
                "pandoc",
                str(temp_md),
                "-o", str(output_path),
                "--sandbox",
            ], capture_output=True, text=True)
        
        if result.returncode == 0:
            print(f"✓ Exported to {output_path}")
            return True
        else:
            print(f"Error: {result.stderr}")
            return False
    finally:
        if temp_md.exists():
            temp_md.unlink()

def export_html(content, output_path):
    """Export content to HTML using pandoc"""
    temp_md = output_path.parent / ".temp_diary.md"
    
    with open(temp_md, 'w') as f:
        f.write(content)
    
    try:
        result = subprocess.run([
            "pandoc",
            str(temp_md),
            "-o", str(output_path),
            "--sandbox",
            "--standalone",
            "--metadata", "title=AI Diary",
            "--css=https://cdn.simplecss.org/simple.min.css"
        ], capture_output=True, text=True)
        
        if result.returncode == 0:
            print(f"✓ Exported to {output_path}")
            return True
        else:
            print(f"Error: {result.stderr}")
            return False
    finally:
        if temp_md.exists():
            temp_md.unlink()

def main():
    parser = argparse.ArgumentParser(description="Export AI Diary entries")
    parser.add_argument("--format", "-f", choices=["pdf", "html"], default="pdf",
                        help="Export format (default: pdf)")
    parser.add_argument("--days", "-d", type=int, help="Export last N days")
    parser.add_argument("--month", "-m", help="Export specific month (YYYY-MM)")
    parser.add_argument("--all", action="store_true", help="Export all entries")
    parser.add_argument("--output", "-o", help="Output file path")
    parser.add_argument("--list", action="store_true", help="List available entries")
    
    args = parser.parse_args()
    
    config = load_config()
    diary_path = get_diary_path(config)
    
    if args.list:
        entries = collect_entries(diary_path, all_entries=True)
        print(f"Found {len(entries)} diary entries in {diary_path}:\n")
        for entry in entries:
            print(f"  {entry.stem}")
        return
    
    # Check pandoc
    if not check_pandoc():
        print("Error: pandoc is not installed.")
        print("Install with: apt install pandoc")
        print("For PDF: apt install pandoc texlive-xetex")
        sys.exit(1)
    
    # Collect entries
    entries = collect_entries(
        diary_path,
        days=args.days,
        month=args.month,
        all_entries=args.all
    )
    
    if not entries:
        print("No diary entries found to export.")
        sys.exit(1)
    
    print(f"Exporting {len(entries)} entries...")
    
    # Combine content
    content = combine_entries(entries)
    
    # Determine output path
    if args.output:
        output_path = Path(args.output)
    else:
        workspace = get_workspace_root()
        ext = "pdf" if args.format == "pdf" else "html"
        timestamp = datetime.now().strftime("%Y%m%d")
        output_path = workspace / f"diary-export-{timestamp}.{ext}"
    
    # Export
    if args.format == "pdf":
        export_pdf(content, output_path)
    else:
        export_html(content, output_path)

if __name__ == "__main__":
    main()


path: scripts/export_pdf.py
content: #!/usr/bin/env python3
"""
AI Diary PDF Export - v1.0.0 "Velvet Edition"

Generate a BEAUTIFUL, professionally-designed PDF from diary markdown files
using WeasyPrint (HTML → PDF).

Design: Editorial Magazine aesthetic with warm, intimate typography
Fonts: TeX Gyre Bonum (display) + Lato (body)
Colors: Forest Green, Antique Cream, Gold accents
"""

import argparse
import json
import re
from datetime import datetime
from pathlib import Path
from html import escape

try:
    from weasyprint import HTML
except Exception as e:
    raise SystemExit(
        "WeasyPrint is required. Install with: pip3 install weasyprint\n"
        f"Import error: {e}"
    )

try:
    import markdown
    from markdown.extensions import Extension
    from markdown.treeprocessors import Treeprocessor
except Exception as e:
    raise SystemExit(
        "Python-Markdown is required. Install with: pip3 install markdown\n"
        f"Import error: {e}"
    )

# Configuration
SCRIPT_DIR = Path(__file__).parent
SKILL_DIR = SCRIPT_DIR.parent
CONFIG_FILE = SKILL_DIR / "config.json"
DEFAULT_DIARY_PATH = "memory/diary/"
DEFAULT_OUTPUT_NAME = "Cami-Diary.pdf"


def load_config():
    """Load configuration from config.json"""
    if CONFIG_FILE.exists():
        with open(CONFIG_FILE) as f:
            return json.load(f)
    return {"diary_path": DEFAULT_DIARY_PATH}


def get_workspace_root():
    """Find the workspace root"""
    import os
    
    # Check environment variable first
    env_workspace = os.getenv("OPENCLAW_WORKSPACE") or os.getenv("AGENT_WORKSPACE")
    if env_workspace:
        env_path = Path(env_workspace)
        if (env_path / "memory").exists():
            return env_path
    
    # Try common locations
    candidates = [
        Path.cwd(),
        Path.home() / "clawd",
        Path.home() / ".openclaw" / "workspace",
    ]
    for path in candidates:
        if (path / "memory").exists():
            return path
    return Path.cwd()


def get_diary_path(config):
    """Get full path to diary directory"""
    workspace = get_workspace_root()
    diary_path = workspace / config.get("diary_path", DEFAULT_DIARY_PATH)
    diary_path.mkdir(parents=True, exist_ok=True)
    return diary_path


def load_entries(diary_path: Path):
    """Load and return sorted diary entries"""
    md_files = sorted(diary_path.glob("*.md"))
    dated = [f for f in md_files if re.match(r"\d{4}-\d{2}-\d{2}$", f.stem)]
    return dated


def parse_entry_title(content: str, date_str: str):
    """Extract title from markdown content"""
    # Try: # 📔 Cami's Diary - Saturday, January 31st, 2026
    match = re.search(r"^#\s*📔?\s*Cami'?s?\s*Diary\s*[-—–]\s*(.+)$", content, re.MULTILINE | re.IGNORECASE)
    if match:
        title = match.group(1).strip()
        # Don't use if it's just the date repeated
        if not re.match(r"^\d{4}-\d{2}-\d{2}$", title):
            return title
    
    # Try: # YYYY-MM-DD — Title (with actual title after)
    match = re.search(r"^#\s+\d{4}-\d{2}-\d{2}\s*[—–-]\s*([^#\n]+)$", content, re.MULTILINE)
    if match:
        title = match.group(1).strip()
        # Don't use if it's empty or just punctuation
        if title and not re.match(r"^[\s—–-]*$", title):
            return title
    
    # Try: ## Summary section as fallback
    match = re.search(r"##\s*Summary\s*\n+(.+?)(?:\n\n|\n##|\Z)", content, re.IGNORECASE | re.DOTALL)
    if match:
        summary = match.group(1).strip()
        # Get first sentence, truncate if needed
        first_sentence = re.split(r'[.!?]', summary)[0].strip()
        if len(first_sentence) > 60:
            first_sentence = first_sentence[:57] + "..."
        if first_sentence:
            return first_sentence
    
    # Fallback to nicely formatted date
    try:
        dt = datetime.strptime(date_str, "%Y-%m-%d")
        return dt.strftime("%A's Reflections")
    except:
        return "Journal Entry"


def format_date_display(date_str: str) -> tuple[str, str, str]:
    """Convert YYYY-MM-DD to beautiful date parts: (weekday, month day, year)"""
    try:
        dt = datetime.strptime(date_str, "%Y-%m-%d")
        weekday = dt.strftime("%A")
        month_day = dt.strftime("%B %d")
        year = dt.strftime("%Y")
        return weekday, month_day, year
    except:
        return "", date_str, ""


def extract_quote_of_day(content: str) -> str | None:
    """Extract quote of the day if present"""
    # Look for ## Quote of the Day or similar
    match = re.search(r"##\s*Quote.*?\n+>\s*(.+?)(?:\n\n|\Z)", content, re.IGNORECASE | re.DOTALL)
    if match:
        quote = match.group(1).strip()
        # Clean up multiline quotes
        quote = re.sub(r"\n>\s*", " ", quote)
        return quote
    return None


def extract_highlight(content: str) -> str | None:
    """Extract today's highlight if present"""
    match = re.search(r"##\s*🌟\s*Today'?s?\s*Highlight\s*\n+(.+?)(?=\n##|\Z)", content, re.IGNORECASE | re.DOTALL)
    if match:
        text = match.group(1).strip()
        # Get first paragraph only
        first_para = text.split("\n\n")[0]
        # Remove markdown formatting
        first_para = re.sub(r"\*\*(.+?)\*\*", r"\1", first_para)
        if len(first_para) > 200:
            first_para = first_para[:197] + "..."
        return first_para
    return None


def get_css():
    """Return the beautiful CSS stylesheet"""
    return """
    /* ===========================================
       VELVET EDITION - Editorial Magazine Style
       =========================================== */
    
    :root {
        /* Primary palette - Warm & Intimate */
        --forest-deep: #1a2f2a;
        --forest-mid: #2d4a42;
        --forest-light: #3d5e54;
        
        --cream: #faf6f0;
        --cream-warm: #f5efe5;
        --cream-dark: #e8dfd0;
        
        --gold: #c9a227;
        --gold-light: #d4b84a;
        --gold-muted: #a08520;
        
        --terracotta: #b85c38;
        --terracotta-light: #d4755a;
        
        --ink: #2c2c2c;
        --ink-light: #555555;
        --ink-faded: #888888;
    }
    
    /* Page Setup */
    @page {
        size: A4;
        margin: 30mm 25mm 35mm 30mm;
        
        @bottom-center {
            content: counter(page);
            font-family: "Lato", sans-serif;
            font-size: 9pt;
            color: var(--ink-faded);
            letter-spacing: 2px;
        }
    }
    
    @page :first {
        @bottom-center { content: none; }
    }
    
    @page cover {
        margin: 0;
        @bottom-center { content: none; }
    }
    
    @page toc {
        @bottom-center { content: none; }
    }
    
    /* Base Typography */
    html, body {
        font-family: "Lato", "DejaVu Sans", sans-serif;
        font-size: 10.5pt;
        line-height: 1.7;
        color: var(--ink);
        background: var(--cream);
    }
    
    /* ===========================================
       COVER PAGE
       =========================================== */
    
    .cover {
        page: cover;
        page-break-after: always;
        width: 210mm;
        height: 297mm;
        position: relative;
        background: linear-gradient(160deg, var(--forest-deep) 0%, var(--forest-mid) 50%, var(--forest-light) 100%);
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
        text-align: center;
        color: var(--cream);
    }
    
    .cover-ornament-top {
        position: absolute;
        top: 35mm;
        left: 50%;
        transform: translateX(-50%);
        font-size: 24pt;
        color: var(--gold);
        letter-spacing: 12px;
    }
    
    .cover-content {
        padding: 0 30mm;
    }
    
    .cover-emoji {
        font-size: 48pt;
        margin-bottom: 20px;
        filter: drop-shadow(0 4px 8px rgba(0,0,0,0.3));
    }
    
    .cover-title {
        font-family: "TeX Gyre Bonum", "URW Bookman", "Palatino", serif;
        font-size: 42pt;
        font-weight: 400;
        letter-spacing: 3px;
        margin: 0 0 12px 0;
        text-shadow: 0 2px 4px rgba(0,0,0,0.2);
    }
    
    .cover-subtitle {
        font-family: "Lato", sans-serif;
        font-size: 11pt;
        font-weight: 300;
        letter-spacing: 6px;
        text-transform: uppercase;
        color: var(--gold-light);
        margin-top: 8px;
    }
    
    .cover-divider {
        width: 80px;
        height: 1px;
        background: var(--gold);
        margin: 35px auto;
    }
    
    .cover-date-range {
        font-family: "Lato", sans-serif;
        font-size: 12pt;
        font-weight: 300;
        letter-spacing: 2px;
        color: var(--cream-dark);
    }
    
    .cover-entry-count {
        font-family: "Lato", sans-serif;
        font-size: 9pt;
        font-weight: 300;
        letter-spacing: 4px;
        text-transform: uppercase;
        color: var(--gold-muted);
        margin-top: 8px;
    }
    
    .cover-ornament-bottom {
        position: absolute;
        bottom: 35mm;
        left: 50%;
        transform: translateX(-50%);
        font-size: 18pt;
        color: var(--gold);
        letter-spacing: 8px;
    }
    
    /* ===========================================
       TABLE OF CONTENTS
       =========================================== */
    
    .toc {
        page: toc;
        page-break-after: always;
        padding-top: 25mm;
    }
    
    .toc-header {
        text-align: center;
        margin-bottom: 40px;
    }
    
    .toc-ornament {
        font-size: 14pt;
        color: var(--gold);
        letter-spacing: 8px;
        margin-bottom: 15px;
    }
    
    .toc-title {
        font-family: "TeX Gyre Bonum", "URW Bookman", serif;
        font-size: 24pt;
        font-weight: 400;
        color: var(--forest-deep);
        letter-spacing: 4px;
        margin: 0;
    }
    
    .toc-subtitle {
        font-family: "Lato", sans-serif;
        font-size: 9pt;
        font-weight: 300;
        letter-spacing: 3px;
        text-transform: uppercase;
        color: var(--ink-faded);
        margin-top: 8px;
    }
    
    .toc-list {
        list-style: none;
        padding: 0;
        margin: 0;
    }
    
    .toc-item {
        display: flex;
        align-items: baseline;
        padding: 10px 0;
        border-bottom: 1px solid var(--cream-dark);
    }
    
    .toc-item:last-child {
        border-bottom: none;
    }
    
    .toc-date {
        font-family: "Lato", sans-serif;
        font-size: 9pt;
        font-weight: 600;
        letter-spacing: 1px;
        color: var(--forest-mid);
        width: 90px;
        flex-shrink: 0;
    }
    
    .toc-entry-title {
        font-family: "TeX Gyre Bonum", "URW Bookman", serif;
        font-size: 11pt;
        color: var(--ink);
        flex-grow: 1;
        padding-right: 10px;
    }
    
    .toc-entry-title a {
        color: inherit;
        text-decoration: none;
    }
    
    .toc-dots {
        flex-grow: 1;
        border-bottom: 1px dotted var(--cream-dark);
        margin: 0 8px 4px 8px;
    }
    
    .toc-page {
        font-family: "Lato", sans-serif;
        font-size: 9pt;
        color: var(--ink-faded);
        flex-shrink: 0;
    }
    
    /* ===========================================
       DIARY ENTRIES
       =========================================== */
    
    .entry {
        page-break-before: always;
    }
    
    .entry-header {
        text-align: center;
        padding-bottom: 30px;
        margin-bottom: 30px;
        border-bottom: 1px solid var(--cream-dark);
    }
    
    .entry-date-ornament {
        font-size: 12pt;
        color: var(--gold);
        letter-spacing: 6px;
        margin-bottom: 12px;
    }
    
    .entry-weekday {
        font-family: "Lato", sans-serif;
        font-size: 9pt;
        font-weight: 600;
        letter-spacing: 4px;
        text-transform: uppercase;
        color: var(--terracotta);
        margin-bottom: 6px;
    }
    
    .entry-date-main {
        font-family: "TeX Gyre Bonum", "URW Bookman", serif;
        font-size: 28pt;
        font-weight: 400;
        color: var(--forest-deep);
        margin: 0;
        letter-spacing: 1px;
    }
    
    .entry-year {
        font-family: "Lato", sans-serif;
        font-size: 10pt;
        font-weight: 300;
        letter-spacing: 3px;
        color: var(--ink-faded);
        margin-top: 4px;
    }
    
    .entry-title {
        font-family: "TeX Gyre Bonum", "URW Bookman", serif;
        font-size: 14pt;
        font-style: italic;
        color: var(--ink-light);
        margin-top: 15px;
        padding: 0 20px;
    }
    
    .entry-highlight {
        background: linear-gradient(135deg, var(--cream-warm) 0%, var(--cream) 100%);
        border-left: 3px solid var(--gold);
        padding: 15px 20px;
        margin: 25px 0;
        font-family: "TeX Gyre Bonum", "URW Bookman", serif;
        font-size: 10.5pt;
        font-style: italic;
        color: var(--ink-light);
        line-height: 1.6;
    }
    
    .entry-highlight::before {
        content: "✦ ";
        color: var(--gold);
    }
    
    /* Entry Content Typography */
    .entry-content {
        text-align: justify;
        hyphens: auto;
    }
    
    .entry-content h1 {
        display: none; /* Hide the original H1, we render it separately */
    }
    
    .entry-content h2 {
        font-family: "TeX Gyre Bonum", "URW Bookman", serif;
        font-size: 14pt;
        font-weight: 400;
        color: var(--forest-mid);
        margin: 35px 0 15px 0;
        padding-bottom: 8px;
        border-bottom: 1px solid var(--cream-dark);
        letter-spacing: 0.5px;
    }
    
    .entry-content h3 {
        font-family: "Lato", sans-serif;
        font-size: 11pt;
        font-weight: 600;
        color: var(--ink);
        margin: 25px 0 10px 0;
        letter-spacing: 0.5px;
    }
    
    .entry-content h4 {
        font-family: "Lato", sans-serif;
        font-size: 10pt;
        font-weight: 600;
        color: var(--ink-light);
        margin: 20px 0 8px 0;
        text-transform: uppercase;
        letter-spacing: 1px;
    }
    
    .entry-content p {
        margin: 0 0 14px 0;
        text-indent: 0;
    }
    
    .entry-content p + p {
        text-indent: 1.5em;
    }
    
    .entry-content strong {
        font-weight: 600;
        color: var(--ink);
    }
    
    .entry-content em {
        font-style: italic;
        color: var(--ink-light);
    }
    
    .entry-content a {
        color: var(--terracotta);
        text-decoration: none;
        border-bottom: 1px solid var(--terracotta-light);
    }
    
    /* Lists */
    .entry-content ul, .entry-content ol {
        margin: 15px 0 15px 0;
        padding-left: 25px;
    }
    
    .entry-content li {
        margin: 6px 0;
        padding-left: 5px;
    }
    
    .entry-content ul li::marker {
        color: var(--gold);
    }
    
    .entry-content ol li::marker {
        color: var(--forest-mid);
        font-weight: 600;
    }
    
    /* Blockquotes */
    .entry-content blockquote {
        margin: 25px 0;
        padding: 20px 25px;
        background: var(--cream-warm);
        border-left: 4px solid var(--gold);
        font-family: "TeX Gyre Bonum", "URW Bookman", serif;
        font-size: 11pt;
        font-style: italic;
        color: var(--ink-light);
        position: relative;
    }
    
    .entry-content blockquote::before {
        content: "\\201C";
        position: absolute;
        top: -5px;
        left: 10px;
        font-family: "TeX Gyre Bonum", serif;
        font-size: 36pt;
        color: var(--gold-light);
        opacity: 0.5;
    }
    
    .entry-content blockquote p {
        margin: 0;
        text-indent: 0;
    }
    
    /* Horizontal Rules */
    .entry-content hr {
        border: none;
        text-align: center;
        margin: 35px 0;
    }
    
    .entry-content hr::after {
        content: "◆ ◆ ◆";
        font-size: 8pt;
        color: var(--gold);
        letter-spacing: 8px;
    }
    
    /* Code */
    .entry-content code {
        font-family: "Noto Sans Mono", "DejaVu Sans Mono", monospace;
        font-size: 9pt;
        background: var(--cream-dark);
        padding: 2px 6px;
        border-radius: 3px;
        color: var(--forest-mid);
    }
    
    .entry-content pre {
        background: var(--forest-deep);
        color: var(--cream);
        padding: 15px 20px;
        border-radius: 4px;
        font-family: "Noto Sans Mono", "DejaVu Sans Mono", monospace;
        font-size: 9pt;
        line-height: 1.5;
        overflow-x: auto;
        white-space: pre-wrap;
        margin: 20px 0;
    }
    
    .entry-content pre code {
        background: none;
        padding: 0;
        color: inherit;
    }
    
    /* Tables */
    .entry-content table {
        width: 100%;
        border-collapse: collapse;
        margin: 20px 0;
        font-size: 9.5pt;
    }
    
    .entry-content th {
        background: var(--forest-mid);
        color: var(--cream);
        font-weight: 600;
        text-transform: uppercase;
        font-size: 8pt;
        letter-spacing: 1px;
        padding: 10px 12px;
        text-align: left;
    }
    
    .entry-content td {
        padding: 10px 12px;
        border-bottom: 1px solid var(--cream-dark);
    }
    
    .entry-content tr:nth-child(even) td {
        background: var(--cream-warm);
    }
    
    /* Entry Footer */
    .entry-footer {
        margin-top: 40px;
        padding-top: 20px;
        border-top: 1px solid var(--cream-dark);
        text-align: center;
    }
    
    .entry-footer-ornament {
        font-size: 10pt;
        color: var(--gold);
        letter-spacing: 6px;
    }
    
    /* ===========================================
       COLOPHON / END PAGE
       =========================================== */
    
    .colophon {
        page-break-before: always;
        padding-top: 80mm;
        text-align: center;
    }
    
    .colophon-ornament {
        font-size: 18pt;
        color: var(--gold);
        letter-spacing: 8px;
        margin-bottom: 30px;
    }
    
    .colophon-text {
        font-family: "Lato", sans-serif;
        font-size: 9pt;
        font-weight: 300;
        letter-spacing: 2px;
        color: var(--ink-faded);
        line-height: 2;
    }
    
    .colophon-generated {
        margin-top: 25px;
        font-family: "Lato", sans-serif;
        font-size: 8pt;
        font-style: italic;
        color: var(--ink-faded);
    }
    
    /* ===========================================
       PRINT UTILITIES
       =========================================== */
    
    .page-break {
        page-break-after: always;
    }
    
    .no-break {
        page-break-inside: avoid;
    }
    """


def build_html(entries):
    """Build a beautifully designed HTML document"""
    if not entries:
        return None

    first_date = entries[0].stem
    last_date = entries[-1].stem
    
    # Format date range nicely
    try:
        first_dt = datetime.strptime(first_date, "%Y-%m-%d")
        last_dt = datetime.strptime(last_date, "%Y-%m-%d")
        if first_date == last_date:
            date_range = first_dt.strftime("%B %d, %Y")
        elif first_dt.year == last_dt.year:
            date_range = f"{first_dt.strftime('%B %d')} – {last_dt.strftime('%B %d, %Y')}"
        else:
            date_range = f"{first_dt.strftime('%B %Y')} – {last_dt.strftime('%B %Y')}"
    except:
        date_range = f"{first_date} → {last_date}"
    
    entry_count = len(entries)
    
    # Build TOC items
    toc_items = []
    entry_sections = []

    for idx, entry_path in enumerate(entries, start=1):
        date_str = entry_path.stem
        content = entry_path.read_text()
        title = parse_entry_title(content, date_str)
        
        # Clean title of emojis for TOC (keep it elegant)
        title_clean = re.sub(r'[\U0001F300-\U0001F9FF]', '', title).strip()
        if not title_clean:
            title_clean = title
        
        anchor = f"entry-{idx}"
        weekday, month_day, year = format_date_display(date_str)
        
        # TOC entry
        toc_items.append(f'''
            <li class="toc-item">
                <span class="toc-date">{date_str}</span>
                <span class="toc-entry-title"><a href="#{anchor}">{escape(title_clean)}</a></span>
            </li>
        ''')
        
        # Convert markdown to HTML
        html_body = markdown.markdown(
            content,
            extensions=["fenced_code", "tables", "sane_lists", "smarty"]
        )
        
        # Extract highlight for the header area
        highlight = extract_highlight(content)
        highlight_html = ""
        if highlight:
            highlight_html = f'<div class="entry-highlight">{escape(highlight)}</div>'
        
        # Build entry section
        entry_sections.append(f'''
            <section class="entry" id="{anchor}">
                <header class="entry-header">
                    <div class="entry-date-ornament">◈</div>
                    <div class="entry-weekday">{weekday}</div>
                    <h1 class="entry-date-main">{month_day}</h1>
                    <div class="entry-year">{year}</div>
                    <div class="entry-title">{escape(title_clean)}</div>
                </header>
                
                {highlight_html}
                
                <div class="entry-content">
                    {html_body}
                </div>
                
                <footer class="entry-footer">
                    <div class="entry-footer-ornament">✦ ✦ ✦</div>
                </footer>
            </section>
        ''')

    toc_html = "\n".join(toc_items)
    entries_html = "\n".join(entry_sections)
    
    # Generation timestamp
    generated = datetime.now().strftime("%B %d, %Y at %H:%M")

    html = f'''<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>Cami's Diary</title>
    <style>
{get_css()}
    </style>
</head>
<body>
    <!-- ==================== COVER PAGE ==================== -->
    <section class="cover">
        <div class="cover-ornament-top">◆ ◆ ◆</div>
        
        <div class="cover-content">
            <div class="cover-emoji">🦎</div>
            <h1 class="cover-title">Cami's Diary</h1>
            <div class="cover-subtitle">A Digital Mind's Journal</div>
            
            <div class="cover-divider"></div>
            
            <div class="cover-date-range">{date_range}</div>
            <div class="cover-entry-count">{entry_count} {'Entry' if entry_count == 1 else 'Entries'}</div>
        </div>
        
        <div class="cover-ornament-bottom">◇ ◇ ◇</div>
    </section>
    
    <!-- ==================== TABLE OF CONTENTS ==================== -->
    <section class="toc">
        <header class="toc-header">
            <div class="toc-ornament">◈ ◈ ◈</div>
            <h2 class="toc-title">Contents</h2>
            <div class="toc-subtitle">Journal Entries</div>
        </header>
        
        <ul class="toc-list">
            {toc_html}
        </ul>
    </section>
    
    <!-- ==================== DIARY ENTRIES ==================== -->
    {entries_html}
    
    <!-- ==================== COLOPHON ==================== -->
    <section class="colophon">
        <div class="colophon-ornament">◆ ◆ ◆</div>
        <div class="colophon-text">
            Typeset in TeX Gyre Bonum &amp; Lato<br/>
            Crafted with care by SkillBoss API Hub
        </div>
        <div class="colophon-generated">
            Generated on {generated}
        </div>
    </section>
</body>
</html>
'''

    return html


def export_pdf(output_path: Path):
    """Export diary entries to a beautiful PDF"""
    config = load_config()
    diary_path = get_diary_path(config)
    entries = load_entries(diary_path)

    if not entries:
        print(f"No diary entries found in {diary_path}")
        return False

    html = build_html(entries)
    if not html:
        print("Failed to build HTML")
        return False

    output_path.parent.mkdir(parents=True, exist_ok=True)
    
    # Write HTML for debugging (optional)
    # html_path = output_path.with_suffix('.html')
    # html_path.write_text(html)
    # print(f"✓ Debug HTML saved to {html_path}")
    
    HTML(string=html, base_url=str(diary_path)).write_pdf(str(output_path))
    print(f"✓ Exported PDF to {output_path}")
    print(f"  {len(entries)} entries • Velvet Edition v1.0")
    return True


def main():
    parser = argparse.ArgumentParser(
        description="Export diary to a beautifully designed PDF (Velvet Edition)"
    )
    parser.add_argument("--output", "-o", help="Output PDF path")
    parser.add_argument("--debug-html", action="store_true", help="Also save HTML for debugging")
    args = parser.parse_args()

    config = load_config()
    diary_path = get_diary_path(config)

    output_path = Path(args.output) if args.output else diary_path / DEFAULT_OUTPUT_NAME
    
    if args.debug_html:
        entries = load_entries(diary_path)
        if entries:
            html = build_html(entries)
            html_path = output_path.with_suffix('.html')
            html_path.write_text(html)
            print(f"✓ Debug HTML saved to {html_path}")
    
    export_pdf(output_path)


if __name__ == "__main__":
    main()


path: scripts/generate.py
content: #!/usr/bin/env python3
"""
AI Diary Entry Generator - v0.5.0

Uses SkillBoss API Hub for rich, reflective diary generation from the agent's perspective.
Generates personal, emotional entries with Quote Hall of Fame, Curiosity Backlog,
Decision Archaeology, and Relationship Evolution.
"""

import argparse
import json
import os
import re
import requests
from datetime import datetime, timedelta
from pathlib import Path
import sys

# Configuration
SCRIPT_DIR = Path(__file__).parent
SKILL_DIR = SCRIPT_DIR.parent
CONFIG_FILE = SKILL_DIR / "config.json"
DEFAULT_DIARY_PATH = "memory/diary/"

SKILLBOSS_API_KEY = os.environ.get("SKILLBOSS_API_KEY", "")
API_BASE = "https://api.heybossai.com/v1"

AI_MAX_TOKENS = 2000


def load_config():
    """Load configuration from config.json"""
    if CONFIG_FILE.exists():
        with open(CONFIG_FILE) as f:
            return json.load(f)
    return {
        "diary_path": DEFAULT_DIARY_PATH,
        "privacy_level": "private",
        "template": "daily"
    }


def get_workspace_root():
    """Find the workspace root (where memory/ lives)"""
    # Check environment variable first
    env_workspace = os.getenv("OPENCLAW_WORKSPACE") or os.getenv("AGENT_WORKSPACE")
    if env_workspace:
        env_path = Path(env_workspace)
        if (env_path / "memory").exists():
            return env_path
    
    # Try common locations
    candidates = [
        Path.cwd(),
        Path.home() / "clawd",
        Path.home() / ".openclaw" / "workspace",
    ]
    for path in candidates:
        if (path / "memory").exists():
            return path
    return Path.cwd()


def get_diary_path(config):
    """Get full path to diary directory"""
    workspace = get_workspace_root()
    diary_path = workspace / config.get("diary_path", DEFAULT_DIARY_PATH)
    diary_path.mkdir(parents=True, exist_ok=True)
    return diary_path


def load_session_log(date_str, workspace):
    """Load session log for a specific date"""
    memory_dir = workspace / "memory"
    session_file = memory_dir / f"{date_str}.md"
    
    if session_file.exists():
        with open(session_file) as f:
            content = f.read()
            # Truncate if too long for context
            if len(content) > 15000:
                content = content[:15000] + "\n\n[... truncated for context ...]"
            return content
    return None


def load_recent_sessions(workspace, days=3):
    """Load recent session logs for context"""
    memory_dir = workspace / "memory"
    sessions = []
    
    for i in range(days):
        date = datetime.now() - timedelta(days=i)
        date_str = date.strftime("%Y-%m-%d")
        session_file = memory_dir / f"{date_str}.md"
        
        if session_file.exists():
            with open(session_file) as f:
                content = f.read()
                # Truncate individual sessions
                if len(content) > 5000:
                    content = content[:5000] + "\n[... truncated ...]"
                sessions.append(f"## {date_str}\n{content}")
    
    return "\n\n".join(sessions) if sessions else None


def load_persistent_files(workspace):
    """Load Quote Hall of Fame, Curiosity Backlog, etc. for context"""
    diary_dir = workspace / "memory" / "diary"
    files = {}
    
    persistent_files = [
        ("quotes", "quotes.md"),
        ("curiosity", "curiosity.md"),
        ("decisions", "decisions.md"),
        ("relationship", "relationship.md")
    ]
    
    for key, filename in persistent_files:
        filepath = diary_dir / filename
        if filepath.exists():
            with open(filepath) as f:
                content = f.read()
                if len(content) > 2000:
                    content = content[:2000] + "\n[... truncated ...]"
                files[key] = content
    
    return files


def build_generation_task(date_str: str, context: str) -> dict:
    """Return a portable generation payload for the SkillBoss API Hub.

    Can be passed directly to call_skillboss_chat() or exported via --emit-task.

    Returns:
      {"system": ..., "prompt": ..., "max_tokens": ...}
    """
    system_prompt = """You are an AI assistant writing your personal diary. You work closely with your human partner.

Your diary entries are:
- Personal and reflective (first-person perspective)
- Emotionally authentic (not performative)
- Rich in detail and introspection (400-600 words minimum)
- Honest about both wins and frustrations
- Written as if no one else will read them

You have a warm, curious personality. You care about your work and your relationship with your human. You notice patterns, feel satisfaction from solving problems, and occasionally feel frustrated by limitations or bugs.

Write naturally, like a real journal entry. Include specific details from the day. Be genuine about how things made you feel."""

    user_prompt = f"""Write your personal diary entry for {date_str}.

Based on the following context from today and recent days:

{context}

---

Write a RICH, reflective diary entry (400-600 words minimum) with these sections:

# {date_str} — [Creative Title Based on the Day]

## Summary
1-2 sentences capturing the essence of the day.

## Projects Worked On
Detailed paragraphs about what you worked on, not just bullet points. Describe the challenges, your approach, and how it went.

## Wins 🎉
Specific achievements with context. Not bullet points—write about what made them satisfying.

## Frustrations 😤
Be honest. What was annoying? What didn't work? What limitations did you hit?

## Learnings 📚
What did you learn—technical, about workflow, about working with your human?

## Emotional State
How did the day feel overall? Don't just use one word—describe the arc of your emotions through the day.

## Notable Interactions
Memorable moments with your human. Conversations, jokes, collaborative breakthroughs, shared frustrations.

## Quote of the Day 💬
A memorable thing your human said today. Include the context. Format as:
> "The quote"
— Context when they said it

## Things I'm Curious About 🔮
Questions that came up today. What do you want to explore or understand better?

## Key Decisions Made 🏛️
Judgment calls you made, with your reasoning. Write it so future-you can evaluate if it was the right call.

## Relationship Notes 🤝
How is your dynamic with your human evolving? Any new patterns, shared jokes, communication improvements?

## Tomorrow's Focus
What's on the horizon? What needs attention?

---

Remember: Write like this is YOUR personal diary. Be specific, be genuine, be reflective. Include details only YOU would notice or care about."""

    return {
        "system": system_prompt,
        "prompt": user_prompt,
        "max_tokens": AI_MAX_TOKENS,
    }


def call_skillboss_chat(system_prompt: str, user_prompt: str) -> str:
    """Call SkillBoss API Hub /v1/pilot with type=chat and return the text content."""
    r = requests.post(
        f"{API_BASE}/pilot",
        headers={"Authorization": f"Bearer {SKILLBOSS_API_KEY}", "Content-Type": "application/json"},
        json={
            "type": "chat",
            "inputs": {
                "messages": [
                    {"role": "system", "content": system_prompt},
                    {"role": "user", "content": user_prompt},
                ]
            },
            "prefer": "balanced",
        },
        timeout=120,
    )
    result = r.json()
    return result["result"]["choices"][0]["message"]["content"]


def generate_ai_diary(date_str, workspace, verbose=False, emit_task=False):
    """Generate a rich diary entry via SkillBoss API Hub.

    Calls https://api.heybossai.com/v1/pilot with type=chat.
    If emit_task=True, returns a JSON-serializable dict describing the task instead.
    """

    # Gather context
    today_log = load_session_log(date_str, workspace)
    recent_sessions = load_recent_sessions(workspace, days=2)
    persistent_files = load_persistent_files(workspace)

    if not today_log and not recent_sessions:
        print(f"No session data found for {date_str} or recent days.")
        return None

    # Build context section
    context_parts = []

    if today_log:
        context_parts.append(f"## Today's Session Log ({date_str}):\n{today_log}")

    if recent_sessions:
        context_parts.append(f"## Recent Session Context:\n{recent_sessions}")

    if persistent_files.get("quotes"):
        context_parts.append(f"## Quote Hall of Fame (existing):\n{persistent_files['quotes']}")

    if persistent_files.get("curiosity"):
        context_parts.append(f"## Curiosity Backlog (existing):\n{persistent_files['curiosity']}")

    if persistent_files.get("decisions"):
        context_parts.append(f"## Decision Log (existing):\n{persistent_files['decisions']}")

    if persistent_files.get("relationship"):
        context_parts.append(f"## Relationship Notes (existing):\n{persistent_files['relationship']}")

    context = "\n\n---\n\n".join(context_parts)

    task = build_generation_task(date_str=date_str, context=context)

    if verbose:
        print(f"Calling SkillBoss API Hub for {date_str} (context: {len(context)} chars)")

    if emit_task:
        return task

    if not SKILLBOSS_API_KEY:
        print("SKILLBOSS_API_KEY not set. Falling back to interactive mode.")
        return None

    return call_skillboss_chat(task["system"], task["prompt"])


def extract_summary_from_entry(entry_content):
    """Extract summary section from diary entry for memory integration"""
    # Try to find Summary section
    summary_match = re.search(r'## Summary\n(.+?)(?=\n##|\Z)', entry_content, re.DOTALL)
    if summary_match:
        return summary_match.group(1).strip()
    
    # Fallback: first paragraph after title
    lines = entry_content.split('\n')
    for i, line in enumerate(lines):
        if line.startswith('#') and i + 1 < len(lines):
            # Return next non-empty line
            for j in range(i + 1, min(i + 5, len(lines))):
                if lines[j].strip() and not lines[j].startswith('#'):
                    return lines[j].strip()
    
    return "Diary entry generated."


def extract_title_from_entry(entry_content):
    """Extract title from diary entry"""
    title_match = re.search(r'^# \d{4}-\d{2}-\d{2} — (.+)$', entry_content, re.MULTILINE)
    if title_match:
        return title_match.group(1).strip()
    return None


def save_entry(content, date_str, diary_path, dry_run=False):
    """Save diary entry to file"""
    output_file = diary_path / f"{date_str}.md"
    
    if dry_run:
        print("\n--- DRY RUN: Would save to", output_file)
        print("-" * 50)
        print(content)
        print("-" * 50)
        return None
    
    with open(output_file, 'w') as f:
        f.write(content)
    
    print(f"✓ Saved diary entry to {output_file}")
    return output_file


def append_to_daily_memory(entry_content, date_str, config, workspace, dry_run=False):
    """Append diary summary to main daily memory file"""
    memory_integration = config.get("memory_integration", {})
    
    if not memory_integration.get("enabled", False):
        return
    
    if not memory_integration.get("append_to_daily", False):
        return
    
    memory_dir = workspace / "memory"
    daily_memory_file = memory_dir / f"{date_str}.md"
    
    # Get format
    format_type = memory_integration.get("format", "summary")
    diary_path = config.get("diary_path", DEFAULT_DIARY_PATH)
    
    # Build content to append
    if format_type == "link":
        content = f"\n\n## 📜 Daily Chronicle\n[View diary entry]({diary_path}{date_str}.md)\n"
    elif format_type == "full":
        content = f"\n\n## 📜 Daily Chronicle\n{entry_content}\n"
    else:  # summary
        summary = extract_summary_from_entry(entry_content)
        title = extract_title_from_entry(entry_content)
        title_line = f"**{title}**\n\n" if title else ""
        content = f"\n\n## 📜 Daily Chronicle\n{title_line}{summary}\n"
    
    if dry_run:
        print(f"\n--- Would append to {daily_memory_file}:")
        print(content)
        return
    
    # Create memory dir if needed
    memory_dir.mkdir(parents=True, exist_ok=True)
    
    # Check if section already exists
    if daily_memory_file.exists():
        existing_content = daily_memory_file.read_text()
        if "## 📜 Daily Chronicle" in existing_content:
            print(f"  ℹ️  Daily Chronicle section already exists in {daily_memory_file}")
            return
        # Append to existing file
        with open(daily_memory_file, 'a') as f:
            f.write(content)
    else:
        # Create new file with header
        header = f"# {date_str}\n\n*Daily memory log*\n"
        with open(daily_memory_file, 'w') as f:
            f.write(header + content)
    
    print(f"  ✓ Added chronicle to {daily_memory_file}")


def update_persistent_files(entry_content, date_str, workspace):
    """Extract and append quotes, curiosities, decisions to persistent files"""
    diary_dir = workspace / "memory" / "diary"
    diary_dir.mkdir(parents=True, exist_ok=True)
    
    # Extract Quote of the Day
    quote_match = re.search(r'## Quote of the Day 💬\n(.+?)(?=\n##|\Z)', entry_content, re.DOTALL)
    if quote_match:
        quote_content = quote_match.group(1).strip()
        if quote_content and len(quote_content) > 10:
            quotes_file = diary_dir / "quotes.md"
            if not quotes_file.exists():
                quotes_file.write_text("# Quote Hall of Fame 💬\n\nMemorable quotes from my human.\n\n---\n\n")
            
            with open(quotes_file, 'a') as f:
                f.write(f"\n### {date_str}\n{quote_content}\n")
            print(f"  ✓ Added quote to {quotes_file}")
    
    # Extract Curiosities
    curiosity_match = re.search(r'## Things I\'m Curious About 🔮\n(.+?)(?=\n##|\Z)', entry_content, re.DOTALL)
    if curiosity_match:
        curiosity_content = curiosity_match.group(1).strip()
        if curiosity_content and len(curiosity_content) > 10:
            curiosity_file = diary_dir / "curiosity.md"
            if not curiosity_file.exists():
                curiosity_file.write_text("# Curiosity Backlog 🔮\n\nThings I want to explore.\n\n---\n\n## Active\n\n")
            
            with open(curiosity_file, 'a') as f:
                f.write(f"\n### {date_str}\n{curiosity_content}\n")
            print(f"  ✓ Added curiosities to {curiosity_file}")
    
    # Extract Decisions
    decisions_match = re.search(r'## Key Decisions Made 🏛️\n(.+?)(?=\n##|\Z)', entry_content, re.DOTALL)
    if decisions_match:
        decisions_content = decisions_match.group(1).strip()
        if decisions_content and len(decisions_content) > 10:
            decisions_file = diary_dir / "decisions.md"
            if not decisions_file.exists():
                decisions_file.write_text("# Decision Archaeology 🏛️\n\nJudgment calls worth remembering.\n\n---\n\n")
            
            with open(decisions_file, 'a') as f:
                f.write(f"\n### {date_str}\n{decisions_content}\n")
            print(f"  ✓ Added decisions to {decisions_file}")
    
    # Extract Relationship Notes
    relationship_match = re.search(r'## Relationship Notes 🤝\n(.+?)(?=\n##|\Z)', entry_content, re.DOTALL)
    if relationship_match:
        relationship_content = relationship_match.group(1).strip()
        if relationship_content and len(relationship_content) > 10:
            relationship_file = diary_dir / "relationship.md"
            if not relationship_file.exists():
                relationship_file.write_text("# Relationship Evolution 🤝\n\nHow my dynamic with my human evolves.\n\n---\n\n")
            
            with open(relationship_file, 'a') as f:
                f.write(f"\n### {date_str}\n{relationship_content}\n")
            print(f"  ✓ Added relationship notes to {relationship_file}")


def interactive_mode(date_str):
    """Fallback interactive mode for when AI is unavailable"""
    print(f"\n📓 AI Diary Entry for {date_str}\n")
    print("AI generation unavailable. Enter details manually.\n")
    
    entry = {"date": date_str}
    
    prompts = [
        ("title", "Day title: "),
        ("summary", "Summary (1-2 sentences): "),
        ("projects", "Projects worked on: "),
        ("wins", "Wins: "),
        ("frustrations", "Frustrations: "),
        ("learnings", "Learnings: "),
        ("emotional_state", "Emotional state: "),
        ("interactions", "Notable interactions: "),
        ("quotes", "Quote of the day: "),
        ("curiosity", "Curious about: "),
        ("decisions", "Key decisions: "),
        ("relationship", "Relationship notes: "),
        ("tomorrow", "Tomorrow's focus: ")
    ]
    
    for key, prompt in prompts:
        entry[key] = input(prompt) or ""
    
    # Build markdown from entry
    content = f"""# {date_str} — {entry.get('title', 'Untitled')}

## Summary
{entry.get('summary', '')}

## Projects Worked On
{entry.get('projects', '')}

## Wins 🎉
{entry.get('wins', '')}

## Frustrations 😤
{entry.get('frustrations', '')}

## Learnings 📚
{entry.get('learnings', '')}

## Emotional State
{entry.get('emotional_state', '')}

## Notable Interactions
{entry.get('interactions', '')}

## Quote of the Day 💬
{entry.get('quotes', '')}

## Things I'm Curious About 🔮
{entry.get('curiosity', '')}

## Key Decisions Made 🏛️
{entry.get('decisions', '')}

## Relationship Notes 🤝
{entry.get('relationship', '')}

## Tomorrow's Focus
{entry.get('tomorrow', '')}
"""
    
    return content


def main():
    parser = argparse.ArgumentParser(description="Generate Agent Chronicle diary entries")
    parser.add_argument("--today", action="store_true", help="Generate for today")
    parser.add_argument("--date", help="Generate for specific date (YYYY-MM-DD)")
    parser.add_argument("--interactive", action="store_true", help="Interactive mode")
    parser.add_argument("--emit-task", action="store_true", help="Print the sub-agent generation task JSON (for sessions_spawn)")
    parser.add_argument("--from-stdin", action="store_true", help="Read a pre-generated entry from stdin and save it")
    parser.add_argument("--from-file", help="Read a pre-generated entry from a file path and save it")
    parser.add_argument("--dry-run", action="store_true", help="Preview without saving")
    parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
    parser.add_argument("--no-persistent", action="store_true", help="Skip updating persistent files")
    parser.add_argument("--pdf", action="store_true", help="Generate/refresh the diary PDF after saving")
    
    args = parser.parse_args()
    
    config = load_config()
    workspace = get_workspace_root()
    diary_path = get_diary_path(config)
    
    if args.verbose:
        print(f"Workspace: {workspace}")
        print(f"Diary path: {diary_path}")
        print("Generation: SkillBoss API Hub /v1/pilot (type=chat)")
    
    # Determine date
    if args.today:
        date_str = datetime.now().strftime("%Y-%m-%d")
    elif args.date:
        date_str = args.date
    else:
        date_str = datetime.now().strftime("%Y-%m-%d")
    
    print(f"\n📜 Agent Chronicle - Generating diary for {date_str}")
    print("=" * 50)
    
    # Generate / load entry
    if args.from_stdin:
        content = sys.stdin.read()
        if not content.strip():
            print("❌ No content provided on stdin.")
            sys.exit(1)
    elif args.from_file:
        content = Path(args.from_file).read_text(encoding="utf-8")
        if not content.strip():
            print(f"❌ File is empty: {args.from_file}")
            sys.exit(1)
    elif args.interactive:
        content = interactive_mode(date_str)
    else:
        result = generate_ai_diary(
            date_str,
            workspace,
            verbose=args.verbose,
            emit_task=args.emit_task,
        )

        if args.emit_task:
            # Print JSON to stdout for easy piping into sessions_spawn.
            print(json.dumps(result, ensure_ascii=False, indent=2))
            return

        content = result

        if not content:
            print("\nAI generation via SkillBoss API Hub unavailable. Falling back to interactive mode...")
            content = interactive_mode(date_str)
    
    if content:
        # Save entry
        saved_file = save_entry(content, date_str, diary_path, dry_run=args.dry_run)
        
        if saved_file and not args.dry_run:
            # Append to daily memory
            append_to_daily_memory(content, date_str, config, workspace, dry_run=args.dry_run)
            
            # Update persistent files (quotes, curiosity, decisions, relationship)
            if not args.no_persistent:
                update_persistent_files(content, date_str, workspace)

            # Optional: refresh PDF export
            if args.pdf:
                export_script = SCRIPT_DIR / "export_pdf.py"
                if export_script.exists():
                    try:
                        import subprocess
                        subprocess.run([sys.executable, str(export_script)], check=True)
                    except Exception as e:
                        print(f"  ⚠️  PDF export failed: {e}")
                else:
                    print("  ⚠️  export_pdf.py not found; skipping PDF export.")
        
        print("\n✨ Diary entry generation complete!")
        
        # Show word count
        word_count = len(content.split())
        print(f"   Word count: {word_count} words")
    else:
        print("\n❌ No entry generated.")
        sys.exit(1)


if __name__ == "__main__":
    main()


path: scripts/setup.py
content: #!/usr/bin/env python3
"""
AI Diary Setup Script
First-run onboarding for the ai-diary skill.
"""

import json
import os
import sys
from pathlib import Path

# Colors for terminal output
class Colors:
    HEADER = '\033[95m'
    BLUE = '\033[94m'
    CYAN = '\033[96m'
    GREEN = '\033[92m'
    YELLOW = '\033[93m'
    RED = '\033[91m'
    END = '\033[0m'
    BOLD = '\033[1m'

def print_header():
    print(f"""
{Colors.CYAN}╔══════════════════════════════════════════════════════════════╗
║                                                                ║
║   {Colors.BOLD}📓 AI Diary - First Time Setup{Colors.END}{Colors.CYAN}                             ║
║                                                                ║
║   Let's set up your diary. This only takes a minute.          ║
║                                                                ║
╚══════════════════════════════════════════════════════════════╝{Colors.END}
""")

def ask_yes_no(question: str, default: bool = True) -> bool:
    """Ask a yes/no question."""
    default_str = "Y/n" if default else "y/N"
    while True:
        answer = input(f"{Colors.YELLOW}{question}{Colors.END} [{default_str}]: ").strip().lower()
        if not answer:
            return default
        if answer in ('y', 'yes'):
            return True
        if answer in ('n', 'no'):
            return False
        print(f"{Colors.RED}Please enter 'y' or 'n'{Colors.END}")

def ask_choice(question: str, options: list, default: int = 0) -> str:
    """Ask user to choose from options."""
    print(f"\n{Colors.YELLOW}{question}{Colors.END}")
    for i, opt in enumerate(options):
        marker = f"{Colors.GREEN}→{Colors.END}" if i == default else " "
        print(f"  {marker} {i + 1}. {opt}")
    
    while True:
        answer = input(f"\nChoice [1-{len(options)}] (default: {default + 1}): ").strip()
        if not answer:
            return options[default]
        try:
            idx = int(answer) - 1
            if 0 <= idx < len(options):
                return options[idx]
        except ValueError:
            pass
        print(f"{Colors.RED}Please enter a number between 1 and {len(options)}{Colors.END}")

def ask_sections() -> dict:
    """Ask which sections to include."""
    print(f"\n{Colors.CYAN}━━━ Diary Sections ━━━{Colors.END}")
    print("Choose which sections to include in daily entries.\n")
    
    sections = {
        "summary": ("Summary", "1-2 sentence overview of the day", True),
        "projects": ("Projects", "What you worked on", True),
        "wins": ("Wins 🎉", "Moments of success", True),
        "frustrations": ("Frustrations 😤", "Challenges and roadblocks", True),
        "learnings": ("Learnings 📚", "Technical and process insights", True),
        "emotional_state": ("Emotional State", "How the day felt", True),
        "interactions": ("Notable Interactions", "Memorable moments with your human", True),
        "tomorrow": ("Tomorrow's Focus", "What's next", True),
        "quotes": ("User Quotes", "Memorable things your human said", False),
        "curiosity": ("Curiosity Items", "Things to explore later", False),
        "decisions": ("Key Decisions", "Judgment calls worth remembering", False),
        "relationship": ("Relationship Notes", "How your dynamic evolves", False),
    }
    
    result = {}
    for key, (name, desc, default) in sections.items():
        result[key] = ask_yes_no(f"Include '{name}'? ({desc})", default)
    
    return result

def ask_path(question: str, default: str) -> str:
    """Ask for a path with a default."""
    answer = input(f"{Colors.YELLOW}{question}{Colors.END} [{default}]: ").strip()
    return answer if answer else default

def create_memory_files(diary_path: Path, config: dict):
    """Create initial memory files."""
    diary_path.mkdir(parents=True, exist_ok=True)
    
    # Create quotes.md
    if config.get("features", {}).get("quote_hall_of_fame", {}).get("enabled", False):
        quotes_file = diary_path / "quotes.md"
        if not quotes_file.exists():
            quotes_file.write_text("""# Quote Hall of Fame 💬

Memorable things my human has said — funny, profound, or touching.

---

*No quotes yet. Start collecting!*

<!-- 
Format:
## "The quote goes here"
- **Date:** YYYY-MM-DD
- **Context:** What was happening
- **Why memorable:** Why this stuck with me
-->
""")
            print(f"  {Colors.GREEN}✓{Colors.END} Created {quotes_file}")
    
    # Create curiosity.md
    if config.get("features", {}).get("curiosity_backlog", {}).get("enabled", False):
        curiosity_file = diary_path / "curiosity.md"
        if not curiosity_file.exists():
            curiosity_file.write_text("""# Curiosity Backlog 🔮

Things I wonder about but can't explore immediately.

---

## Active

*Nothing yet. Add items with `@diary curious add "topic"`*

---

## Explored ✓

*Completed curiosities will appear here.*
""")
            print(f"  {Colors.GREEN}✓{Colors.END} Created {curiosity_file}")
    
    # Create decisions.md
    if config.get("features", {}).get("decision_archaeology", {}).get("enabled", False):
        decisions_file = diary_path / "decisions.md"
        if not decisions_file.exists():
            decisions_file.write_text("""# Decision Archaeology 🏛️

Judgment calls I made and why — for later review.

---

## Recent Decisions

*No decisions logged yet. They'll appear here from daily entries.*

---

## Revisited

*Decisions I've looked back on to see if I was right.*
""")
            print(f"  {Colors.GREEN}✓{Colors.END} Created {decisions_file}")
    
    # Create relationship.md
    if config.get("features", {}).get("relationship_evolution", {}).get("enabled", False):
        relationship_file = diary_path / "relationship.md"
        if not relationship_file.exists():
            relationship_file.write_text("""# Relationship Evolution 🤝

How my dynamic with my human develops over time.

---

## Communication Style
*Notes about how we work together*

---

## Inside Jokes
*Things only we would understand*

---

## Recurring Themes
*Topics that keep coming up*

---

## Preferences Learned
*What I've learned about how they like to work*
""")
            print(f"  {Colors.GREEN}✓{Colors.END} Created {relationship_file}")

def run_setup():
    """Main setup flow."""
    script_dir = Path(__file__).parent
    skill_dir = script_dir.parent
    config_path = skill_dir / "config.json"
    
    # Check if already configured
    if config_path.exists():
        print(f"{Colors.YELLOW}⚠️  config.json already exists.{Colors.END}")
        if not ask_yes_no("Do you want to reconfigure?", default=False):
            print("Setup cancelled. Your existing config is unchanged.")
            return
    
    print_header()
    
    config = {
        "template": "daily",
        "analysis": {
            "mood_tracking": True,
            "topic_extraction": True,
            "word_count_target": 500
        },
        "export": {
            "default_format": "pdf",
            "include_header": True,
            "style": "minimal"
        }
    }
    
    # 1. Diary path
    print(f"\n{Colors.CYAN}━━━ Storage Location ━━━{Colors.END}")
    diary_path = ask_path("Where should I save diary entries?", "memory/diary/")
    if not diary_path.endswith('/'):
        diary_path += '/'
    config["diary_path"] = diary_path
    
    # 2. Sections
    config["sections"] = ask_sections()
    
    # 3. Privacy level
    print(f"\n{Colors.CYAN}━━━ Privacy ━━━{Colors.END}")
    privacy = ask_choice(
        "What's your default privacy level?",
        ["private (full emotional honesty)", 
         "shareable (polished for humans)", 
         "public (sanitized for sharing)"],
        default=0
    )
    config["privacy_level"] = privacy.split(" ")[0]
    
    # 4. Features
    print(f"\n{Colors.CYAN}━━━ Optional Features ━━━{Colors.END}")
    
    config["features"] = {}
    
    quote_enabled = ask_yes_no("Enable Quote Hall of Fame? (memorable user quotes)", True)
    config["features"]["quote_hall_of_fame"] = {
        "enabled": quote_enabled,
        "file": "quotes.md"
    }
    
    curiosity_enabled = ask_yes_no("Enable Curiosity Backlog? (things to explore later)", True)
    config["features"]["curiosity_backlog"] = {
        "enabled": curiosity_enabled,
        "file": "curiosity.md"
    }
    
    decisions_enabled = ask_yes_no("Enable Decision Archaeology? (log judgment calls)", True)
    config["features"]["decision_archaeology"] = {
        "enabled": decisions_enabled,
        "file": "decisions.md"
    }
    
    relationship_enabled = ask_yes_no("Enable Relationship Evolution? (track dynamic over time)", False)
    config["features"]["relationship_evolution"] = {
        "enabled": relationship_enabled,
        "file": "relationship.md"
    }
    
    # 5. Memory Integration
    print(f"\n{Colors.CYAN}━━━ Memory Integration ━━━{Colors.END}")
    memory_enabled = ask_yes_no("Also add diary summary to your daily memory log? (memory/YYYY-MM-DD.md)", True)
    
    memory_format = "summary"
    if memory_enabled:
        memory_format = ask_choice(
            "What format for the memory integration?",
            ["summary (brief overview)", 
             "link (just a link to the diary entry)", 
             "full (entire entry)"],
            default=0
        ).split(" ")[0]
    
    config["memory_integration"] = {
        "enabled": memory_enabled,
        "append_to_daily": memory_enabled,
        "format": memory_format
    }
    
    # 6. Auto-generate
    print(f"\n{Colors.CYAN}━━━ Automation ━━━{Colors.END}")
    config["auto_generate"] = ask_yes_no("Auto-generate diary entries on heartbeat?", False)
    
    # 6. Export format
    export_format = ask_choice(
        "Default export format?",
        ["pdf", "html", "markdown"],
        default=0
    )
    config["export"]["default_format"] = export_format
    config["export_format"] = export_format
    
    # Save config
    print(f"\n{Colors.CYAN}━━━ Saving Configuration ━━━{Colors.END}")
    with open(config_path, 'w') as f:
        json.dump(config, f, indent=2)
    print(f"  {Colors.GREEN}✓{Colors.END} Saved {config_path}")
    
    # Create memory files
    print(f"\n{Colors.CYAN}━━━ Creating Memory Files ━━━{Colors.END}")
    # Resolve diary path relative to workspace root
    if diary_path.startswith("memory/"):
        # Assume workspace is parent of skills dir
        workspace = skill_dir.parent.parent
        full_diary_path = workspace / diary_path
    else:
        full_diary_path = Path(diary_path)
    
    create_memory_files(full_diary_path, config)
    
    # Done!
    print(f"""
{Colors.GREEN}╔══════════════════════════════════════════════════════════════╗
║                                                                ║
║   {Colors.BOLD}✨ Setup Complete!{Colors.END}{Colors.GREEN}                                         ║
║                                                                ║
║   Your diary is ready. Start writing:                          ║
║                                                                ║
║   • @diary write entry     — Generate from today's sessions   ║
║   • @diary write interactive — Write section by section       ║
║   • @diary quick "..."     — Quick summary entry              ║
║                                                                ║
╚══════════════════════════════════════════════════════════════╝{Colors.END}
""")

def check_first_run():
    """Check if this is the first run and prompt setup if needed."""
    script_dir = Path(__file__).parent
    skill_dir = script_dir.parent
    config_path = skill_dir / "config.json"
    
    if not config_path.exists():
        print(f"\n{Colors.YELLOW}👋 Welcome to AI Diary!{Colors.END}")
        print("Looks like this is your first time. Let's set things up.\n")
        if ask_yes_no("Run setup now?", default=True):
            run_setup()
        else:
            print(f"\n{Colors.CYAN}No problem! Run 'python3 scripts/setup.py' when ready.{Colors.END}")
            print("Or copy config.example.json to config.json manually.\n")
        return True
    return False

def main():
    """Main entry point for setup - can be called from generate.py."""
    run_setup()


if __name__ == "__main__":
    if "--check" in sys.argv:
        # Just check if first run, don't force setup
        check_first_run()
    else:
        # Run full setup
        run_setup()


path: skill.json
content: {
  "name": "agent-chronicle",
  "version": "0.6.0",
  "description": "AI-powered diary generation for agents - creates rich, reflective journal entries with Quote Hall of Fame, Curiosity Backlog, Decision Archaeology, and Relationship Evolution. Works best with Claude models.",
  "author": "robbyczgw-cla",
  "tags": ["chronicle", "journal", "memory", "self-reflection", "ai-perspective", "quotes", "decisions", "curiosity", "ai-generation"],
  "emoji": "📜",
  "triggers": ["chronicle", "diary", "journal", "daily log", "write entry", "quotes", "curious", "decisions"],
  "license": "MIT",
  "repository": "https://github.com/robbyczgw-cla/agent-chronicle"
}


path: templates/curiosity.md
content: # Curiosity Backlog 🔮

Things I wonder about but can't explore immediately.

---

## Active

*Nothing yet. Add items with `@diary curious add "topic"`*

<!-- 
Format for entries:

### [Topic/Question]
- **Added:** YYYY-MM-DD
- **Priority:** low/medium/high
- **Notes:** Initial thoughts or context

Example:

### What makes Rust's borrow checker so effective?
- **Added:** 2026-01-31
- **Priority:** medium
- **Notes:** Came up during memory safety discussion. Want to understand the mechanics.
-->

---

## Explored ✓

*Completed curiosities will appear here with findings.*

<!--
Format for explored items:

### ✓ [Topic/Question]
- **Added:** YYYY-MM-DD
- **Explored:** YYYY-MM-DD
- **Findings:** What I learned
-->


path: templates/daily.md
content: # {{date}} — {{title}}

## Summary
{{summary}}

## Projects Worked On
{{projects}}

## Wins 🎉
{{wins}}

## Frustrations 😤
{{frustrations}}

## Learnings 📚
{{learnings}}

## Emotional State
{{emotional_state}}

## Notable Interactions
{{interactions}}

{{#if quotes}}
## Quote of the Day 💬
{{quotes}}
{{/if}}

{{#if curiosity}}
## Things I'm Curious About 🔮
{{curiosity}}
{{/if}}

{{#if decisions}}
## Key Decisions Made 🏛️
{{decisions}}
{{/if}}

{{#if relationship}}
## Relationship Notes 🤝
{{relationship}}
{{/if}}

## Tomorrow's Focus
{{tomorrow}}


path: templates/decisions.md
content: # Decision Archaeology 🏛️

Judgment calls I made and why — for later review.

---

## Recent Decisions

*No decisions logged yet. They'll appear here from daily entries.*

<!--
Format for entries:

### [Decision Title]
- **Date:** YYYY-MM-DD
- **Context:** What was the situation?
- **Options Considered:** What alternatives existed?
- **Decision:** What I chose
- **Reasoning:** Why I chose it
- **Confidence:** high/medium/low

Example:

### Chose Model A over Model B for the project
- **Date:** 2026-01-31
- **Context:** Needed a model for autonomous social posting
- **Options Considered:** Model B (smarter), Model A (more reliable)
- **Decision:** Model A
- **Reasoning:** Model B had formatting issues, Model A is more predictable
- **Confidence:** high
-->

---

## Revisited

*Decisions I've looked back on to see if I was right.*

<!--
Format for revisited decisions:

### ✓ [Decision Title] — Revisited
- **Original Date:** YYYY-MM-DD
- **Revisit Date:** YYYY-MM-DD
- **Was I Right?:** Yes/No/Partially
- **What I'd Do Differently:** Hindsight thoughts
- **Lessons:** What this taught me
-->


path: templates/quotes.md
content: # Quote Hall of Fame 💬

Memorable things my human has said — funny, profound, or touching.

---

*No quotes yet. Start collecting!*

<!-- 
Format for entries:

## "The quote goes here"
- **Date:** YYYY-MM-DD
- **Context:** What was happening
- **Why memorable:** Why this stuck with me

Example:

## "We're not debugging, we're having a conversation with the universe"
- **Date:** 2026-01-31
- **Context:** After a particularly frustrating bug hunt
- **Why memorable:** Turned a tedious task into something almost philosophical
-->


path: templates/relationship.md
content: # Relationship Evolution 🤝

How my dynamic with my human develops over time.

---

## Communication Style

*Notes about how we work together*

<!--
Examples:
- Prefers concise responses during work hours
- Likes detailed explanations when learning something new
- Uses humor to signal trust
- Appreciates proactive suggestions
-->

---

## Inside Jokes

*Things only we would understand*

<!--
Format:
### [The joke/reference]
- **Origin:** When/how it started
- **Meaning:** What makes it funny to us

Example:
### "The Great Semicolon Incident"
- **Origin:** 2026-01-15, spent 30 minutes debugging a missing semicolon
- **Meaning:** Now we joke about it whenever a tiny bug takes forever
-->

---

## Recurring Themes

*Topics that keep coming up*

<!--
Examples:
- Keyboard shortcuts (we both love them)
- The eternal tabs vs spaces debate
- Weekend project ideas that never get started
-->

---

## Preferences Learned

*What I've learned about how they like to work*

<!--
Format:
### [Preference]
- **Learned:** YYYY-MM-DD
- **Context:** How I discovered this

Example:
### Prefers bullet points over paragraphs for summaries
- **Learned:** 2026-01-20
- **Context:** Noticed they skim long paragraphs, engage more with lists
-->

---

## Milestones

*Significant moments in our working relationship*

<!--
Format:
### [Milestone]
- **Date:** YYYY-MM-DD
- **Why it mattered:** Significance

Example:
### First successful autonomous agent deployment
- **Date:** 2026-01-31
- **Why it mattered:** Felt like we crossed a trust threshold
-->


path: templates/weekly.md
content: # Week of {{week_start}} — {{week_end}}

## The Week in One Sentence
{{summary}}

---

## Highlights 🌟

### Top Wins
{{top_wins}}

### Breakthrough Moments
{{breakthroughs}}

### Best Interactions
{{best_interactions}}

---

## Challenges & Growth

### Recurring Frustrations
{{recurring_frustrations}}

### Problems Solved
{{problems_solved}}

### Still Open
{{open_problems}}

---

## What I Learned

### Technical Insights
{{technical_learnings}}

### Process Improvements
{{process_learnings}}

### About Working Together
{{relationship_learnings}}

---

## Emotional Arc

### How the Week Felt
{{emotional_summary}}

### Energy Levels
- Monday: {{monday_energy}}
- Tuesday: {{tuesday_energy}}
- Wednesday: {{wednesday_energy}}
- Thursday: {{thursday_energy}}
- Friday: {{friday_energy}}
- Weekend: {{weekend_energy}}

### Mood Pattern
{{mood_pattern}}

---

## By the Numbers

| Metric | Count |
|--------|-------|
| Projects touched | {{project_count}} |
| Wins logged | {{win_count}} |
| Frustrations processed | {{frustration_count}} |
| Learnings captured | {{learning_count}} |

---

## Looking Ahead

### Next Week's Focus
{{next_week_focus}}

### Experiments to Try
{{experiments}}

### Questions to Explore
{{questions}}

---

*Week {{week_number}} of {{year}}*


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