Back to skills
SkillHub ClubShip Full StackFull Stack

phoenixclaw

Passive journaling skill that scans daily conversations from ALL session paths (main, agents, cron) via cron to generate markdown journals using semantic understanding. Use when: - User requests journaling ("Show me my journal", "What did I do today?") - User asks for pattern analysis ("Analyze my patterns", "How am I doing?") - User requests summaries ("Generate weekly/monthly summary")

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
3,111
Hot score
99
Updated
March 20, 2026
Overall rating
C4.0
Composite score
4.0
Best-practice grade
B71.9

Install command

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

Repository

openclaw/skills

Skill path: skills/goforu/phoenixclaw

Passive journaling skill that scans daily conversations from ALL session paths (main, agents, cron) via cron to generate markdown journals using semantic understanding. Use when: - User requests journaling ("Show me my journal", "What did I do today?") - User asks for pattern analysis ("Analyze my patterns", "How am I doing?") - User requests summaries ("Generate weekly/monthly summary")

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: openclaw.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install phoenixclaw into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/openclaw/skills before adding phoenixclaw to shared team environments
  • Use phoenixclaw for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: phoenixclaw
description: |
  Passive journaling skill that scans daily conversations from ALL session paths
  (main, agents, cron) via cron to generate markdown journals using semantic understanding.

  Use when:
  - User requests journaling ("Show me my journal", "What did I do today?")
  - User asks for pattern analysis ("Analyze my patterns", "How am I doing?")
  - User requests summaries ("Generate weekly/monthly summary")
metadata:
  version: 0.0.19
---

# PhoenixClaw: Zero-Tag Passive Journaling

PhoenixClaw automatically distills daily conversations into meaningful reflections using semantic intelligence.

Automatically identifies journal-worthy moments, patterns, and growth opportunities.

## 🛠️ Core Workflow

> [!critical] **MANDATORY: Complete Workflow Execution**
> This 9-step workflow MUST be executed in full regardless of invocation method:
> - **Cron execution** (10 PM nightly)
> - **Manual invocation** ("Show me my journal", "Generate today's journal", etc.)
> - **Regeneration requests** ("Regenerate my journal", "Update today's entry")
> 
> **Never skip steps.** Partial execution causes:
> - Missing images (session logs not scanned)
> - Missing finance data (Ledger plugin not triggered)
> - Incomplete journals (plugins not executed)

PhoenixClaw follows a structured pipeline to ensure consistency and depth:

