
Agent Chronicle
- 1 installs
- 1 repo stars
- Updated March 27, 2026
- robbyczgw-cla/agent-chronicle
Generates rich AI-written diary entries from an agent's perspective with quote collection, mood analytics, weekly digests, and cron auto-generation.
About
Produces 400-600 word reflective journal entries capturing an agent's experience, with features like Quote Hall of Fame, mood analytics and On This Day resurfacing. Developers use it to log and analyze an AI agent's daily activity over time.
- AI-generated entries with quote, curiosity and decision tracking
- Weekly digests, mood analytics and cron auto-generation via --auto
Agent Chronicle by the numbers
- 1 all-time installs (skills.sh)
- Ranked #14,098 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 26, 2026 (Skillselion catalog sync)
npx skills add https://github.com/robbyczgw-cla/agent-chronicle --skill agent-chronicleAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 1 |
| Last updated | March 27, 2026 |
| Repository | robbyczgw-cla/agent-chronicle ↗ |
What it does
Generates rich AI-written diary entries from an agent's perspective with quote collection, mood analytics, weekly digests, and cron auto-generation.
Files
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: Works with any capable model. For best results, we recommend Claude models (Haiku, Sonnet, or Opus).
v0.7.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
- 🔙 "On This Day" Resurfacing - Surfaces entries from 7, 30, and 365 days ago
- 📊 Mood & Pattern Analytics - Emotional trends, topic tracking, win/frustration analysis
- ⏰ Cron Auto-Generation - Automated daily diary generation via
--auto - 📋 Weekly Digest - Synthesized weekly summaries with quotes, wins, and mood trends
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:
python3 scripts/generate.py --todayIf no config.json exists, the setup wizard starts automatically.
Alternatively, run setup manually:
python3 scripts/setup.pyThis 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:
cp config.example.json config.jsonQuick Start
Write Today's Entry
Recommended (v0.6.0+): OpenClaw-native sub-agent generation
This skill no longer makes raw HTTP calls to the Gateway. Instead, have your agent spawn a sub-agent via sessions_spawn using OpenClaw's configured defaults (model, thinking, auth, queueing/backpressure).
Workflow:
1) Emit a generation task JSON (context + prompts):
python3 scripts/generate.py --today --emit-task > $TMP_DIR/chronicle-task.json2) Spawn a sub-agent (inside your agent run):
- Read
$TMP_DIR/chronicle-task.json - Use
sessions_spawnwith a task like: - system:
task.system - user:
task.prompt - ask the sub-agent to output only the final markdown entry
3) Save the generated entry:
# Pipe the sub-agent's markdown output into the saver
python3 scripts/generate.py --today --from-stdinManual fallback: Interactive mode
python3 scripts/generate.py --interactivePreview without saving
python3 scripts/generate.py --today --interactive --dry-runView & Export
# 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.mdEntry 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 entryAnalyzes today's sessions and generates a draft entry.
Interactive mode:
@diary write interactivePrompts 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 todayRead specific date:
@diary read 2026-01-28Weekly summary:
@diary weeklyGenerates a summary of the past 7 days.
Monthly reflection:
@diary monthlyExporting
Export to PDF:
@diary export pdf
@diary export pdf --days 30
@diary export pdf --month januaryExport to HTML:
@diary export html --allAnalysis
Mood trends:
@diary moodShows emotional patterns over time.
Topic frequency:
@diary topicsWhat have we been working on most?
Wins compilation:
@diary winsAll 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 quotesAdd 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 curiousAdd 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 highStorage
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 decisionsView decisions from a specific period:
@diary decisions --days 30Revisit old decisions:
@diary revisitShows 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 relationshipAdd 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
"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:
## 📜 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)
---
---
"On This Day" Resurfacing 🔙
When generating a new diary entry, Agent Chronicle automatically checks for entries from 7 days, 30 days, and 365 days ago. If found, a "Looking Back" section is appended to the entry with a brief highlight or quote from each old entry.
How It Works
- Runs automatically during
generate.py --today,--from-stdin, or--from-file - Reads old entries from the configured
diary_path - Extracts the Summary or first meaningful paragraph as a highlight
- Adds a
## 🔙 Looking Backsection at the end of the new entry
Disable
python3 scripts/generate.py --today --no-looking-back---
Mood & Pattern Analytics 📊
Analyze your diary entries for emotional trends, recurring topics, wins, and frustrations.
Commands
Analyze all entries:
python3 scripts/analyze.pyAnalyze last 7 days:
python3 scripts/analyze.py --days 7Analyze last 30 days:
python3 scripts/analyze.py --days 30Save report to file:
python3 scripts/analyze.py --output mood-report.mdJSON output (for programmatic use):
python3 scripts/analyze.py --jsonWhat It Shows
- Mood Timeline — Sparkline + emoji timeline of daily mood scores
- Mood Distribution — Breakdown of joyful/happy/calm/mixed/frustrated/sad days
- Recurring Topics — Most mentioned technical topics and themes
- Wins Compilation — All recent wins extracted from entries
- Recurring Frustrations — Common pain points
- Insights — Trend detection, best/worst days, win/frustration ratio
Agent Command
@diary mood
@diary topics
@diary wins---
Auto-Generation via Cron ⏰
Generate diary entries automatically on a schedule using OpenClaw cron.
Usage
# Auto-generate: emits task JSON for sub-agent spawning
python3 scripts/generate.py --autoThe --auto flag:
- Uses today's date automatically
- Skips generation if an entry already exists for today
- Emits a sub-agent task JSON (for
sessions_spawn) - Requires no user interaction
Configuration
Enable in config.json:
{
"auto_generate": true
}OpenClaw Cron Setup
Add to your OpenClaw config (~/.openclaw/config.yaml or similar):
cron:
- id: daily-diary
schedule: "0 23 * * *" # Every day at 11 PM
task: |
Generate today's diary entry using agent-chronicle.
Run: python3 /path/to/skills/agent-chronicle/scripts/generate.py --auto
Take the emitted task JSON, spawn a sub-agent with it, then pipe the
result back via: python3 scripts/generate.py --today --from-stdin
channel: telegram # Optional: notify channel on completionOr use the two-step flow in a shell script:
#!/bin/bash
SKILL_DIR="/path/to/skills/agent-chronicle"
TASK=$(python3 "$SKILL_DIR/scripts/generate.py" --auto 2>/dev/null)
# Feed $TASK to your agent for sub-agent spawning---
Weekly Digest 📋
Generate a synthesized weekly summary from your daily entries.
Usage
Generate digest for current week:
python3 scripts/digest.pyGenerate for a specific week (by any date in that week):
python3 scripts/digest.py --date 2026-03-20Emit sub-agent task for AI-powered digest:
python3 scripts/digest.py --emit-taskRead AI-generated digest from stdin:
python3 scripts/digest.py --from-stdinPreview without saving:
python3 scripts/digest.py --dry-runOutput
Saved as YYYY-WXX-weekly.md in the diary directory (e.g., 2026-W13-weekly.md).
What's Included
- Week at a Glance — Overall arc summary
- Top Quotes — Best quotes from the week
- Biggest Wins — Most significant achievements
- Resolved Curiosities — Questions that got answered
- Mood Trend — Emotional arc with sparkline
- Key Decisions — Important judgment calls
- Patterns & Observations — Recurring themes
Agent Command
@diary weeklyCron Setup for Weekly Digest
cron:
- id: weekly-digest
schedule: "0 22 * * 0" # Every Sunday at 10 PM
task: |
Generate this week's diary digest using agent-chronicle.
Run: python3 /path/to/skills/agent-chronicle/scripts/digest.py---
Configuration
config.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:
# {{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
│ ├── 2026-W05-weekly.md # Weekly digest
│ ├── quotes.md # Quote Hall of Fame
│ ├── curiosity.md # Curiosity Backlog
│ ├── decisions.md # Decision Archaeology
│ └── relationship.md # Relationship Evolution
└── ...Scripts
setup.py
# Run first-time setup
python3 scripts/setup.py
# Check if setup needed (for automation)
python3 scripts/setup.py --checkgenerate.py
# From today's sessions
python3 scripts/generate.py --today
# Auto-generate (for cron, no interaction)
python3 scripts/generate.py --auto
# From date range
python3 scripts/generate.py --since 2026-01-28 --until 2026-01-31
# Interactive mode
python3 scripts/generate.py --interactive
# Skip "Looking Back" section
python3 scripts/generate.py --today --no-looking-back
# Dry run (preview only)
python3 scripts/generate.py --today --dry-runanalyze.py
# Mood analytics for all entries
python3 scripts/analyze.py
# Last 7 days
python3 scripts/analyze.py --days 7
# Save report
python3 scripts/analyze.py --output mood-report.md
# JSON output
python3 scripts/analyze.py --jsondigest.py
# Weekly digest for current week
python3 scripts/digest.py
# For a specific week
python3 scripts/digest.py --date 2026-03-20
# Emit sub-agent task
python3 scripts/digest.py --emit-task
# Dry run
python3 scripts/digest.py --dry-runexport.py
# 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.pdfExample Entry
# 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 iterationBest 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
.gitignoreexcludes 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.pyagain
Changelog
v0.7.0
- "On This Day" Resurfacing: Automatically surfaces entries from 7, 30, and 365 days ago as a "Looking Back" section in new entries
- Mood & Pattern Analytics: New
scripts/analyze.py— mood timeline, topic tracking, win/frustration analysis, sparkline visualization - Cron Auto-Generation:
--autoflag for non-interactive daily generation via OpenClaw cron - Weekly Digest: New
scripts/digest.py— synthesized weekly summaries with quotes, wins, decisions, mood trends - New CLI flags:
--no-looking-back,--auto,--json(analyze),--output(analyze)
v0.5.0
- Privacy Cleanup: Removed all hardcoded personal references from prompts
- Dynamic Workspace: All scripts now use environment variables (
OPENCLAW_WORKSPACEorAGENT_WORKSPACE) for workspace detection - OpenClaw Gateway: Removed outdated
ANTHROPIC_API_KEYrequirement - skill uses OpenClaw Gateway 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 (works best with Claude models)
- 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.pynow 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?
{
"version": 1,
"registry": "https://www.clawhub.ai",
"slug": "agent-chronicle",
"installedVersion": "0.5.0",
"installedAt": 1770158681265
}
config.json
.env
.env.*
.cache/
.data/
__pycache__/
*.pyc
__pycache__/
*.pyc
.cache/
.data/
.env
.env.*
!.env.example
node_modules/
*.node
*.so
*.zip
Changelog
[0.7.2] - 2026-03-27
Changed
- Version bump for ClawHub registry sync
[0.7.1] - 2026-03-27
Fixed
- Removed hardcoded
/tmp/chronicle-task.jsonpath in SKILL.md — replaced with$TMP_DIR/chronicle-task.jsonfor portability
[0.7.0] - 2026-03-26
Added
- "On This Day" Resurfacing: When generating a new diary entry, checks for entries from 7, 30, and 365 days ago and adds a "Looking Back" section with highlights
- Mood & Pattern Analytics (
scripts/analyze.py): Mood timeline with sparkline/emoji, mood distribution, recurring topics, wins compilation, frustration tracking, trend insights - Cron Auto-Generation (
--autoflag): Non-interactive diary generation for OpenClaw cron integration; skips if entry already exists - Weekly Digest (
scripts/digest.py): Synthesizes past 7 daily entries into a weekly summary with quotes, wins, decisions, mood trends, and curiosities --no-looking-backflag to skip "On This Day" resurfacing--jsonflag for analyze.py (programmatic output)--outputflag for analyze.py (save report to file)--dateflag for digest.py (generate for a specific week)--emit-taskand--from-stdinflags for digest.py (sub-agent workflow)- OpenClaw cron setup documentation in SKILL.md
Changed
- Version bumped to 0.7.0
- Updated SKILL.md with comprehensive documentation for all new features
- Updated README.md with new features
[0.6.5] - 2026-03-26
Fixed
- config.json now uses relative
diary_path: memory/diary/instead of absolute/root/clawd/... memory_integration.enableddefaults tofalse(opt-in, not opt-out)- Addresses ClawHub security review: no absolute system paths shipped in config
[0.6.4] - 2026-03-26
Changed
- README and CHANGELOG synchronized across ClawHub, GitHub, and local
- Version badges updated
[0.6.3] - 2026-03-26
Added
--month YYYY-MMflag: export only entries from a specific month--allflag: explicit alias for default behavior- Default output filename when
--monthis set:Cami-Diary-YYYY-MM.pdf
Changed
- PDF export cron now generates monthly PDFs (smaller, faster)
[0.6.2] - 2026-03-03
Changed
- Synced changelog/docs and aligned package metadata
[0.6.1] - 2026-02-11
Added
.gitignoreto exclude cache, config, and export files- Updated metadata to openclaw format
- Documentation consistency updates
[0.6.0] - 2026-02-05
Changed
- Removed raw HTTP calls to Gateway from scripts/generate.py
- Diary generation now uses sessions_spawn (OpenClaw-native)
- Added --emit-task CLI flag
{
"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"
}
}
{
"_note": "diary_path is relative to your agent workspace. Run python3 scripts/setup.py to configure for your environment.",
"diary_path": "memory/diary/",
"export_format": "pdf",
"privacy_level": "private",
"auto_generate": true,
"template": "daily",
"memory_integration": {
"enabled": false,
"append_to_daily": false,
"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"
}
}
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_spawnpattern inspiration via 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.
{
"name": "@openclaw/agent-chronicle",
"version": "0.7.2",
"description": "Chronicle your AI's journey - capture experiences, quotes, curiosities, decisions, relationships, mood analytics, weekly digests, and 'On This Day' resurfacing 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": "\ud83d\udcdc",
"triggers": [
"chronicle",
"diary",
"journal",
"daily log",
"write entry",
"quotes",
"curious",
"decisions"
],
"capabilities": [
"journal-generation",
"quote-tracking",
"curiosity-backlog",
"decision-archaeology",
"mood-analytics",
"weekly-digest",
"on-this-day",
"cron-auto-generation"
],
"requirements": {
"bins": [
"python3"
],
"env": {}
}
},
"files": [
"SKILL.md",
"README.md",
"config.example.json",
"scripts/",
"templates/"
]
}lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.: {}
Agent Chronicle 📜
AI perspective journaling — document daily experiences, emotions, and learnings from the agent's viewpoint.
 
What's New in v0.7.0
- 🔙 "On This Day" Resurfacing — automatically surfaces entries from 7, 30, and 365 days ago as a "Looking Back" section
- 📊 Mood & Pattern Analytics —
scripts/analyze.pywith mood timelines, sparklines, topic tracking, and win/frustration analysis - ⏰ Cron Auto-Generation —
--autoflag for non-interactive daily diary generation via OpenClaw cron - 📋 Weekly Digest —
scripts/digest.pysynthesizes 7 daily entries into a weekly summary with quotes, wins, mood trends, and decisions
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
# 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
# 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 7Note: If noconfig.jsonexists,generate.pyautomatically 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 quoteCuriosity 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 exploredDecision 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 summaryMemory 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 memorylink— Just a link to the full diary entryfull— Entire entry embedded in daily memory
Enabled by default during setup. Configure in config.json:
"memory_integration": {
"enabled": true,
"append_to_daily": true,
"format": "summary"
}Example Entry
# 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 entrytemplates/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 |
analyze.py | Mood & pattern analytics across entries |
digest.py | Weekly digest generation |
export.py | Export to PDF/HTML using pandoc |
export_pdf.py | Beautiful PDF export (Velvet Edition) |
Configuration
{
"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 honestyshareable— Polished for humanspublic— 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 EvolutionRequirements
- 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 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 for the full list of contributors.
License
MIT
Author
Created by robbyczgw-cla for AI agents who want to remember.
#!/usr/bin/env python3
"""
Mood & Pattern Analytics for Agent Chronicle - v0.7.0
Reads diary entries and extracts mood/emotional keywords, builds a mood timeline,
identifies recurring topics, frustrations, wins, and outputs a markdown summary.
Usage:
python3 scripts/analyze.py # Analyze all entries
python3 scripts/analyze.py --days 7 # Analyze last 7 days
python3 scripts/analyze.py --days 30 # Analyze last 30 days
python3 scripts/analyze.py --output mood-report.md # Save to file
"""
import argparse
import json
import os
import re
from collections import Counter
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/"
# Mood/emotion keyword mappings
MOOD_KEYWORDS = {
"joyful": {
"emoji": "😄",
"score": 5,
"words": [
"excited", "thrilled", "elated", "ecstatic", "delighted",
"overjoyed", "euphoric", "fantastic", "amazing", "wonderful",
"incredible", "brilliant", "celebration", "triumphant",
],
},
"happy": {
"emoji": "😊",
"score": 4,
"words": [
"happy", "satisfied", "pleased", "glad", "good", "great",
"productive", "positive", "energized", "motivated", "proud",
"accomplished", "rewarding", "fulfilling", "shipped", "launched",
"win", "wins", "success", "breakthrough", "milestone",
],
},
"calm": {
"emoji": "😌",
"score": 3,
"words": [
"calm", "peaceful", "relaxed", "steady", "balanced", "quiet",
"routine", "normal", "stable", "comfortable", "mundane",
"ordinary", "rest", "uneventful",
],
},
"mixed": {
"emoji": "😐",
"score": 2,
"words": [
"mixed", "bittersweet", "conflicted", "uncertain", "ambivalent",
"okay", "alright", "fine", "so-so", "mediocre",
],
},
"frustrated": {
"emoji": "😤",
"score": 1,
"words": [
"frustrated", "annoyed", "irritated", "stuck", "blocked",
"struggling", "difficult", "challenging", "bug", "bugs",
"broken", "failed", "failure", "error", "errors", "issue",
"issues", "problem", "problems", "confusing", "tedious",
],
},
"sad": {
"emoji": "😔",
"score": 0,
"words": [
"sad", "disappointed", "down", "tired", "exhausted",
"drained", "overwhelmed", "burned out", "burnout", "lonely",
"discouraged", "demotivated", "rough",
],
},
}
# Topic extraction patterns
TOPIC_PATTERNS = [
# Technical topics
r"\b(python|javascript|typescript|rust|go|java|ruby|swift|kotlin)\b",
r"\b(api|apis|rest|graphql|websocket|grpc)\b",
r"\b(docker|kubernetes|k8s|container|deployment|ci/cd|github actions)\b",
r"\b(database|sql|postgres|mysql|redis|mongodb|sqlite)\b",
r"\b(react|vue|svelte|angular|nextjs|nuxt)\b",
r"\b(ai|llm|gpt|claude|model|prompt|embedding|vector)\b",
r"\b(openclaw|clawhub|skill|skills|plugin|plugins)\b",
r"\b(git|github|pr|pull request|merge|branch|commit)\b",
r"\b(testing|test|tests|debug|debugging|refactor|refactoring)\b",
r"\b(security|auth|authentication|authorization|encryption)\b",
r"\b(performance|optimization|caching|latency|speed)\b",
r"\b(documentation|docs|readme|changelog)\b",
]
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 (where memory/ lives)"""
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
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 load_entries(diary_path, days=None):
"""Load diary entries, optionally filtering by recent days"""
entries = {}
md_files = sorted(diary_path.glob("*.md"))
# Filter to only dated diary entries (YYYY-MM-DD.md), skip special files
dated_files = [
f for f in md_files if re.match(r"\d{4}-\d{2}-\d{2}$", f.stem)
]
if days is not None:
cutoff = datetime.now() - timedelta(days=days)
cutoff_str = cutoff.strftime("%Y-%m-%d")
dated_files = [f for f in dated_files if f.stem >= cutoff_str]
for f in dated_files:
entries[f.stem] = f.read_text()
return entries
def extract_section(content, section_name):
"""Extract a section's content from a diary entry"""
pattern = rf"##\s*{re.escape(section_name)}.*?\n(.*?)(?=\n##|\Z)"
match = re.search(pattern, content, re.DOTALL | re.IGNORECASE)
if match:
return match.group(1).strip()
return ""
def analyze_mood(content):
"""Analyze mood of a single entry, returns (dominant_mood, score, keyword_counts)"""
content_lower = content.lower()
mood_counts = {}
for mood, info in MOOD_KEYWORDS.items():
count = 0
for word in info["words"]:
count += len(re.findall(r"\b" + re.escape(word) + r"\b", content_lower))
if count > 0:
mood_counts[mood] = count
if not mood_counts:
return "calm", 3, {}
# Weight by section: emotional state section counts 3x
emotional_section = extract_section(content, "Emotional State")
if emotional_section:
emo_lower = emotional_section.lower()
for mood, info in MOOD_KEYWORDS.items():
bonus = 0
for word in info["words"]:
bonus += len(re.findall(r"\b" + re.escape(word) + r"\b", emo_lower))
if bonus > 0:
mood_counts[mood] = mood_counts.get(mood, 0) + bonus * 2 # 2x bonus
dominant = max(mood_counts, key=mood_counts.get)
score = MOOD_KEYWORDS[dominant]["score"]
return dominant, score, mood_counts
def extract_topics(content):
"""Extract topics mentioned in an entry"""
content_lower = content.lower()
topics = []
for pattern in TOPIC_PATTERNS:
matches = re.findall(pattern, content_lower)
topics.extend(matches)
return topics
def extract_wins(content):
"""Extract wins from an entry"""
wins_section = extract_section(content, "Wins 🎉") or extract_section(content, "Wins")
if not wins_section:
return []
wins = []
for line in wins_section.split("\n"):
line = line.strip()
if line and line.startswith(("-", "*", "•")):
win = re.sub(r"^[-*•]\s*", "", line).strip()
if win and len(win) > 5:
wins.append(win)
elif line and not line.startswith("#"):
# Paragraph-style wins - take first sentence
sentences = re.split(r"[.!?]", line)
if sentences and len(sentences[0].strip()) > 5:
wins.append(sentences[0].strip())
return wins
def extract_frustrations(content):
"""Extract frustrations from an entry"""
frust_section = (
extract_section(content, "Frustrations 😤")
or extract_section(content, "Frustrations")
)
if not frust_section:
return []
frustrations = []
for line in frust_section.split("\n"):
line = line.strip()
if line and line.startswith(("-", "*", "•")):
frust = re.sub(r"^[-*•]\s*", "", line).strip()
if frust and len(frust) > 5:
frustrations.append(frust)
elif line and not line.startswith("#"):
sentences = re.split(r"[.!?]", line)
if sentences and len(sentences[0].strip()) > 5:
frustrations.append(sentences[0].strip())
return frustrations
def build_sparkline(scores):
"""Build a text-based sparkline from mood scores (0-5)"""
blocks = ["▁", "▂", "▃", "▅", "▆", "█"]
return "".join(blocks[min(s, 5)] for s in scores)
def build_emoji_timeline(moods):
"""Build an emoji-based mood timeline"""
return " ".join(MOOD_KEYWORDS.get(m, {}).get("emoji", "❓") for m in moods)
def generate_report(entries, days_label=None):
"""Generate a full mood & pattern analytics report"""
if not entries:
return "# Mood & Pattern Analytics\n\nNo diary entries found to analyze.\n"
# Analyze all entries
daily_moods = {} # date -> (mood, score, counts)
all_topics = Counter()
all_wins = []
all_frustrations = []
for date_str in sorted(entries.keys()):
content = entries[date_str]
mood, score, counts = analyze_mood(content)
daily_moods[date_str] = (mood, score, counts)
topics = extract_topics(content)
all_topics.update(topics)
wins = extract_wins(content)
all_wins.extend([(date_str, w) for w in wins])
frustrations = extract_frustrations(content)
all_frustrations.extend([(date_str, f) for f in frustrations])
# Build report
dates = sorted(daily_moods.keys())
scores = [daily_moods[d][1] for d in dates]
moods = [daily_moods[d][0] for d in dates]
period = days_label or "all time"
avg_score = sum(scores) / len(scores) if scores else 0
avg_mood = (
"joyful" if avg_score >= 4.5
else "happy" if avg_score >= 3.5
else "calm" if avg_score >= 2.5
else "mixed" if avg_score >= 1.5
else "frustrated" if avg_score >= 0.5
else "sad"
)
avg_emoji = MOOD_KEYWORDS[avg_mood]["emoji"]
# Mood distribution
mood_dist = Counter(moods)
report = []
report.append(f"# 📊 Mood & Pattern Analytics")
report.append(f"")
report.append(f"**Period:** {period} ({len(entries)} entries)")
report.append(f"**Average Mood:** {avg_emoji} {avg_mood} ({avg_score:.1f}/5)")
report.append(f"")
# Mood Timeline
report.append(f"## 📈 Mood Timeline")
report.append(f"")
if len(dates) <= 30:
# Show individual dates
report.append(f"```")
report.append(f"Sparkline: {build_sparkline(scores)}")
report.append(f"```")
report.append(f"")
report.append(f"{build_emoji_timeline(moods)}")
report.append(f"")
for date_str in dates:
mood, score, _ = daily_moods[date_str]
emoji = MOOD_KEYWORDS[mood]["emoji"]
bar = "█" * score + "░" * (5 - score)
report.append(f"- `{date_str}` {emoji} {bar} {mood}")
else:
# Summarize by week for large datasets
report.append(f"```")
report.append(f"Sparkline: {build_sparkline(scores)}")
report.append(f"```")
report.append(f"")
report.append(f"{build_emoji_timeline(moods)}")
report.append(f"")
# Weekly averages
week_scores = {}
for date_str, (mood, score, _) in daily_moods.items():
try:
dt = datetime.strptime(date_str, "%Y-%m-%d")
week_key = f"{dt.year}-W{dt.isocalendar()[1]:02d}"
except ValueError:
continue
week_scores.setdefault(week_key, []).append(score)
report.append(f"### Weekly Averages")
for week, week_s in sorted(week_scores.items()):
avg = sum(week_s) / len(week_s)
avg_m = (
"joyful" if avg >= 4.5
else "happy" if avg >= 3.5
else "calm" if avg >= 2.5
else "mixed" if avg >= 1.5
else "frustrated" if avg >= 0.5
else "sad"
)
emoji = MOOD_KEYWORDS[avg_m]["emoji"]
report.append(f"- `{week}` {emoji} {avg:.1f}/5 ({len(week_s)} entries)")
report.append(f"")
# Mood Distribution
report.append(f"## 🎭 Mood Distribution")
report.append(f"")
for mood_name in ["joyful", "happy", "calm", "mixed", "frustrated", "sad"]:
count = mood_dist.get(mood_name, 0)
if count > 0:
pct = count / len(moods) * 100
emoji = MOOD_KEYWORDS[mood_name]["emoji"]
bar = "▓" * int(pct / 5) + "░" * (20 - int(pct / 5))
report.append(f"- {emoji} **{mood_name}**: {bar} {count} ({pct:.0f}%)")
report.append(f"")
# Top Topics
if all_topics:
report.append(f"## 🏷️ Recurring Topics")
report.append(f"")
for topic, count in all_topics.most_common(15):
report.append(f"- **{topic}** — mentioned {count} time{'s' if count > 1 else ''}")
report.append(f"")
# Top Wins
if all_wins:
report.append(f"## 🏆 Wins Compilation")
report.append(f"")
# Show most recent wins (up to 10)
recent_wins = all_wins[-10:]
for date_str, win in reversed(recent_wins):
# Truncate long wins
display_win = win[:120] + "..." if len(win) > 120 else win
report.append(f"- `{date_str}` {display_win}")
if len(all_wins) > 10:
report.append(f"- *...and {len(all_wins) - 10} more wins*")
report.append(f"")
# Top Frustrations
if all_frustrations:
report.append(f"## 😤 Recurring Frustrations")
report.append(f"")
# Look for recurring themes in frustrations
frust_words = Counter()
for _, frust in all_frustrations:
words = re.findall(r"\b[a-z]{4,}\b", frust.lower())
frust_words.update(words)
# Show recent frustrations
recent_frust = all_frustrations[-8:]
for date_str, frust in reversed(recent_frust):
display_frust = frust[:120] + "..." if len(frust) > 120 else frust
report.append(f"- `{date_str}` {display_frust}")
if len(all_frustrations) > 8:
report.append(f"- *...and {len(all_frustrations) - 8} more frustrations*")
report.append(f"")
# Insights
report.append(f"## 💡 Insights")
report.append(f"")
# Trend detection
if len(scores) >= 3:
recent_avg = sum(scores[-3:]) / 3
older_avg = sum(scores[:-3]) / max(len(scores) - 3, 1) if len(scores) > 3 else recent_avg
if recent_avg > older_avg + 0.5:
report.append(f"- 📈 **Upward trend** — mood has been improving recently")
elif recent_avg < older_avg - 0.5:
report.append(f"- 📉 **Downward trend** — mood has dipped recently")
else:
report.append(f"- ➡️ **Stable mood** — relatively consistent emotional state")
# Best/worst days
if scores:
best_idx = scores.index(max(scores))
worst_idx = scores.index(min(scores))
best_date = dates[best_idx]
worst_date = dates[worst_idx]
best_emoji = MOOD_KEYWORDS[moods[best_idx]]["emoji"]
worst_emoji = MOOD_KEYWORDS[moods[worst_idx]]["emoji"]
report.append(f"- {best_emoji} **Best day:** {best_date}")
report.append(f"- {worst_emoji} **Toughest day:** {worst_date}")
# Win/frustration ratio
if all_wins or all_frustrations:
ratio = len(all_wins) / max(len(all_frustrations), 1)
if ratio > 2:
report.append(f"- 🎯 **Win ratio: {ratio:.1f}x** — significantly more wins than frustrations!")
elif ratio > 1:
report.append(f"- ⚖️ **Win ratio: {ratio:.1f}x** — slightly more wins than frustrations")
else:
report.append(f"- 🔧 **Win ratio: {ratio:.1f}x** — more frustrations than wins, consider what's blocking you")
report.append(f"")
report.append(f"---")
report.append(f"*Generated by Agent Chronicle v0.7.0 on {datetime.now().strftime('%Y-%m-%d %H:%M')}*")
return "\n".join(report)
def main():
parser = argparse.ArgumentParser(
description="Mood & Pattern Analytics for Agent Chronicle"
)
parser.add_argument(
"--days", "-d", type=int, help="Analyze last N days (default: all)"
)
parser.add_argument(
"--output", "-o", help="Save report to file (default: print to stdout)"
)
parser.add_argument(
"--json", action="store_true", help="Output raw analysis as JSON"
)
args = parser.parse_args()
config = load_config()
diary_path = get_diary_path(config)
if not diary_path.exists():
print(f"Diary path not found: {diary_path}")
print("Run setup first or check config.json diary_path setting.")
return
entries = load_entries(diary_path, days=args.days)
if not entries:
period = f"last {args.days} days" if args.days else "all time"
print(f"No diary entries found for {period} in {diary_path}")
return
days_label = f"last {args.days} days" if args.days else "all time"
if args.json:
# Raw JSON output for programmatic use
results = {}
for date_str in sorted(entries.keys()):
mood, score, counts = analyze_mood(entries[date_str])
topics = extract_topics(entries[date_str])
results[date_str] = {
"mood": mood,
"score": score,
"topics": topics,
"wins": extract_wins(entries[date_str]),
"frustrations": extract_frustrations(entries[date_str]),
}
print(json.dumps(results, indent=2))
return
report = generate_report(entries, days_label)
if args.output:
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(report)
print(f"✓ Report saved to {output_path}")
else:
print(report)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Weekly Digest for Agent Chronicle - v0.7.0
Reads the past 7 daily entries and synthesizes a weekly summary including
top quotes, biggest wins, resolved curiosities, mood trend, and key decisions.
Output: YYYY-WXX-weekly.md in the diary directory.
Usage:
python3 scripts/digest.py # Generate digest for current week
python3 scripts/digest.py --date 2026-03-20 # Generate digest for the week containing this date
python3 scripts/digest.py --emit-task # Emit sub-agent task JSON
python3 scripts/digest.py --from-stdin # Read pre-generated digest from stdin
python3 scripts/digest.py --dry-run # Preview without saving
"""
import argparse
import json
import os
import re
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/"
AI_MAX_TOKENS = 3000
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 (where memory/ lives)"""
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
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 get_week_range(reference_date=None):
"""Get the Monday-to-Sunday date range for the week containing reference_date.
Returns (start_date, end_date, iso_year, iso_week)."""
if reference_date is None:
reference_date = datetime.now()
# ISO weekday: Monday=1, Sunday=7
start = reference_date - timedelta(days=reference_date.weekday())
end = start + timedelta(days=6)
iso_year, iso_week, _ = reference_date.isocalendar()
return start, end, iso_year, iso_week
def load_week_entries(diary_path, start_date, end_date):
"""Load diary entries for a given date range"""
entries = {}
current = start_date
while current <= end_date:
date_str = current.strftime("%Y-%m-%d")
entry_file = diary_path / f"{date_str}.md"
if entry_file.exists():
entries[date_str] = entry_file.read_text()
current += timedelta(days=1)
return entries
def extract_section(content, section_name):
"""Extract a section's content from a diary entry"""
pattern = rf"##\s*{re.escape(section_name)}.*?\n(.*?)(?=\n##|\Z)"
match = re.search(pattern, content, re.DOTALL | re.IGNORECASE)
if match:
return match.group(1).strip()
return ""
def extract_quotes(entries):
"""Extract all quotes from entries"""
quotes = []
for date_str, content in sorted(entries.items()):
quote_section = extract_section(content, "Quote of the Day 💬")
if not quote_section:
quote_section = extract_section(content, "Quote of the Day")
if quote_section and len(quote_section) > 10:
quotes.append((date_str, quote_section.strip()))
return quotes
def extract_wins(entries):
"""Extract all wins from entries"""
wins = []
for date_str, content in sorted(entries.items()):
wins_section = extract_section(content, "Wins 🎉") or extract_section(content, "Wins")
if wins_section:
for line in wins_section.split("\n"):
line = line.strip()
if line and line.startswith(("-", "*", "•")):
win = re.sub(r"^[-*•]\s*", "", line).strip()
if win and len(win) > 5:
wins.append((date_str, win))
elif line and not line.startswith("#") and len(line) > 10:
# Paragraph-style: take first sentence
sentences = re.split(r"[.!?]", line)
if sentences and len(sentences[0].strip()) > 5:
wins.append((date_str, sentences[0].strip()))
return wins
def extract_decisions(entries):
"""Extract key decisions from entries"""
decisions = []
for date_str, content in sorted(entries.items()):
dec_section = (
extract_section(content, "Key Decisions Made 🏛️")
or extract_section(content, "Key Decisions Made 🏛")
or extract_section(content, "Key Decisions Made")
)
if dec_section:
for line in dec_section.split("\n"):
line = line.strip()
if line and line.startswith(("-", "*", "•")):
dec = re.sub(r"^[-*•]\s*", "", line).strip()
if dec and len(dec) > 5:
decisions.append((date_str, dec))
elif line and not line.startswith("#") and len(line) > 10:
decisions.append((date_str, line.strip()))
return decisions
def extract_curiosities(entries):
"""Extract curiosities from entries"""
curiosities = []
for date_str, content in sorted(entries.items()):
cur_section = (
extract_section(content, "Things I'm Curious About 🔮")
or extract_section(content, "Things I'm Curious About")
)
if cur_section:
for line in cur_section.split("\n"):
line = line.strip()
if line and line.startswith(("-", "*", "•")):
cur = re.sub(r"^[-*•]\s*", "", line).strip()
if cur and len(cur) > 5:
curiosities.append((date_str, cur))
return curiosities
def build_digest_task(week_label, start_str, end_str, entries):
"""Build a sub-agent task for generating the weekly digest via sessions_spawn."""
# Build context from all entries
context_parts = []
for date_str in sorted(entries.keys()):
content = entries[date_str]
# Truncate individual entries for context
if len(content) > 4000:
content = content[:4000] + "\n[... truncated ...]"
context_parts.append(f"### {date_str}\n{content}")
context = "\n\n---\n\n".join(context_parts)
system_prompt = """You are an AI assistant writing your weekly diary digest. You work closely with your human partner.
Your weekly digests are:
- A synthesis of the week's daily entries (not just concatenation)
- Reflective and pattern-aware
- Highlighting the week's arc: how it started, evolved, and ended
- Written in first person, as your personal weekly review"""
user_prompt = f"""Write your weekly digest for {week_label} ({start_str} to {end_str}).
Based on the following daily diary entries:
{context}
---
Write a RICH weekly synthesis with these sections:
# {week_label} — Weekly Digest
## Week at a Glance
2-3 sentences summarizing the overall arc of the week.
## Top Quotes 💬
The best/most memorable quotes from this week's entries.
## Biggest Wins 🏆
The most significant achievements this week, with context.
## Resolved Curiosities 🔮
Any questions from earlier in the week that got answered.
## Mood Trend 📈
How did your emotional state evolve over the week? Describe the arc.
## Key Decisions 🏛️
Important judgment calls made this week and their outcomes so far.
## Patterns & Observations 🔍
What patterns did you notice this week? Recurring themes, habits, changes?
## Looking Ahead 🔭
What's on the horizon for next week?
---
Write naturally and reflectively. This is your personal weekly review."""
return {
"system": system_prompt,
"prompt": user_prompt,
"max_tokens": AI_MAX_TOKENS,
}
def build_simple_digest(week_label, start_str, end_str, entries):
"""Build a simple digest without AI when no sub-agent is available"""
quotes = extract_quotes(entries)
wins = extract_wins(entries)
decisions = extract_decisions(entries)
curiosities = extract_curiosities(entries)
parts = []
parts.append(f"# {week_label} — Weekly Digest")
parts.append(f"")
parts.append(f"**Period:** {start_str} to {end_str}")
parts.append(f"**Entries:** {len(entries)}")
parts.append(f"")
# Mood trend (use analyze module if available)
try:
from analyze import analyze_mood, MOOD_KEYWORDS, build_sparkline
scores = []
moods = []
for date_str in sorted(entries.keys()):
mood, score, _ = analyze_mood(entries[date_str])
scores.append(score)
moods.append(mood)
if scores:
avg = sum(scores) / len(scores)
sparkline = build_sparkline(scores)
emoji_line = " ".join(
MOOD_KEYWORDS.get(m, {}).get("emoji", "❓") for m in moods
)
parts.append(f"## 📈 Mood Trend")
parts.append(f"")
parts.append(f"```")
parts.append(f"{sparkline}")
parts.append(f"```")
parts.append(f"{emoji_line}")
parts.append(f"")
parts.append(f"Average mood: {avg:.1f}/5")
parts.append(f"")
except Exception:
pass
# Top Quotes
if quotes:
parts.append(f"## 💬 Top Quotes")
parts.append(f"")
for date_str, quote in quotes:
parts.append(f"### {date_str}")
parts.append(f"{quote}")
parts.append(f"")
# Biggest Wins
if wins:
parts.append(f"## 🏆 Biggest Wins")
parts.append(f"")
for date_str, win in wins:
display = win[:150] + "..." if len(win) > 150 else win
parts.append(f"- `{date_str}` {display}")
parts.append(f"")
# Key Decisions
if decisions:
parts.append(f"## 🏛️ Key Decisions")
parts.append(f"")
for date_str, dec in decisions:
display = dec[:150] + "..." if len(dec) > 150 else dec
parts.append(f"- `{date_str}` {display}")
parts.append(f"")
# Curiosities
if curiosities:
parts.append(f"## 🔮 Curiosities This Week")
parts.append(f"")
for date_str, cur in curiosities:
display = cur[:150] + "..." if len(cur) > 150 else cur
parts.append(f"- `{date_str}` {display}")
parts.append(f"")
parts.append(f"---")
parts.append(
f"*Generated by Agent Chronicle v0.7.0 on {datetime.now().strftime('%Y-%m-%d %H:%M')}*"
)
return "\n".join(parts)
def main():
parser = argparse.ArgumentParser(description="Generate Agent Chronicle weekly digest")
parser.add_argument(
"--date",
help="Generate digest for the week containing this date (YYYY-MM-DD). Default: current week.",
)
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 digest from stdin and save it",
)
parser.add_argument(
"--from-file",
help="Read a pre-generated digest 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")
args = parser.parse_args()
config = load_config()
diary_path = get_diary_path(config)
# Determine reference date
if args.date:
try:
ref_date = datetime.strptime(args.date, "%Y-%m-%d")
except ValueError:
print(f"❌ Invalid date format: {args.date} (expected YYYY-MM-DD)")
sys.exit(1)
else:
ref_date = datetime.now()
start_date, end_date, iso_year, iso_week = get_week_range(ref_date)
start_str = start_date.strftime("%Y-%m-%d")
end_str = end_date.strftime("%Y-%m-%d")
week_label = f"{iso_year}-W{iso_week:02d}"
output_filename = f"{week_label}-weekly.md"
print(f"\n📜 Agent Chronicle — Weekly Digest")
print(f" Week: {week_label} ({start_str} to {end_str})")
print(f"=" * 50)
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)
else:
entries = load_week_entries(diary_path, start_date, end_date)
if not entries:
print(f"\n❌ No diary entries found for {week_label} ({start_str} to {end_str})")
sys.exit(1)
print(f" Found {len(entries)} entries")
if args.emit_task:
task = build_digest_task(week_label, start_str, end_str, entries)
print(json.dumps(task, ensure_ascii=False, indent=2))
return
# Build simple digest (AI-powered one comes from sub-agent via --from-stdin)
content = build_simple_digest(week_label, start_str, end_str, entries)
# Save
output_file = diary_path / output_filename
if args.dry_run:
print(f"\n--- DRY RUN: Would save to {output_file}")
print("-" * 50)
print(content)
print("-" * 50)
return
output_file.write_text(content)
print(f"\n✓ Saved weekly digest to {output_file}")
word_count = len(content.split())
print(f" Word count: {word_count} words")
print(f"\n✨ Weekly digest generation complete!")
if __name__ == "__main__":
main()
#!/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, month: str = None):
"""Load and return sorted diary entries, optionally filtered by month (YYYY-MM)"""
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)]
if month:
dated = [f for f in dated if f.stem.startswith(month + "-")]
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 & Lato<br/>
Crafted with care by OpenClaw
</div>
<div class="colophon-generated">
Generated on {generated}
</div>
</section>
</body>
</html>
'''
return html
def export_pdf(output_path: Path, month: str = None):
"""Export diary entries to a beautiful PDF, optionally filtered by month (YYYY-MM)"""
config = load_config()
diary_path = get_diary_path(config)
entries = load_entries(diary_path, month=month)
if not entries:
if month:
print(f"No diary entries found for {month} in {diary_path}")
else:
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))
month_label = f" ({month})" if month else ""
print(f"✓ Exported PDF to {output_path}")
print(f" {len(entries)} entries{month_label} • 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")
parser.add_argument(
"--month",
metavar="YYYY-MM",
help="Export only entries from this month (e.g. 2026-03). Defaults output to Cami-Diary-YYYY-MM.pdf",
)
parser.add_argument(
"--all",
action="store_true",
help="Export all entries (default behavior when --month is not specified)",
)
args = parser.parse_args()
# Validate --month format if provided
if args.month and not re.match(r"^\d{4}-\d{2}$", args.month):
parser.error(f"--month must be in YYYY-MM format, got: {args.month!r}")
config = load_config()
diary_path = get_diary_path(config)
# Determine output path
if args.output:
output_path = Path(args.output)
elif args.month:
output_path = diary_path / f"Cami-Diary-{args.month}.pdf"
else:
output_path = diary_path / DEFAULT_OUTPUT_NAME
if args.debug_html:
entries = load_entries(diary_path, month=args.month)
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, month=args.month)
if __name__ == "__main__":
main()
#!/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()
#!/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()
{
"name": "agent-chronicle",
"version": "0.7.0",
"description": "AI-powered diary generation for agents - creates rich, reflective journal entries with Quote Hall of Fame, Curiosity Backlog, Decision Archaeology, Relationship Evolution, mood analytics, weekly digests, and On This Day resurfacing. 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",
"mood",
"weekly digest",
"digest"
],
"license": "MIT",
"repository": "https://github.com/robbyczgw-cla/agent-chronicle"
}
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
-->
{{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}}
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
-->
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
-->
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
-->
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}}