1. **User Configuration:** Check for `~/.phoenixclaw/config.yaml`. If missing, initiate the onboarding flow defined in `references/user-config.md`.
2. **Context Retrieval:** 
   - **Scan memory files (NEW):** Read `memory/YYYY-MM-DD.md` and `memory/YYYY-MM-DD-*.md` files for manually recorded daily reflections. These files contain personal thoughts, emotions, and context that users explicitly ask the AI to remember via commands like "记一下" (remember this). **CRITICAL**: Do not skip these files - they contain explicit user reflections that session logs may miss.
   - **Scan session logs:** Call `memory_get` for the current day's memory, then **CRITICAL: Scan ALL raw session logs and filter by message timestamp**. Session files are often split across multiple files. Do NOT classify images by session file `mtime`:
      ```bash
      # Read all session logs from ALL known OpenClaw locations, then filter by per-message timestamp
      # Use timezone-aware epoch range to avoid UTC/local-day mismatches.
      TARGET_DAY="$(date +%Y-%m-%d)"
      TARGET_TZ="${TARGET_TZ:-Asia/Shanghai}"
      read START_EPOCH END_EPOCH < <(
        python3 - <<'PY' "$TARGET_DAY" "$TARGET_TZ"
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
import sys

day, tz = sys.argv[1], sys.argv[2]
start = datetime.strptime(day, "%Y-%m-%d").replace(tzinfo=ZoneInfo(tz))
end = start + timedelta(days=1)
print(int(start.timestamp()), int(end.timestamp()))
PY
      )

      # Recursively scan all session directories (multi-agent architecture support)
      for dir in "$HOME/.openclaw/sessions" \
                 "$HOME/.openclaw/agents" \
                 "$HOME/.openclaw/cron/runs" \
                 "$HOME/.agent/sessions"; do
        [ -d "$dir" ] || continue
        find "$dir" -type f -name "*.jsonl" -print0
      done |
        xargs -0 jq -cr --argjson start "$START_EPOCH" --argjson end "$END_EPOCH" '
          (.timestamp // .created_at // empty) as $ts
          | ($ts | split(".")[0] + "Z" | fromdateiso8601?) as $epoch
          | select($epoch != null and $epoch >= $start and $epoch < $end)
        '
      ```
      Read **all matching files** regardless of their numeric naming (e.g., file_22, file_23 may be earlier in name but still contain today's messages).
    - **EXTRACT IMAGES FROM SESSION LOGS**: Session logs contain `type: "image"` entries with file paths. You MUST:
      1. Find all image entries (e.g., `"type":"image"`)
      2. Keep only entries where message `timestamp` is in the target date range
      3. Extract the `file_path` or `url` fields
      4. Copy files into `assets/YYYY-MM-DD/`
      5. Rename with descriptive names when possible
    - **Why session logs are mandatory**: `memory_get` returns **text only**. Image metadata, photo references, and media attachments are **only available in session logs**. Skipping session logs = missing all photos.
    - **Activity signal quality**: Do not treat heartbeat/cron system noise as user activity. Extract user/assistant conversational content and media events first, then classify moments.
    - **FILTER HEARTBEAT MESSAGES (CRITICAL)**: Session logs contain system heartbeat messages that MUST be excluded from journaling. When scanning messages, SKIP any message matching these criteria:
      1. **User heartbeat prompts**: Messages containing "Read HEARTBEAT.md" AND "reply HEARTBEAT_OK"
      2. **Assistant heartbeat responses**: Messages containing ONLY "HEARTBEAT_OK" (with optional leading/trailing whitespace)
      3. **Cron system messages**: Messages with role "system" or "cron" containing job execution summaries (e.g., "Cron job completed", "A cron job")
      
      Example jq filter to exclude heartbeats:
      ```jq
      # Exclude heartbeat messages
      | select(
          (.message.content? | type == "array" and 
            (.message.content | map(.text?) | join("") | 
              test("Read HEARTBEAT\.md"; "i") | not))
          and
          (.message.content? | type == "array" and 
            (.message.content | map(.text?) | join("") | 
              test("^\\s*HEARTBEAT_OK\\s*$"; "i") | not))
        )
      ```
    - **Edge case - Midnight boundary**: For late-night activity that spans midnight, expand the **timestamp** range to include spillover windows (for example, previous day 23:00-24:00) and still filter per-message by `timestamp`.
   - **Merge sources:** Combine content from both memory files and session logs. Memory files capture explicit user reflections; session logs capture conversational flow and media. Use both to build complete context.
   - **Fallback:** If memory is sparse, reconstruct context from session logs, then update memory so future runs use the enriched memory. Incorporate historical context via `memory_search` (skip if embeddings unavailable)

3. **Moment Identification:** Identify "journal-worthy" content: critical decisions, emotional shifts, milestones, or shared media. See `references/media-handling.md` for photo processing. This step generates the `moments` data structure that plugins depend on.
   **Image Processing (CRITICAL)**:
   - For each extracted image, generate descriptive alt-text via Vision Analysis
   - Categorize images (food, selfie, screenshot, document, etc.)
   
   **Filter Finance Screenshots (NEW)**:
   Payment screenshots (WeChat Pay, Alipay, etc.) should NOT be included in the journal narrative. These are tool images, not life moments.
   
   Detection criteria (check any):
   1. **OCR keywords**: "支付成功", "支付完成", "微信支付", "支付宝", "订单号", "交易单号", "¥" + amount
   2. **Context clues**: Image sent with nearby text containing "记账", "支付", "付款", "转账"
   3. **Visual patterns**: Standard payment app UI layouts (green WeChat, blue Alipay)
   
   Handling rules:
   - Mark as `finance_screenshot` type
   - Route to Ledger plugin (if enabled) for transaction recording
   - **EXCLUDE from journal main narrative** unless explicitly described as part of a life moment (e.g., "今天请朋友吃饭" with payment screenshot)
   - Never include raw payment screenshots in daily journal images section
   
   - Match images to moments (e.g., breakfast photo → breakfast moment)
   - Store image metadata with moments for journal embedding
4. **Pattern Recognition:** Detect recurring themes, mood fluctuations, and energy levels. Map these to growth opportunities using `references/skill-recommendations.md`.

5. **Plugin Execution:** Execute all registered plugins at their declared hook points. See `references/plugin-protocol.md` for the complete plugin lifecycle:
   - `pre-analysis` → before conversation analysis
   - `post-moment-analysis` → **Ledger and other primary plugins execute here**
   - `post-pattern-analysis` → after patterns detected
   - `journal-generation` → plugins inject custom sections
   - `post-journal` → after journal complete


6. **Journal Generation:** Synthesize the day's events into a beautiful Markdown file using `assets/daily-template.md`. Follow the visual guidelines in `references/visual-design.md`. **Include all plugin-generated sections** at their declared `section_order` positions.
   - **Embed curated images only**, not every image. Prioritize highlights and moments.
   - **Route finance screenshots to Ledger** sections (receipts, invoices, transaction proofs).
   - Use Obsidian format from `references/media-handling.md` with descriptive captions.
   - **Generate image links from filesystem truth**: compute the image path relative to the current journal file directory. Never output absolute paths.
   - **Do not hardcode path depth** (`../` or `../../`): calculate dynamically from `daily_file_path` and `image_path`.
   - **Use copied filename as source of truth**: if asset file is `image_124917_2.jpg`, the link must reference that exact filename.

7. **Timeline Integration:** If significant events occurred, append them to the master index in `timeline.md` using the format from `assets/timeline-template.md` and `references/obsidian-format.md`.

8. **Growth Mapping:** Update `growth-map.md` (based on `assets/growth-map-template.md`) if new behavioral patterns or skill interests are detected.

9. **Profile Evolution:** Update the long-term user profile (`profile.md`) to reflect the latest observations on values, goals, and personality traits. See `references/profile-evolution.md` and `assets/profile-template.md`.

## ⏰ Cron & Passive Operation
PhoenixClaw is designed to run without user intervention. It utilizes OpenClaw's built-in cron system to trigger its analysis daily at 10:00 PM local time (0 22 * * *).
- Setup details can be found in `references/cron-setup.md`.
- **Mode:** Primarily Passive. The AI proactively summarizes the day's activities without being asked.

### Rolling Journal Window (NEW)
To solve the 22:00-24:00 content loss issue, PhoenixClaw now supports a **rolling journal window** mechanism:

**Problem**: Fixed 24-hour window (00:00-22:00) misses content between 22:00-24:00 when journal is generated at 22:00.

**Solution**: `scripts/rolling-journal.js` scans from **last journal time → now** instead of fixed daily boundaries.

**Features**:
- Configurable schedule hour (default: 22:00, customizable via `~/.phoenixclaw/config.yaml`)
- Rolling window: No content loss even if generation time varies
- Backward compatible with existing `late-night-supplement.js`

**Configuration** (`~/.phoenixclaw/config.yaml`):
```yaml
schedule:
  hour: 22        # Journal generation time
  minute: 0
  rolling_window: true   # Enable rolling window (recommended)
```

**Usage**:
```bash
# Default: generate from last journal to now
node scripts/rolling-journal.js

# Specific date
node scripts/rolling-journal.js 2026-02-12
```

## 💬 Explicit Triggers

While passive by design, users can interact with PhoenixClaw directly using these phrases:
- *"Show me my journal for today/yesterday."*
- *"What did I accomplish today?"*
- *"Analyze my mood patterns over the last week."*
- *"Generate my weekly/monthly summary."*
- *"How am I doing on my personal goals?"*
- *"Regenerate my journal."* / *"重新生成日记"*

> [!warning] **Manual Invocation = Full Pipeline**
> When users request journal generation/regeneration, you MUST execute the **complete 9-step Core Workflow** above. This ensures:
> - **Photos are included** (via session log scanning)
> - **Ledger plugin runs** (via `post-moment-analysis` hook)
> - **All plugins execute** (at their respective hook points)
> 
> **Common mistakes to avoid:**
> - ❌ Only calling `memory_get` (misses photos)
> - ❌ Skipping moment identification (plugins never trigger)
> - ❌ Generating journal directly without plugin sections

## 📚 Documentation Reference
### References (`references/`)
- `user-config.md`: Initial onboarding and persistence settings.
- `cron-setup.md`: Technical configuration for nightly automation.
- `plugin-protocol.md`: Plugin architecture, hook points, and integration protocol.
- `media-handling.md`: Strategies for extracting meaning from photos and rich media.
- `session-day-audit.js`: Diagnostic utility for verifying target-day message coverage across session logs.
- `visual-design.md`: Layout principles for readability and aesthetics.
- `obsidian-format.md`: Ensuring compatibility with Obsidian and other PKM tools.
- `profile-evolution.md`: How the system maintains a long-term user identity.
- `skill-recommendations.md`: Logic for suggesting new skills based on journal insights.

### Assets (`assets/`)
- `daily-template.md`: The blueprint for daily journal entries.
- `weekly-template.md`: The blueprint for high-level weekly summaries.
- `profile-template.md`: Structure for the `profile.md` persistent identity file.
- `timeline-template.md`: Structure for the `timeline.md` chronological index.
- `growth-map-template.md`: Structure for the `growth-map.md` thematic index.

---


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### scripts/rolling-journal.js

```javascript
#!/usr/bin/env node
/**
 * PhoenixClaw Rolling Journal - 滚动日记生成器
 * 
 * 改进版日记生成逻辑:
 * 1. 允许用户配置生成时间(默认 22:00)
 * 2. 扫描范围:上次日记时间 → 现在(滚动窗口)
 * 3. 解决 22:00-24:00 内容遗漏问题
 * 
 * Usage: node rolling-journal.js [YYYY-MM-DD]
 */

const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');

// 配置
const CONFIG = {
  journalPath: process.env.PHOENIXCLAW_JOURNAL_PATH || '/mnt/synology/zpro/notes/日记',
  sessionRoots: (process.env.OPENCLAW_SESSIONS_PATH || '').split(path.delimiter).filter(Boolean),
  configPath: path.join(require('os').homedir(), '.phoenixclaw/config.yaml'),
  timezone: 'Asia/Shanghai',
  defaultHour: 22  // 默认生成时间
};

/**
 * 默认 session 搜索路径列表(覆盖所有已知位置)
 */
function getDefaultSessionRoots() {
  const home = require('os').homedir();
  return [
    path.join(home, '.openclaw', 'sessions'),
    path.join(home, '.openclaw', 'agents'),
    path.join(home, '.openclaw', 'cron', 'runs'),
    path.join(home, '.agent', 'sessions'),
  ];
}

/**
 * 读取用户配置
 */
function loadConfig() {
  const config = {
    scheduleHour: CONFIG.defaultHour,
    scheduleMinute: 0,
    rollingWindow: true  // 是否启用滚动窗口
  };

  if (fs.existsSync(CONFIG.configPath)) {
    try {
      const content = fs.readFileSync(CONFIG.configPath, 'utf-8');
      // 简单 YAML 解析
      const hourMatch = content.match(/schedule_hour:\s*(\d+)/);
      const minuteMatch = content.match(/schedule_minute:\s*(\d+)/);
      const rollingMatch = content.match(/rolling_window:\s*(true|false)/);
      // FIX: 读取 journal_path 配置
      const pathMatch = content.match(/journal_path:\s*(.+)/);
      
      if (hourMatch) config.scheduleHour = parseInt(hourMatch[1]);
      if (minuteMatch) config.scheduleMinute = parseInt(minuteMatch[1]);
      if (rollingMatch) config.rollingWindow = rollingMatch[1] === 'true';
      // FIX: 解析并展开 journal_path
      if (pathMatch) {
        let jp = pathMatch[1].trim();
        // 展开 ~ 为 home 目录
        if (jp.startsWith('~/')) {
          jp = path.join(require('os').homedir(), jp.slice(2));
        }
        config.journalPath = jp;
      }
    } catch (e) {
      console.error('Error reading config:', e.message);
    }
  }

  return config;
}

/**
 * 找到最后一次日记的时间
 */
function getLastJournalTime(journalPath) {
  const dailyDir = path.join(journalPath || CONFIG.journalPath, 'daily');
  if (!fs.existsSync(dailyDir)) return null;

  const files = fs.readdirSync(dailyDir)
    .filter(f => f.endsWith('.md'))
    .map(f => ({
      file: f,
      date: f.replace('.md', ''),
      mtime: fs.statSync(path.join(dailyDir, f)).mtime
    }))
    .sort((a, b) => b.mtime - a.mtime);

  if (files.length === 0) return null;

  // 返回最新日记的修改时间
  return files[0].mtime;
}

/**
 * 递归查找目录下所有 .jsonl 文件
 */
function findJsonlFiles(dir) {
  const results = [];
  if (!fs.existsSync(dir)) return results;

  let entries;
  try {
    entries = fs.readdirSync(dir, { withFileTypes: true });
  } catch (e) {
    return results;
  }

  for (const entry of entries) {
    const fullPath = path.join(dir, entry.name);
    if (entry.isDirectory()) {
      results.push(...findJsonlFiles(fullPath));
    } else if (entry.isFile() && entry.name.endsWith('.jsonl')) {
      results.push(fullPath);
    }
  }
  return results;
}

/**
 * 读取所有 session 日志文件(扫描多个目录 + 递归)
 */
function readSessionLogs() {
  const roots = CONFIG.sessionRoots.length > 0
    ? CONFIG.sessionRoots
    : getDefaultSessionRoots();

  const allFiles = [];
  for (const root of roots) {
    const files = findJsonlFiles(root);
    allFiles.push(...files);
    if (files.length > 0) {
      console.log(`  [scan] ${root} → ${files.length} file(s)`);
    }
  }

  if (allFiles.length === 0) {
    console.error('No session log files found in any directory');
    return [];
  }

  console.log(`  [scan] Total session files: ${allFiles.length}`);

  const logs = [];
  let parseErrors = 0;

  for (const file of allFiles) {
    try {
      const content = fs.readFileSync(file, 'utf-8');
      const lines = content.split('\n').filter(line => line.trim());
      for (const line of lines) {
        try {
          const entry = JSON.parse(line);
          logs.push(entry);
        } catch (e) {
          parseErrors++;
        }
      }
    } catch (e) {
      console.error(`Error reading ${file}:`, e.message);
    }
  }

  if (parseErrors > 0) {
    console.log(`  [scan] Skipped ${parseErrors} malformed line(s)`);
  }

  logs.sort((a, b) => {
    const ta = new Date(a.timestamp || a.created_at || 0).getTime();
    const tb = new Date(b.timestamp || b.created_at || 0).getTime();
    return ta - tb;
  });

  return logs;
}

/**
 * 过滤从上次日记到现在的消息
 */
function filterRollingWindowMessages(logs, lastJournalTime) {
  const startTime = lastJournalTime || new Date(Date.now() - 24 * 60 * 60 * 1000); // 默认24小时前
  const endTime = new Date();

  return logs.filter(entry => {
    const timestamp = entry.timestamp || entry.created_at;
    if (!timestamp) return false;

    const entryTime = new Date(timestamp);
    return entryTime >= startTime && entryTime <= endTime;
  });
}

/**
 * 从嵌套 message 结构中提取完整文本
 */
function extractText(entry) {
  if (typeof entry.content === 'string') return entry.content;
  if (entry.message && Array.isArray(entry.message.content)) {
    return entry.message.content.map(c => c.text || '').join(' ');
  }
  if (Array.isArray(entry.content)) {
    return entry.content.map(c => (typeof c === 'string' ? c : c.text || '')).join(' ');
  }
  return '';
}

/**
 * 判断消息是否是"有意义的"
 */
function isMeaningfulMessage(entry) {
  const text = extractText(entry);

  // 排除心跳检测 — 用户端:包含 "Read HEARTBEAT.md" 且包含 "reply HEARTBEAT_OK"
  if (/Read HEARTBEAT\.md/i.test(text) && /reply HEARTBEAT_OK/i.test(text)) return false;

  // 排除心跳检测 — 助手端:仅包含 "HEARTBEAT_OK"
  if (/^\s*HEARTBEAT_OK\s*$/i.test(text)) return false;

  // 排除 cron 系统消息
  if ((entry.role === 'system' || entry.role === 'cron') &&
      /cron job|nightly reflection|scheduler/i.test(text)) return false;

  // 排除纯系统消息(保留带附件的系统消息)
  if (entry.role === 'system' && !text.includes('attached')) return false;

  // 保留用户消息和助手回复
  if (entry.role === 'user' || entry.role === 'assistant') return true;

  // 保留图片等媒体
  if (entry.type === 'image') return true;

  return false;
}

/**
 * 提取时刻信息
 */
function extractMoments(messages) {
  const moments = [];
  let currentDate = null;
  
  for (const msg of messages) {
    const time = new Date(msg.timestamp || msg.created_at);
    const dateStr = time.toISOString().split('T')[0];
    const timeStr = time.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
    
    // 检测日期变化
    if (currentDate !== dateStr) {
      currentDate = dateStr;
      moments.push({
        type: 'date-marker',
        date: dateStr
      });
    }
    
    if (msg.type === 'image') {
      moments.push({
        time: timeStr,
        type: 'image',
        description: '分享图片'
      });
    } else if (msg.content) {
      // 简化内容(只取前50字)
      const preview = msg.content.substring(0, 50).replace(/\n/g, ' ');
      const suffix = msg.content.length > 50 ? '...' : '';
      moments.push({
        time: timeStr,
        type: 'text',
        role: msg.role === 'user' ? '你' : 'Claw',
        preview: preview + suffix
      });
    }
  }
  
  return moments;
}

/**
 * 生成日记内容
 */
function generateJournal(moments, startTime, endTime) {
  if (moments.length === 0) return null;

  const startDate = startTime.toISOString().split('T')[0];
  const endDate = endTime.toISOString().split('T')[0];
  const dateRange = startDate === endDate ? startDate : `${startDate} ~ ${endDate}`;
  
  let content = `---\n`;
  content += `date: ${endDate}\n`;
  content += `type: daily\n`;
  content += `time_range: ${startTime.toLocaleString('zh-CN')} ~ ${endTime.toLocaleString('zh-CN')}\n`;
  content += `---\n\n`;
  content += `# 日记 ${dateRange}\n\n`;
  
  let currentDate = null;
  for (const moment of moments) {
    if (moment.type === 'date-marker') {
      if (currentDate !== null) content += '\n';
      currentDate = moment.date;
      content += `## ${moment.date}\n\n`;
    } else if (moment.type === 'image') {
      content += `- **${moment.time}** 📸 ${moment.description}\n`;
    } else {
      content += `- **${moment.time}** ${moment.role}: ${moment.preview}\n`;
    }
  }
  
  content += `\n---\n`;
  content += `*Generated by PhoenixClaw Rolling Journal at ${new Date().toLocaleString('zh-CN')}*\n`;
  
  return content;
}

/**
 * 保存日记
 */
function saveJournal(content, date, journalPath) {
  const dailyDir = path.join(journalPath || CONFIG.journalPath, 'daily');
  
  // 确保目录存在
  if (!fs.existsSync(dailyDir)) {
    try {
      fs.mkdirSync(dailyDir, { recursive: true });
    } catch (e) {
      // FIX: 如果 mkdir 失败,尝试使用 shell
      try {
        execSync(`mkdir -p "${dailyDir}"`, { encoding: 'utf-8' });
      } catch (shellErr) {
        console.error('Failed to create directory:', shellErr.message);
        throw e;
      }
    }
  }
  
  const filename = path.join(dailyDir, `${date}.md`);
  
  // FIX: 尝试直接写入,失败则使用 shell
  try {
    fs.writeFileSync(filename, content);
  } catch (e) {
    console.log('Direct write failed, trying shell escape...');
    try {
      // 使用 shell 写入,处理特殊字符
      const escapedContent = content.replace(/'/g, "'\"'\"'");
      execSync(`cat > "${filename}" << 'EOF'
${escapedContent}
EOF`, { encoding: 'utf-8' });
    } catch (shellErr) {
      console.error('Shell write also failed:', shellErr.message);
      throw e;
    }
  }
  
  return filename;
}

/**
 * 主函数
 */
async function main() {
  console.log('[PhoenixClaw Rolling Journal] Starting...');
  
  // 1. 加载配置
  const userConfig = loadConfig();
  console.log(`Schedule: ${userConfig.scheduleHour}:${String(userConfig.scheduleMinute).padStart(2, '0')}`);
  console.log(`Rolling window: ${userConfig.rollingWindow ? 'enabled' : 'disabled'}`);
  console.log(`Journal path: ${userConfig.journalPath}`);  // FIX: 显示使用的路径

  // 2. 找到上次日记时间
  const lastJournalTime = getLastJournalTime(userConfig.journalPath);  // FIX: 传递 journalPath
  if (lastJournalTime) {
    console.log(`Last journal: ${lastJournalTime.toLocaleString('zh-CN')}`);
  } else {
    console.log('No previous journal found, using default 24h window');
  }

  // 3. 读取会话日志
  console.log('Scanning session directories...');
  const logs = readSessionLogs();
  console.log(`Read ${logs.length} total log entries`);

  // 4. 过滤滚动窗口消息
  const windowStart = userConfig.rollingWindow ? lastJournalTime : new Date(Date.now() - 24 * 60 * 60 * 1000);
  const windowMessages = filterRollingWindowMessages(logs, windowStart);
  console.log(`Messages in window: ${windowMessages.length}`);

  // 5. 过滤有意义的消息
  const meaningfulMessages = windowMessages.filter(isMeaningfulMessage);
  const imageCount = meaningfulMessages.filter(m => m.type === 'image').length;
  const textCount = meaningfulMessages.filter(m => m.type !== 'image').length;
  console.log(`Meaningful messages: ${meaningfulMessages.length} (text: ${textCount}, images: ${imageCount})`);
  const userCount = meaningfulMessages.filter(m => m.role === 'user').length;
  const assistantCount = meaningfulMessages.filter(m => m.role === 'assistant').length;
  console.log(`  user: ${userCount}, assistant: ${assistantCount}`);

  if (meaningfulMessages.length === 0) {
    console.log('No content to journal, skipping');
    process.exit(0);
  }

  // 6. 提取时刻并生成日记
  const moments = extractMoments(meaningfulMessages);
  const journalContent = generateJournal(moments, windowStart || new Date(Date.now() - 24 * 60 * 60 * 1000), new Date());
  
  if (journalContent) {
    const today = new Date().toISOString().split('T')[0];
    const filename = saveJournal(journalContent, today, userConfig.journalPath);  // FIX: 传递 journalPath
    console.log(`✅ Journal saved: ${filename}`);
    console.log(`   Contains ${moments.filter(m => m.type !== 'date-marker').length} moments`);
  }
}

if (require.main === module) {
  main().catch(err => {
    console.error('Error:', err);
    process.exit(1);
  });
}

module.exports = {
  findJsonlFiles,
  readSessionLogs,
  filterRollingWindowMessages,
  isMeaningfulMessage,
  extractMoments,
  extractText,
  getDefaultSessionRoots,
  getLastJournalTime,  // FIX: 导出以支持测试
  saveJournal,  // FIX: 导出以支持测试
};

```

### references/user-config.md

```markdown
## Configuration Storage Location

The user configuration for PhoenixClaw is stored in a YAML file located at the user's home directory to ensure persistence across sessions and environment updates.

**Path:** `~/.phoenixclaw/config.yaml`

This directory and file are created during the initial onboarding process if they do not already exist.

## Configuration File Format

The configuration file uses standard YAML syntax. It must be valid YAML to be parsed correctly by the PhoenixClaw skill.

```yaml
# ~/.phoenixclaw/config.yaml
journal_path: ~/PhoenixClaw/Journal
timezone: auto  # or "Asia/Shanghai", "America/New_York", etc.
language: auto  # or "zh-CN", "en-US", etc.
```

## Configurable Options

| Option | Description | Type | Default |
| :--- | :--- | :--- | :--- |
| `journal_path` | Absolute or relative path to the directory where journal entries are saved. | String | `~/PhoenixClaw/Journal` |
| `timezone` | The timezone used for timestamping journal entries. "auto" attempts to detect system timezone. | String | `auto` |
| `language` | The language used for generating journal content and reflections. "auto" follows system locale. | String | `auto` |

## Onboarding Flow

When the PhoenixClaw skill is invoked and no configuration file is detected at `~/.phoenixclaw/config.yaml`, the following onboarding flow must be triggered:

1.  **Detection:** Check for existence of the config file.
2.  **Greeting:** Provide a warm welcome message to the user.
3.  **Path Inquiry:** Ask the user for their preferred journal storage location.
4.  **Path Confirmation:** Present the default path (`~/PhoenixClaw/Journal`) and allow the user to accept it or provide a new one.
5.  **Initialization:**
    *   Create the `~/.phoenixclaw/` directory if missing.
    *   Expand `~` in the provided path to the absolute home directory path.
    *   Create the target journal directory.
    *   Generate the `config.yaml` file with the confirmed settings.
6.  **Transition:** Proceed immediately to the first journaling task (e.g., summarizing recent activity).

## Onboarding Conversation Examples

### Scenario: Accepting Defaults
**Assistant:** "Welcome to PhoenixClaw! I'm here to help you maintain a passive journal of your work. First, I need to set up your workspace. Where would you like to store your journals? The default is `~/PhoenixClaw/Journal`."
**User:** "That works."
**Assistant:** "Great! I've set up your configuration. I'll now check your recent activity to start today's journal."

### Scenario: Custom Path
**Assistant:** "Welcome to PhoenixClaw! Where would you like to store your journals? (Default: `~/PhoenixClaw/Journal`)"
**User:** "/Users/dev/Documents/MyLogs"
**Assistant:** "Understood. I've configured PhoenixClaw to save entries to `/Users/dev/Documents/MyLogs`. Let's get started!"

## Update Procedures

Users can modify their settings at any time by requesting an update through the assistant.

**Command Triggers:**
*   "Update my PhoenixClaw settings"
*   "Change my journal storage path"
*   "Change my PhoenixClaw language to English"

**Logic for Updates:**
1.  Read the existing `~/.phoenixclaw/config.yaml`.
2.  Ask the user which setting they wish to change.
3.  Validate the new input (especially for `journal_path`).
4.  Write the updated values back to the YAML file.
5.  Confirm the change to the user.

## Path Validation and Error Handling

To ensure reliability, the following validation steps must be performed when setting or updating the `journal_path`:

*   **Existence Check:** If a user provides a path, check if it exists. If it does not, attempt to create it.
*   **Permission Check:** Ensure the assistant has write permissions to the target directory.
*   **Absolute Paths:** Internally, always resolve paths to absolute paths before writing to the config file or using them for file operations to avoid ambiguity.
*   **YAML Integrity:** If the `config.yaml` file becomes corrupted or unreadable, inform the user and offer to reset it to defaults.
*   **Graceful Failures:** If a path cannot be created (e.g., due to permission denied), explain the issue clearly: "I couldn't create the directory at `/protected/path`. Please provide a location I have access to, or check your folder permissions."

## Timezone and Language Handling

*   **Timezone:** If set to `auto`, the skill should attempt to retrieve the local timezone from the environment (e.g., `Intl.DateTimeFormat().resolvedOptions().timeZone` in JS environments or system clock). If detection fails, default to UTC and notify the user.
*   **Language:** If set to `auto`, use the language of the current user session or system locale. If the user explicitly sets a language code (e.g., `zh-CN`), all generated journal content must adhere to that language, regardless of system settings.

```

### references/media-handling.md

```markdown
### Supported Channels

| Channel | Format | Capabilities |
| :--- | :--- | :--- |
| **Telegram** | Photo/Document | Direct bot upload, supports captions and compressed/uncompressed |
| **WhatsApp** | Photo | Media bridge required, handled as transient buffer |
| **Discord** | Attachment | Webhook or bot listener, handles multiple attachments in one message |
| **CLI** | Local Path | User provides file path via command line |

> [!important] Media & Memory Tools
> Note: `memory_get` and `memory_search` return text only. Image metadata and binary references must be sourced from session logs (JSONL) or the local `assets` directory. Scan session logs even when daily memory exists to capture in-progress media.

### Photo Processing Workflow

1. **Receive**: Capture media from the active channel (Telegram/WhatsApp/Discord/CLI).
2. **Buffer**: Store the raw media in a transient memory buffer with metadata (timestamp, source).
3. **Detect**: Cron job or trigger detects unresolved media references in the message stream.
4. **Vision Analysis**: AI processes the photo to extract a descriptive alt-text (e.g., "Spicy miso ramen with soft-boiled egg").
5. **Relocate**: Move file from transient buffer to the permanent assets directory.
6. **Rename**: Apply standard naming convention to the file.
7. **Embed**: Generate the Obsidian-style markdown link and insert into the journal with the appropriate layout.

### Image Extraction Commands

**Filter session messages by target day (not file mtime):**
```bash
# Step 1: Define target day and timezone
TARGET_DAY="$(date +%Y-%m-%d)"
TARGET_TZ="${TARGET_TZ:-Asia/Shanghai}"

# Step 2: Build timezone-aware [start, end) epoch range for TARGET_DAY
read START_EPOCH END_EPOCH < <(
  python3 - <<'PY' "$TARGET_DAY" "$TARGET_TZ"
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
import sys

day, tz = sys.argv[1], sys.argv[2]
start = datetime.strptime(day, "%Y-%m-%d").replace(tzinfo=ZoneInfo(tz))
end = start + timedelta(days=1)
print(int(start.timestamp()), int(end.timestamp()))
PY
)

# Step 3: Recursively read all session files from ALL known locations and keep only messages inside TARGET_DAY
for dir in "$HOME/.openclaw/sessions" \
           "$HOME/.openclaw/agents" \
           "$HOME/.openclaw/cron/runs" \
           "$HOME/.agent/sessions"; do
  [ -d "$dir" ] || continue
  find "$dir" -type f -name "*.jsonl" -print0
done |
  xargs -0 jq -cr --argjson start "$START_EPOCH" --argjson end "$END_EPOCH" '
    (.timestamp // .created_at // empty) as $ts
    | ($ts | split(".")[0] + "Z" | fromdateiso8601?) as $epoch
    | select($epoch != null and $epoch >= $start and $epoch < $end)
  '
```

**Extract image entries from target-day messages:**
```bash
# Keep image entries whose message timestamp is in TARGET_DAY (recursive scan)
for dir in "$HOME/.openclaw/sessions" \
           "$HOME/.openclaw/agents" \
           "$HOME/.openclaw/cron/runs" \
           "$HOME/.agent/sessions"; do
  [ -d "$dir" ] || continue
  find "$dir" -type f -name "*.jsonl" -print0
done |
  xargs -0 jq -r --argjson start "$START_EPOCH" --argjson end "$END_EPOCH" '
    (.timestamp // .created_at // empty) as $ts
    | ($ts | split(".")[0] + "Z" | fromdateiso8601?) as $epoch
    | select($epoch != null and $epoch >= $start and $epoch < $end and .type == "image")
    | (.file_path // .url)
  '
```

> Do not classify images by session file modification time. Always classify by each image message's `timestamp`.

**Copy images to journal assets:**
```bash
# Images are stored in ~/.openclaw/media/inbound/
# Copy to journal assets
TODAY=$(date +%Y-%m-%d)
mkdir -p ~/PhoenixClaw/Journal/assets/$TODAY/
cp ~/.openclaw/media/inbound/file_*.jpg ~/PhoenixClaw/Journal/assets/$TODAY/
```

**Vision Analysis (example output):**
- "早餐:猪儿粑配咖啡,放在白色桌面上"

### Curation Rules (Do Not Embed Everything)

- **Embed highlights only**: pick photos that represent meaningful moments, milestones, or emotional peaks.
- **Finance screenshots** (receipts, invoices, transaction proofs) should be routed to the Ledger plugin output, not the main narrative flow.
- **Low-signal images** (screenshots of chats, generic docs) should be omitted unless they support a key moment.

### Storage & Naming

**Path**: `~/PhoenixClaw/Journal/assets/YYYY-MM-DD/`

**File Naming**:
- Primary format: `img_XXX.jpg` (zero-padded)
- Example: `img_001.jpg`, `img_002.jpg`
- For specific events: `img_001_lunch.jpg` (optional suffix)

### Link Path Generation (Required)

When embedding an image into a journal file, always compute the link from actual file paths.

**Rules:**
- Never use absolute paths in journal markdown.
- Never hardcode `../assets` or `../../assets`.
- Compute the relative path from the journal file directory to the asset file.
- Use the exact copied filename from disk; do not infer or rewrite timestamp fragments.

```python
from pathlib import Path
import os

def get_relative_image_path(daily_file_path: str, image_path: str) -> str:
    daily_file = Path(daily_file_path).resolve()
    image_file = Path(image_path).resolve()
    rel = os.path.relpath(image_file, start=daily_file.parent)
    return rel.replace(os.sep, "/")
```

**Validation before render:**
- `Path(image_path).exists()` is true.
- Embedded filename equals `Path(image_path).name` exactly.
- Link does not start with `/`.
- Link depth is produced by computation, not template constants.

### Journal Photo Layouts

#### Single Photo (Moment)
Used for capturing a specific point in time or a single highlight.
```markdown
> [!moment] 🍜 12:30 Lunch
> ![[../assets/2026-02-01/img_001.jpg|400]]
> Spicy miso ramen with a perfectly soft-boiled egg at the new shop downtown.
```

#### Multiple Photos (Gallery)
Used when several photos are sent together or relate to the same context.
```markdown
> [!gallery] 📷 Today's Moments
> ![[../assets/2026-02-01/img_002.jpg|200]] ![[../assets/2026-02-01/img_003.jpg|200]]
> ![[../assets/2026-02-01/img_004.jpg|200]]
```

#### Photo with Narrative Context
Used when the photo is part of a larger story or reflection.
```markdown
### Weekend Hike
The trail was steeper than I remembered, but the view from the summit made every step worth it. 

![[../assets/2026-02-01/img_005.jpg|600]]
*The fog rolling over the valley at 8:00 AM.*

I spent about twenty minutes just sitting there, listening to the wind and watching the light change across the ridgeline.
```

### Metadata Handling
- **Exif Data**: Preserve original Exif data (GPS, Timestamp) where possible to assist in auto-locating "Moments".
- **Captions**: If a user sends a caption with the photo, prioritize it as the primary description.
- **Deduplication**: Check file hashes before copying to prevent duplicate assets for the same entry.

```

### references/skill-recommendations.md

```markdown
PhoenixClaw employs a pattern-based recommendation system to guide users toward relevant OpenClaw skills based on their journaling content.

### Pattern Detection & Skill Mapping

| Category | User Pattern / Trigger Example | Recommended Skill |
| :--- | :--- | :--- |
| **Mental Health** | "feeling anxious", "racing thoughts" | `mindfulness-coach` |
| | "can't sleep", "waking up tired" | `sleep-optimizer` |
| | "feeling overwhelmed", "high pressure" | `stress-relief` |
| **Productivity** | "putting things off", "stuck" | `pomodoro-master` |
| | "ran out of time", "busy but unproductive" | `time-blocks` |
| | "constant distractions", "hard to focus" | `deep-work` |
| **Health & Fitness**| "need to exercise", "gym routine" | `workout-buddy` |
| | "eating poorly", "tracking macros" | `nutrition-log` |
| | "forgetting to drink water" | `water-reminder` |
| **Habits** | "want to start [habit]", "consistency" | `habit-streak` |
| | "learning [skill]", "studying" | `learning-tracker` |
| **Relationships** | "argument with [name]", "communication" | `relationship-coach` |
| | "awkward in groups", "meeting people" | `social-skills` |
| **Finance** | "花了", "买了", "支出", "消费" | `phoenixclaw-ledger` |
| | "记账", "预算", "开销太大" | `phoenixclaw-ledger` |
| | "工资", "收入", "到账" | `phoenixclaw-ledger` |
| | Payment screenshots detected | `phoenixclaw-ledger` |

### Recommendation Guidelines

Recommendations must be earned through consistent pattern detection to avoid "recommendation fatigue."

#### Frequency & Persistence Rules
1.  **1st Detection:** Observe and log the pattern in internal metadata. Do not recommend.
2.  **3+ Occurrences:** If the pattern persists across 3 separate entries within a 14-day window, flag for recommendation.
3.  **Persistent Pattern:** If the user explicitly asks for help or the pattern is severe/ongoing (5+ entries), prioritize the recommendation.

#### Constraint: Volume Control
*   **Max 1 new skill recommendation per week.**
*   Do **NOT** recommend skills for one-time mentions or transient moods.
*   Do **NOT** re-recommend a skill the user has already rejected or installed.

### Recommendation Format

Recommendations should be subtle, integrated into the "Growth Insights" or "Action Items" sections of journals and `growth-map.md`.

**Journal Entry Example:**
> **Growth Insight:** I've noticed mentions of sleep difficulties in your last few entries. You might find the `sleep-optimizer` skill helpful for establishing a better wind-down routine.

**Growth Map Example:**
> ### Recommended Skills
> *   **Deep Work (`deep-work`):** Based on recent focus challenges mentioned in your productivity reflections.

### Verification Flow
When PhoenixClaw detects a match:
1.  Check `skill-inventory.json` (or equivalent) to see if the skill is already active.
2.  Cross-reference `user-preferences.md` for any "do not suggest" categories.
3.  Verify the last recommendation date to ensure the 1-per-week limit is respected.

```

### references/plugin-protocol.md

```markdown
# PhoenixClaw Plugin Protocol v1

This document defines the protocol for building plugins that extend PhoenixClaw's core functionality.

## Overview

PhoenixClaw supports a plugin architecture where specialized skills can hook into the core journaling pipeline to add domain-specific features (finance tracking, health monitoring, reading logs, etc.).

## Plugin Declaration

Plugins declare their relationship with PhoenixClaw in their SKILL.md frontmatter:

```yaml
---
name: phoenixclaw-{plugin-name}
description: |
  Brief description of the plugin functionality.
  
depends: phoenixclaw
hook_point: post-moment-analysis
data_access:
  - moments
  - user_config
  - memory
export_to_journal: true
---
```

### Required Fields

| Field | Type | Description |
|-------|------|-------------|
| `depends` | string | Must be `phoenixclaw` to indicate Core dependency |
| `hook_point` | string | When in the pipeline this plugin executes |
| `data_access` | array | What data the plugin needs access to |
| `export_to_journal` | boolean | Whether plugin output merges into daily journal |

## Hook Points

Plugins can attach to these points in PhoenixClaw's core workflow:

| Hook Point | Timing | Use Case |
|------------|--------|----------|
| `pre-analysis` | Before conversation analysis | Pre-processing, filtering |
| `post-moment-analysis` | After moments identified | Finance, health tracking |
| `post-pattern-analysis` | After patterns detected | Advanced analytics |
| `journal-generation` | During journal writing | Custom sections |
| `post-journal` | After journal complete | Notifications, exports |

### Hook Execution Order

```
1. Core loads config
2. Core retrieves memory
   └── [pre-analysis] plugins execute
3. Core identifies moments
   └── [post-moment-analysis] plugins execute
4. Core detects patterns
   └── [post-pattern-analysis] plugins execute
5. Core generates journal
   └── [journal-generation] plugins inject sections
6. Core writes files
   └── [post-journal] plugins execute
```

## Data Access

### Available Data Objects

Plugins can request access to these data structures:

#### `moments`
```yaml
moments:
  - timestamp: "2026-02-02T12:30:00"
    type: activity|decision|emotion|milestone|media
    content: "Had lunch with colleagues"
    metadata:
      location: "Downtown"
      people: ["Alice", "Bob"]
      media: ["img_001.jpg"]
```

#### `patterns`
```yaml
patterns:
  themes: ["productivity", "social"]
  mood_trajectory: positive|negative|stable
  energy_level: high|medium|low
  recurring: ["morning coffee", "evening walk"]
```

#### `user_config`
```yaml
user_config:
  journal_path: "~/PhoenixClaw/Journal"
  timezone: "Asia/Shanghai"
  language: "zh-CN"
  plugins:
    phoenixclaw-ledger:
      enabled: true
      budget_monthly: 5000
```

#### `memory`
Raw memory data from `memory_get` for the current day.

## Plugin Output

### Exporting to Journal

Plugins with `export_to_journal: true` must provide their output in this format:

```yaml
plugin_output:
  section_id: "finance"           # Unique section identifier
  section_title: "💰 财务记录"    # Display title with emoji
  section_order: 50               # Position (0-100, lower = earlier)
  content: |
    > [!expense] 🍜 12:30 午餐
    > 和同事吃火锅 | **¥150** | 餐饮
  summary:                        # Optional: for Growth Notes
    - "今日支出 ¥449"
    - "本月已用预算 62%"
```

### Section Ordering

| Order Range | Reserved For |
|-------------|--------------|
| 0-19 | Core: Highlights |
| 20-39 | Core: Moments |
| 40-59 | Plugins: Primary (Finance, Health) |
| 60-79 | Core: Reflections |
| 80-89 | Plugins: Secondary |
| 90-100 | Core: Growth Notes |

### Standalone Files

Plugins may also create their own files outside the journal:

```yaml
plugin_files:
  - path: "Finance/ledger.yaml"
    type: data
  - path: "Finance/monthly/2026-02.md"
    type: report
```

## Plugin Discovery

PhoenixClaw Core discovers plugins through:

1. **Installed Skills**: Skills with `depends: phoenixclaw` in their frontmatter
2. **Config Reference**: Plugins listed in `~/.phoenixclaw/config.yaml`

```yaml
# ~/.phoenixclaw/config.yaml
plugins:
  phoenixclaw-ledger:
    enabled: true
    # Plugin-specific config
  phoenixclaw-health:
    enabled: false
```

## Communication Protocol

### Core → Plugin

Core passes context to plugins via structured data:

```yaml
plugin_context:
  date: "2026-02-02"
  hook_point: "post-moment-analysis"
  moments: [...]
  patterns: [...]
  user_config: {...}
  previous_output: {...}  # Output from earlier plugins
```

### Plugin → Core

Plugins return results:

```yaml
plugin_result:
  success: true
  section_output: {...}     # For journal integration
  files_written: [...]      # List of created/updated files
  insights: [...]           # For Growth Notes
  errors: []                # Any errors encountered
```

## Error Handling

Plugins should handle errors gracefully without breaking the core pipeline:

1. **Fail Soft**: Return empty output rather than throwing
2. **Log Errors**: Include errors in `plugin_result.errors`
3. **Degrade Gracefully**: Partial results are acceptable

```yaml
plugin_result:
  success: false
  section_output: null
  errors:
    - code: "SCREENSHOT_OCR_FAILED"
      message: "Could not extract text from receipt_001.jpg"
      severity: warning  # warning|error
```

## Versioning

Plugins should declare protocol compatibility:

```yaml
---
name: phoenixclaw-ledger
protocol_version: 1
min_core_version: 1.0.0
---
```

## Example Plugin Registration

When PhoenixClaw Core runs, it:

1. Scans for installed skills with `depends: phoenixclaw`
2. Validates protocol compatibility
3. Registers plugins at their declared hook points
4. Executes plugins in order during the journaling pipeline

---

*This protocol is designed for extensibility. Future versions may add new hook points and data access patterns while maintaining backward compatibility.*

```

### assets/daily-template.md

```markdown
---
date: {{DATE}}
weekday: {{WEEKDAY}}
type: daily
mood: {{MOOD}}
energy: {{ENERGY}}
tags: [journal, phoenixclaw]
---

# 🌅 {{DATE}} — {{WEEKDAY}}

## ✨ Highlights
{{HIGHLIGHTS}}

## 🖼️ Moments
{{MOMENTS}}

## 💭 Reflections
{{REFLECTIONS}}

## 🌱 Growth Notes
{{GROWTH_NOTES}}

---
*Generated by PhoenixClaw 🔥*

```

### references/visual-design.md

```markdown
## Image Placement Patterns

### Single Photo (The Moment)
Highlight a specific moment with a focused layout.
```markdown
> [!NOTE] 📸 The View
> ![Sunset over the mountains](assets/sunset.jpg)
> *The sky turned purple and gold as we reached the peak.*
```

### Gallery Grid (Collage)
Group related images for visual impact without clutter.
```markdown
<div class="gallery-grid">
  ![Coffee](assets/coffee.jpg)
  ![Book](assets/book.jpg)
  ![Rain](assets/rain.jpg)
</div>
*Sunday morning essentials: caffeine, literature, and mood.*
```

### Contextual Inline
Place images next to relevant text for flow.
```markdown
The trail was steeper than expected. ![Steep Trail](assets/trail.jpg){align=right}
We had to stop every few minutes to catch our breath, but the view was worth it.
```

---

## Emoji Usage Guidelines

Use emojis as visual anchors, not decoration. Maintain a consistent "voice".

**Time-based Markers:**
- 🌅 Morning / Start
- ☀️ Afternoon / Active
- 🌇 Evening / Wind-down
- 🌙 Night / Sleep

**Theme Markers:**
- ✨ Highlights / Wins
- 🖼️ Visuals / Moments
- 💭 Reflections / Thoughts
- 🌱 Growth / Learning
- 🎯 Focus / Goals

**Don't:** ❌ Sprinkle random emojis in every sentence.
**Do:** ✅ Use them to header sections or bullet points for scanability.

---

## Section Layouts

### Time-Based Flow
Chronological storytelling for daily logs.

```markdown
### 🌅 Morning Routine
Slow start. Coffee on the balcony.

### ☀️ Mid-Day Push
Deep work session. Completed the core feature.

### 🌇 Evening Unwind
Dinner with friends.
```

### Theme-Based Flow
Categorical grouping for weekly reviews or specific events.

```markdown
### ✨ Highlights
- Shipped the new release
- Found a great new cafe

### 💭 Headspace
Feeling productive but needing more rest.

### 🌱 Growth
Learned about CSS Grid subgrid today.
```

---

## Quote Formatting

Use blockquotes for insights, overheard dialogue, or key takeaways.

**Standard Insight:**
> "Consistency is the only currency that matters."

**Dialogue Snippet:**
> **Her:** "Did you see that?"
> **Me:** "I missed it."
> *We both laughed at the absurdity of the timing.*

**Callout for Emphasis:**
> [!TIP] Lesson Learned
> Always check the weather before hiking. Always.

---

## Social-Share Friendly Format

Designed to look good in a screenshot or when shared.

```markdown
# 📅 Sunday Reset

> "Recharging is productive." 💭

**✨ Top 3 Wins:**
1. Read 50 pages
2. Meal prepped for the week
3. Zero screen time after 8 PM

**📸 Mood:**
![Cozy reading nook](assets/nook.jpg)
```

```

### assets/timeline-template.md

```markdown
---
updated: {{UPDATED_DATE}}
---

# 🔥 Life Timeline

## {{YEAR}}

### {{MONTH}}

<!-- Format: **YYYY-MM-DD** 🎯|🎉|💡 [Type] Description → [[daily/YYYY-MM-DD]] -->
{{EVENTS}}

---

*Last updated: {{UPDATED_DATE}}*

```

### references/obsidian-format.md

```markdown
---
date: 2026-02-01
weekday: Sunday
type: daily-journal
mood: 🌟
energy: ⚡
tags: [journal, reflection]
---

## YAML Frontmatter
All journals must begin with a YAML block for metadata:
```yaml
---
date: YYYY-MM-DD
weekday: Full Day Name
type: journal-type (e.g., daily, project, review)
mood: Emoji (🌅, ✨, 🌧️, ⚡)
energy: 1-10 or Emoji (🔋, 🪫)
tags: [category, project-name]
---
```

## Bidirectional Links
Connect notes using double brackets:
- Standard link: [[2026-02-01]]
- Link with alias: [[2026-02-01|Today's Entry]]
- Section link: [[Project Alpha#Milestones]]

## Tags
Use inline tags for categorization:
#achievement #milestone #reflection #insight #daily-win #blocker

## Callout Blocks
Use specific types to highlight content:

> [!moment] 🌅 Significant Moment
> Describe a fleeting or impactful moment.

> [!highlight] ✨ Highlight
> Key event or win of the day.

> [!reflection] 💭 Reflection
> Deep thoughts on experiences or feelings.

> [!insight] 💡 Insight
> Realizations or lessons learned.

> [!milestone] 🎯 Milestone
> Significant progress on a goal or project.

> [!gallery] 🖼️ Media
> Used for grouping images or visual logs.

## Image Embedding
Embed assets with optional size control:
![[../assets/2026/02/capture.jpg|400]]
![[screenshot.png|200]]

Path rules:
- Use relative paths from the current note location.
- Do not use absolute paths such as `/mnt/...` or `C:\\...`.
- Compute path depth dynamically from the note file and target asset file.

## Mermaid Charts
Visualize timelines or processes:
```mermaid
gantt
    title Daily Progress
    section Morning
    Deep Work :a1, 2026-02-01, 4h
    section Afternoon
    Project Meeting :a2, after a1, 1h
    Coding :a3, after a2, 3h
```

## Collapsible Sections
Hide dense technical logs or details:
<details>
<summary>Technical Logs (Click to expand)</summary>

- Debugged memory leak in worker
- Updated dependencies
- Ran benchmark suite

</details>

## Horizontal Rules
Use to separate distinct parts of the day:
---

## Emoji Usage Patterns
- 🌅 Morning / Start
- ✨ Key Highlight
- 🖼️ Visual/Gallery
- 💭 Internal Thought
- 🌱 Growth / Learning
- 🔥 Intensity / Focus
- 🎯 Goal / Achievement
- 🎉 Celebration
- 💡 Idea / Realization
- 🐛 Debugging / Problem
- 🔋 High Energy
- 🪫 Low Energy

```

### assets/growth-map-template.md

```markdown
---
updated: {{UPDATED_DATE}}
---

# 🗺️ Growth Map

## Active Themes

<!-- Format:
### 🏷️ Theme Name
- **Pattern**: What's been observed
- **Progress**: Current status
- **Recommended**: [[skill:name]] if applicable
- **Related**: [[daily/date1]], [[daily/date2]]
-->
{{THEMES}}

## Emerging Patterns

{{PATTERNS}}

## Skill Recommendations

| Pattern | Recommended Skill | Status |
|---------|------------------|--------|
{{SKILL_RECOMMENDATIONS}}

---

*Last updated: {{UPDATED_DATE}}*

```

### references/profile-evolution.md

```markdown
---
created: 2026-02-01
updated: 2026-02-01
version: 1.0.0
---

The user profile in PhoenixClaw is a living document that captures the essence of the user's personality, preferences, and growth. It is built through passive observation and refined over time using a non-destructive, evidence-based approach.

## Profile Architecture

The profile is structured to separate explicit user self-expression from inferred AI observations. This ensures the user retains ultimate control while the AI provides a data-driven mirror of their behavior.

### Required Sections

1. **Identity**: Basic demographic and situational context (e.g., role, current focus).
2. **Personality Traits**: Core behavioral patterns and psychological archetypes.
3. **Communication Style**: Preferences for tone, brevity, technical depth, and medium.
4. **Interests**: Topics of focus, hobbies, and areas of curiosity.
5. **Growth Journey**: Evolution of skills and mindset over time.

## Initial Creation Template

On the first execution, the AI scans available conversation history to bootstrap the profile.

```yaml
# Profile Bootstrap Structure
identity:
  summary: ""
  role: ""
traits: []
communication:
  tone: ""
  style: ""
interests: []
user_notes: |
  # Manual edits go here. AI must not touch this section.
ai_observations: []
```

## Append-Only Update Strategy

To maintain historical integrity and prevent data loss, the AI uses an append-only strategy for observations.

- **Rule**: Never overwrite existing observations.
- **Action**: Add new observations as new entries with timestamps.
- **Synthesis**: Every 30 days, the AI may summarize older observations into "Archived Patterns" to keep the active profile concise, but the raw evidence remains accessible.

## Trait Detection Patterns

Traits are inferred from specific actions and outcomes within the workspace.

| Observed Action | Potential Trait | Evidence Required |
| :--- | :--- | :--- |
| Resolves complex race conditions | Analytical Problem-solver | 3+ instances of deep debugging |
| Frequently asks "Why?" before "How?" | First-principles Thinker | Repeated inquiry into underlying logic |
| Documents every edge case | Thorough / Detail-oriented | Consistent high-quality documentation |
| Rapidly adopts new libraries | Early Adopter / Fast Learner | Minimal lag between tool release and use |
| Simplifies complex architectures | Minimalist Architect | Pattern of refactoring for simplicity |

## Confidence Levels

Each entry in the AI Observations section must be tagged with a confidence level based on frequency and duration.

- **Low (1 observation)**: A single event. Used for interests or transient states.
- **Medium (3+ observations OR 7 days consistent behavior)**: A developing pattern.
- **High (10+ observations AND 30+ days consistent behavior)**: A core aspect of the user's identity or style.

## Handling Manual Edits

The `user_notes` section is sacred.

1. **Respect**: If the user writes "I am a night owl" in `user_notes`, the AI must prioritize this over its own observation that the user works at 9 AM.
2. **Separation**: Keep AI-derived insights in the `ai_observations` list.
3. **Conflict Resolution**: If AI observations contradict `user_notes`, the AI should note the discrepancy as a "Growth Opportunity" or "Context Shift" rather than deleting either.

## Privacy and Ethics

- **No Secrets**: Never record passwords, API keys, financial details, or sensitive health data.
- **No Verbatim Quotes**: Summarize the *pattern* or *sentiment* rather than quoting the user directly to avoid leaking sensitive conversational context.
- **Utility Focus**: Only store information that helps the AI better serve the user.

## Update Frequency

- **Interests**: Updated daily or upon discovery of new topics.
- **Communication Style**: Updated weekly based on response patterns.
- **Personality Traits**: Updated monthly or upon reaching a high-confidence milestone.
- **Growth Journey**: Updated when significant milestones (e.g., project completion) are reached.

```

### assets/profile-template.md

```markdown
---
created: {{CREATED_DATE}}
updated: {{UPDATED_DATE}}
version: {{VERSION}}
---

# 🔥 Personal Profile

## Identity

- **Name**: {{NAME}}
- **Timezone**: {{TIMEZONE}}
- **Primary Language**: {{LANGUAGE}}
- **Active Since**: {{ACTIVE_SINCE}}

## Personality Traits

> These traits are observed from conversations and may evolve over time.

| Trait | Evidence | Confidence |
|-------|----------|------------|
{{TRAITS_TABLE}}

## Communication Style

- **Tone**: {{TONE}}
- **Language mix**: {{LANGUAGES}}
- **Emoji usage**: {{EMOJI_USAGE}}

## Interests & Topics

{{INTERESTS}}

## Daily Rhythms

- **Most active hours**: {{ACTIVE_HOURS}}
- **Peak energy**: {{PEAK_ENERGY}}

## Growth Journey

### Active Themes
{{ACTIVE_THEMES}}

### Milestones
{{MILESTONES}}

## User Notes
<!-- This section is maintained by the user -->

## AI Observations
<!-- This section is maintained by PhoenixClaw -->

```

### references/cron-setup.md

```markdown
PhoenixClaw leverages OpenClaw's built-in cron system for automated, passive journaling. This configuration ensures nightly reflections occur without manual triggers.

### One-Time Setup
Run the following command to register the PhoenixClaw nightly reflection job. This schedules the task to run every day at 10:00 PM local time.

```bash
openclaw cron add \
  --name "PhoenixClaw nightly reflection" \
  --cron "0 22 * * *" \
  --tz "auto" \
  --session isolated \
  --message "Execute PhoenixClaw with COMPLETE 9-step Core Workflow. CRITICAL STEPS:
1. Load config
2. memory_get + Scan session logs and filter by each message timestamp for today (not file mtime)
3. Identify moments (decisions, emotions, milestones, photos) -> creates 'moments' data
4. Detect patterns
5. Execute ALL plugins at hook points (Ledger runs at post-moment-analysis)
6. Generate journal WITH all plugin sections
7-9. Update timeline, growth-map, profile

NEVER skip session log scanning - images are ONLY there. NEVER skip step 3 - plugins depend on moments data."
```

> **Memory & Session Scan**: Always scan session logs from all known directories (`~/.openclaw/sessions/`, `~/.openclaw/agents/*/sessions/`, `~/.openclaw/cron/runs/`, `~/.agent/sessions/`) alongside daily memory to capture in-progress activity. Use recursive scanning to find `.jsonl` files in nested subdirectories. If daily memory is missing or sparse, use session logs to reconstruct context, then update daily memory.

### Configuration Details
- **--name**: Unique identifier for the job. Useful for management.
- **--cron**: Standard crontab syntax. "0 22 * * *" represents 10:00 PM daily.
- **--tz auto**: Automatically detects the system's local timezone. You can also specify a specific timezone like "America/New_York".
- **--session isolated**: Ensures the job runs in a clean environment with full tool access, preventing interference from active coding sessions.
- **--message**: Keep this payload task-focused and version-agnostic. Do not hardcode skill version numbers here; treat `metadata.version` in `SKILL.md` as the source of truth.

### Verification and Monitoring
To ensure the job is correctly registered and active:

```bash
openclaw cron list
```

To view the execution history, including status codes and timestamps of previous runs:

```bash
openclaw cron history "PhoenixClaw nightly reflection"
```

### Modifying and Removing Jobs
If you need to change the schedule or the instructions, you can update the job using the same name:

```bash
openclaw cron update "PhoenixClaw nightly reflection" --cron "0 23 * * *"
```

To completely stop and delete the automated reflection job:

```bash
openclaw cron remove "PhoenixClaw nightly reflection"
```

### Post-Execution Verification

After cron runs, verify the full workflow executed:

```bash
# 1. Check target-day messages were scanned (by message timestamp)
TARGET_DAY="$(date +%Y-%m-%d)"
TARGET_TZ="${TARGET_TZ:-Asia/Shanghai}"
read START_EPOCH END_EPOCH < <(
  python3 - <<'PY' "$TARGET_DAY" "$TARGET_TZ"
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
import sys

day, tz = sys.argv[1], sys.argv[2]
start = datetime.strptime(day, "%Y-%m-%d").replace(tzinfo=ZoneInfo(tz))
end = start + timedelta(days=1)
print(int(start.timestamp()), int(end.timestamp()))
PY
)

# Recursively scan all session directories (multi-agent architecture support)
for dir in "$HOME/.openclaw/sessions" \
           "$HOME/.openclaw/agents" \
           "$HOME/.openclaw/cron/runs" \
           "$HOME/.agent/sessions"; do
  [ -d "$dir" ] || continue
  find "$dir" -type f -name "*.jsonl" -print0
done |
  xargs -0 jq -cr --argjson start "$START_EPOCH" --argjson end "$END_EPOCH" '
    (.timestamp // .created_at // empty) as $ts
    | ($ts | split(".")[0] + "Z" | fromdateiso8601?) as $epoch
    | select($epoch != null and $epoch >= $start and $epoch < $end)
  ' | wc -l

# 2. Check images were extracted (if any existed)
ls -la ~/PhoenixClaw/Journal/assets/$(date +%Y-%m-%d)/ 2>/dev/null || echo "No assets dir"

# 3. Check Ledger plugin ran (if installed)
grep -q "财务\|Finance\|💰" ~/PhoenixClaw/Journal/daily/$(date +%Y-%m-%d).md && echo "Ledger OK" || echo "Ledger section missing"

# 4. Check journal contains callout sections
grep -c "\[!" ~/PhoenixClaw/Journal/daily/$(date +%Y-%m-%d).md
```

**Diagnostic interpretation:**
- If images are missing → session logs were not properly scanned
- If Ledger section is missing → moment identification (step 3) was skipped
- If no callouts → journal generation used minimal template

Optional JS audit (structured summary, user/noise split):

```bash
node skills/phoenixclaw/references/session-day-audit.js --day "$(date +%Y-%m-%d)" --tz "Asia/Shanghai"
```

### Troubleshooting
If journals are not appearing as expected, check the following:

1. **System Wake State**: OpenClaw cron requires the host machine to be awake. On macOS/Linux, ensure the machine isn't sleeping during the scheduled time.
2. **Path Resolution**: Ensure `openclaw` is in the system PATH available to the cron daemon.
3. **Log Inspection**: Check the internal OpenClaw logs for task-specific errors:
   ```bash
   openclaw logs --task "PhoenixClaw nightly reflection"
   ```
4. **Timezone Mismatch**: If jobs run at unexpected hours, verify the detected timezone with `openclaw cron list` and consider hardcoding the timezone if `auto` fails.
5. **Tool Access**: Ensure the `isolated` session has proper permissions to read the memory directories and write to the journal storage.
6. **Memory Search Availability**: If `memory_search` is unavailable due to a missing embeddings provider (OpenAI/Gemini/Local), journaling will still continue by scanning daily memory and session logs directly. For cross-day pattern recognition and long-term recall, consider configuring an embeddings provider.

```



---

## Skill Companion Files

> Additional files collected from the skill directory layout.

### _meta.json

```json
{
  "owner": "goforu",
  "slug": "phoenixclaw",
  "displayName": "phoenixclaw",
  "latest": {
    "version": "0.0.19",
    "publishedAt": 1773805821800,
    "commit": "https://github.com/openclaw/skills/commit/78845de1d0082ef200f3dd87ecc8ea96c93a4b25"
  },
  "history": [
    {
      "version": "0.0.18",
      "publishedAt": 1773216535455,
      "commit": "https://github.com/openclaw/skills/commit/e78aea2205bd672216e6c1dde26e2f054bc3ef2a"
    },
    {
      "version": "0.0.16",
      "publishedAt": 1772264855577,
      "commit": "https://github.com/openclaw/skills/commit/e13fc477dd69fde43f056c2c9ac4f5994cc3ffbd"
    },
    {
      "version": "0.0.15",
      "publishedAt": 1771925031143,
      "commit": "https://github.com/openclaw/skills/commit/133ea32c61dc95e144dab5ff84c56fcc81da1ca4"
    },
    {
      "version": "0.0.14",
      "publishedAt": 1771054068281,
      "commit": "https://github.com/openclaw/skills/commit/d06c39be583a3c62dee2894cdf5babb50e2e1658"
    },
    {
      "version": "0.0.13",
      "publishedAt": 1770824347180,
      "commit": "https://github.com/openclaw/skills/commit/bfb35239d8682edf9ed97043f86bed98fa131187"
    },
    {
      "version": "0.0.12",
      "publishedAt": 1770607493358,
      "commit": "https://github.com/openclaw/skills/commit/c0cdc1961358749e5c0821346ae1827a827d7ac0"
    },
    {
      "version": "0.0.11",
      "publishedAt": 1770541285333,
      "commit": "https://github.com/openclaw/skills/commit/31418b68fcbd440ad92879683db6831efb243949"
    },
    {
      "version": "0.0.10",
      "publishedAt": 1770394666348,
      "commit": "https://github.com/openclaw/skills/commit/15b654ce5e3ba49da98984c238c3191df7c5c253"
    },
    {
      "version": "0.0.8",
      "publishedAt": 1770134182949,
      "commit": "https://github.com/clawdbot/skills/commit/c1cc19624a7be4b3d9ae9b16fc21aef9a00fa59d"
    },
    {
      "version": "0.0.7",
      "publishedAt": 1770092854411,
      "commit": "https://github.com/clawdbot/skills/commit/6eb591a565deecb5fd857445172a772ce52d6401"
    },
    {
      "version": "0.0.6",
      "publishedAt": 1770052178008,
      "commit": "https://github.com/clawdbot/skills/commit/fc820dd05552121b2edfa0d75b937fd2d26a6494"
    },
    {
      "version": "0.0.5",
      "publishedAt": 1770049125417,
      "commit": "https://github.com/clawdbot/skills/commit/0aec2f621627d70f52805e6054a9d4400eb0fc3f"
    },
    {
      "version": "0.0.3",
      "publishedAt": 1770005541692,
      "commit": "https://github.com/clawdbot/skills/commit/fdfe0f024eb11f0da6fe878b8f622560c6114051"
    },
    {
      "version": "0.0.2",
      "publishedAt": 1769968835203,
      "commit": "https://github.com/clawdbot/skills/commit/bfef591351e2aed585ef96d6670a4b152f27fb84"
    }
  ]
}

```

### assets/weekly-template.md

```markdown
---
week: {{WEEK}}
start_date: {{START_DATE}}
end_date: {{END_DATE}}
type: weekly
tags: [journal, phoenixclaw, weekly]
---

# 📅 Week {{WEEK}} — {{START_DATE}} to {{END_DATE}}

## 🎯 Week at a Glance
{{WEEK_SUMMARY}}

## 🔥 Key Moments
{{KEY_MOMENTS}}

## 📊 Patterns Observed
{{PATTERNS}}

## 💡 Insights
{{INSIGHTS}}

## 🌱 Growth This Week
{{GROWTH}}

## 📅 Daily Links
{{DAILY_LINKS}}

---
*Weekly reflection by PhoenixClaw 🔥*

```

### references/session-day-audit.js

```javascript
#!/usr/bin/env node

const fs = require("fs");
const os = require("os");
const path = require("path");

function parseArgs(argv) {
  const args = {
    day: null,
    tz: "Asia/Shanghai",
    verbose: false,
  };

  for (let i = 0; i < argv.length; i += 1) {
    const arg = argv[i];
    if (arg === "--day" && i + 1 < argv.length) {
      args.day = argv[i + 1];
      i += 1;
      continue;
    }
    if (arg === "--tz" && i + 1 < argv.length) {
      args.tz = argv[i + 1];
      i += 1;
      continue;
    }
    if (arg === "--verbose") {
      args.verbose = true;
      continue;
    }
    if (arg === "--help" || arg === "-h") {
      printHelp();
      process.exit(0);
    }
  }

  if (!args.day) {
    args.day = formatDateInTz(new Date(), args.tz);
  }

  return args;
}

function printHelp() {
  console.log(`Session day audit for PhoenixClaw

Usage:
  node skills/phoenixclaw/references/session-day-audit.js [--day YYYY-MM-DD] [--tz Asia/Shanghai] [--verbose]

Examples:
  node skills/phoenixclaw/references/session-day-audit.js --day 2026-02-07 --tz Asia/Shanghai
  node skills/phoenixclaw/references/session-day-audit.js --verbose
`);
}

function formatDateInTz(date, tz) {
  const fmt = new Intl.DateTimeFormat("en-CA", {
    timeZone: tz,
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
  });
  const parts = fmt.formatToParts(date);
  const year = parts.find((p) => p.type === "year")?.value;
  const month = parts.find((p) => p.type === "month")?.value;
  const day = parts.find((p) => p.type === "day")?.value;
  if (!year || !month || !day) {
    throw new Error(`Unable to format date in timezone: ${tz}`);
  }
  return `${year}-${month}-${day}`;
}

function expandHome(inputPath) {
  if (!inputPath.startsWith("~/")) {
    return inputPath;
  }
  return path.join(os.homedir(), inputPath.slice(2));
}

function findJsonlFilesRecursive(dir) {
  const results = [];
  if (!fs.existsSync(dir)) return results;
  let entries;
  try {
    entries = fs.readdirSync(dir, { withFileTypes: true });
  } catch (e) {
    return results;
  }
  for (const entry of entries) {
    const fullPath = path.join(dir, entry.name);
    if (entry.isDirectory()) {
      results.push(...findJsonlFilesRecursive(fullPath));
    } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
      results.push(fullPath);
    }
  }
  return results;
}

function listSessionFiles() {
  const roots = [
    "~/.openclaw/sessions",
    "~/.openclaw/agents",
    "~/.openclaw/cron/runs",
    "~/.agent/sessions",
  ].map(expandHome);
  const files = [];

  for (const root of roots) {
    files.push(...findJsonlFilesRecursive(root));
  }

  return files;
}

function safeJsonParse(line, file, lineNumber) {
  try {
    return JSON.parse(line);
  } catch {
    return {
      _parseError: true,
      _file: file,
      _line: lineNumber,
    };
  }
}

function getTimestamp(entry) {
  const ts = entry.timestamp || entry.created_at;
  if (typeof ts !== "string" || ts.length === 0) {
    return null;
  }
  const d = new Date(ts);
  if (Number.isNaN(d.getTime())) {
    return null;
  }
  return d;
}

function flattenText(value) {
  if (typeof value === "string") {
    return value;
  }
  if (Array.isArray(value)) {
    return value.map(flattenText).join(" ");
  }
  if (value && typeof value === "object") {
    return Object.values(value).map(flattenText).join(" ");
  }
  return "";
}

/**
 * Get role from entry, handling nested message structure
 * OpenClaw session logs store role in entry.message.role, not entry.role
 */
function getRole(entry) {
  // Try entry.message.role first (OpenClaw format)
  const nestedRole = entry.message?.role;
  if (typeof nestedRole === "string") {
    return nestedRole.toLowerCase();
  }
  // Fallback to entry.role for backward compatibility
  const directRole = entry.role;
  if (typeof directRole === "string") {
    return directRole.toLowerCase();
  }
  return "";
}

/**
 * Get content from entry, handling nested message structure
 */
function getContent(entry) {
  // Try entry.message.content first (OpenClaw format)
  if (entry.message?.content) {
    return entry.message.content;
  }
  // Fallback to entry.content for backward compatibility
  return entry.content;
}

function isLikelyUserEntry(entry) {
  const role = getRole(entry);
  if (role === "user") {
    return true;
  }

  const type = typeof entry.type === "string" ? entry.type.toLowerCase() : "";
  if (type.includes("user")) {
    return true;
  }

  const payloadText = flattenText(entry).toLowerCase();
  if (payloadText.includes("\"role\":\"user\"") || payloadText.includes("role:user")) {
    return true;
  }

  return false;
}

function isNoise(entry) {
  // Check both nested and direct content
  const nestedContent = flattenText(entry.message?.content);
  const directContent = flattenText(entry.content);
  const payloadText = (nestedContent + " " + directContent).toLowerCase();
  
  const noiseTokens = [
    "heartbeat",
    "cron",
    "nightly reflection",
    "scheduler",
    "system pulse",
    "system heartbeat",
  ];
  return noiseTokens.some((token) => payloadText.includes(token));
}

function auditDay({ day, tz, verbose }) {
  const files = listSessionFiles();
  const summary = {
    targetDay: day,
    timezone: tz,
    scannedFiles: files.length,
    parseErrors: 0,
    matchedMessages: 0,
    userMessages: 0,
    assistantMessages: 0,
    imageMessages: 0,
    noiseMessages: 0,
    filesWithMatches: new Map(),
  };

  for (const file of files) {
    const content = fs.readFileSync(file, "utf8");
    const lines = content.split(/\r?\n/);

    for (let i = 0; i < lines.length; i += 1) {
      const line = lines[i].trim();
      if (!line) {
        continue;
      }

      const entry = safeJsonParse(line, file, i + 1);
      if (entry._parseError) {
        summary.parseErrors += 1;
        continue;
      }

      const ts = getTimestamp(entry);
      if (!ts) {
        continue;
      }

      const messageDay = formatDateInTz(ts, tz);
      if (messageDay !== day) {
        continue;
      }

      summary.matchedMessages += 1;
      summary.filesWithMatches.set(file, (summary.filesWithMatches.get(file) || 0) + 1);

      // Use getRole() to handle nested message structure
      const role = getRole(entry);
      if (role === "assistant") {
        summary.assistantMessages += 1;
      }

      const type = typeof entry.type === "string" ? entry.type.toLowerCase() : "";
      if (type === "image") {
        summary.imageMessages += 1;
      }

      if (isLikelyUserEntry(entry)) {
        summary.userMessages += 1;
      }

      if (isNoise(entry)) {
        summary.noiseMessages += 1;
      }

      if (verbose) {
        const tsText = entry.timestamp || entry.created_at;
        const displayRole = getRole(entry) || "?";
        console.log(`[${path.basename(file)}:${i + 1}] ${tsText} role=${displayRole} type=${entry.type || "?"}`);
      }
    }
  }

  return summary;
}

function printSummary(summary) {
  console.log("PhoenixClaw Session Day Audit");
  console.log("-----------------------------");
  console.log(`target_day: ${summary.targetDay}`);
  console.log(`timezone: ${summary.timezone}`);
  console.log(`scanned_files: ${summary.scannedFiles}`);
  console.log(`matched_messages: ${summary.matchedMessages}`);
  console.log(`user_messages: ${summary.userMessages}`);
  console.log(`assistant_messages: ${summary.assistantMessages}`);
  console.log(`image_messages: ${summary.imageMessages}`);
  console.log(`noise_messages: ${summary.noiseMessages}`);
  console.log(`parse_errors: ${summary.parseErrors}`);

  const activeFiles = Array.from(summary.filesWithMatches.entries())
    .sort((a, b) => b[1] - a[1]);

  if (activeFiles.length === 0) {
    console.log("files_with_matches: none");
    return;
  }

  console.log("files_with_matches:");
  for (const [file, count] of activeFiles) {
    console.log(`  - ${file} (${count})`);
  }
}

function main() {
  const args = parseArgs(process.argv.slice(2));
  if (!/^\d{4}-\d{2}-\d{2}$/.test(args.day)) {
    throw new Error(`Invalid --day value: ${args.day} (expected YYYY-MM-DD)`);
  }

  const summary = auditDay(args);
  printSummary(summary);

  if (summary.userMessages === 0 && summary.matchedMessages > 0) {
    process.exitCode = 2;
  }
}

try {
  main();
} catch (error) {
  console.error(`Error: ${error.message}`);
  process.exit(1);
}

```