
Last Words
- 1 installs
- Updated March 25, 2026
- dilboy/agent-skills
Records a final message and auto-delivers it by email to loved ones after 30 days of user inactivity, with warning notices at 10 and 20 days.
About
Stores a final voice or text message locally, monitors chat activity, and emails the message to a chosen contact after 30 days of inactivity. A user uses it to configure a dead-man's-switch message with SMTP delivery and a daily cron check.
- Records a final voice or text message and monitors user activity
- Auto-delivers via email after 30 days of inactivity with 10 and 20 day warnings
Last Words by the numbers
- 1 all-time installs (skills.sh)
- Ranked #1,983 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Jul 8, 2026 (Skillselion catalog sync)
npx skills add https://github.com/dilboy/agent-skills --skill last-wordsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| Last updated | March 25, 2026 |
| Repository | dilboy/agent-skills ↗ |
What it does
Records a final message and auto-delivers it by email to loved ones after 30 days of user inactivity, with warning notices at 10 and 20 days.
Files
Last Words - 最后留言
Manage final messages to be delivered to loved ones when the user is unreachable for an extended period.
Overview
This skill helps users record a final message (voice or text) to be delivered to their parents or loved ones if they haven't been active for 30 days. The system:
1. Records and stores the final message securely (text + optional voice/audio) 2. Monitors user activity via chat history 3. Sends warning notifications at 10 and 20 days of inactivity 4. Automatically delivers the message after 30 days of no activity (email includes voice as attachment if available) 5. Supports delivery via email (WeChat and phone are planned)
Workflow
1. Record a Final Message
When user says something like "我想给我爸妈留下最后一句话":
1. Respond: "可以,请说吧,顺便提醒一下这句话默认设置半个月没和我聊天记录的话就会自动触发" 2. Accept voice or text input as the message content 3. Save the message using: scripts/save_message.py --message "content" [--audio-path path] 4. Confirm: "已保存,请确认发送形式:邮件 或 微信"
2. Configure Delivery Settings
Ask user to choose delivery method:
- 邮件: Collect email address
- 微信: Collect WeChat ID (placeholder - not yet implemented)
- 电话: Collect phone number (placeholder - not yet implemented)
Save settings using: scripts/configure_delivery.py --method email --contact "address@example.com"
3. Interactive Email Configuration (Chat)
Users can configure email settings through natural chat conversation:
Trigger phrases:
- "配置最后留言邮箱"
- "设置最后留言邮箱"
- "最后留言 配置邮箱"
- "最后留言 设置邮箱"
- "修改最后留言邮箱"
Interactive flow:
1. Ask for sender email:
- Respond: "请提供你的发件邮箱(用于发送留言的邮箱,目前支持QQ邮箱):"
- Wait for user input: e.g., "your-email@qq.com"
2. Ask for authorization code:
- Respond: "请提供邮箱授权码(不是登录密码)。QQ邮箱授权码获取方式:登录QQ邮箱→设置→账户→开启POP3/SMTP服务→获取授权码:"
- Wait for user input: e.g., "xxxxxxxxxxxxxxxx"
3. Ask for recipient email:
- Respond: "请提供收件人邮箱(父母/亲人的邮箱):"
- Wait for user input: e.g., "parent@example.com"
4. Confirm and save:
- Show summary: "配置确认:\n发件人:{smtp_user}\n收件人:{contact}\n是否确认保存?(确认/取消)"
- If user confirms:
- Run:
python3 scripts/configure_delivery.py --method email --contact "{contact}" --smtp-host smtp.qq.com --smtp-port 465 --smtp-user "{smtp_user}" --smtp-pass "{smtp_pass}" - Respond: "✓ 邮箱配置已保存。正在测试邮件发送..."
- Run test:
python3 scripts/debug_mode.py onthenpython3 scripts/debug_mode.py send - Respond with result
- If user cancels: "已取消配置。"
Security notes for interactive config:
- Passwords are masked in chat display (e.g., "授权码已收到:************")
- Credentials are stored locally only
- User can reconfigure anytime by saying "修改最后留言邮箱"
4. Voice/Audio Support
Users can attach a voice recording to their message:
Option A: Save existing audio file
python3 scripts/audio_manager.py save /path/to/recording.wavOption B: Record from microphone (if available)
python3 scripts/audio_manager.py recordPlay back saved audio:
python3 scripts/audio_manager.py playList all saved audio files:
python3 scripts/audio_manager.py listThe audio file will be attached to the email when the final message is delivered.
5. Debug Mode Management (Chat)
Users can manage debug mode through normal chat conversation by explicitly mentioning "最后留言":
Enable debug mode: When user says: "最后留言 开启调试模式", "最后留言 打开调试", or "最后留言 启用调试" 1. Run: python3 scripts/debug_mode.py on 2. Respond: "最后留言调试模式已开启。现在可以立即发送测试消息,无需等待30天。"
Disable debug mode: When user says: "最后留言 关闭调试模式" or "最后留言 禁用调试" 1. Run: python3 scripts/debug_mode.py off 2. Respond: "最后留言调试模式已关闭。系统恢复正常运行(30天无活动后发送)。"
Check debug mode status: When user says: "最后留言 调试模式状态" or "最后留言 调试状态" 1. Run: python3 scripts/debug_mode.py status 2. Show current status and configuration summary
Send immediate test (when debug mode is on): When user says: "最后留言 立即发送测试" or "最后留言 测试发送" 1. Run: python3 scripts/debug_mode.py send 2. Report result: delivery success/failure details
6. Daily Check Process
Run scripts/check_activity.py daily via cron to:
- Check last chat timestamp
- Send warning at 10 days of inactivity
- Send warning at 20 days of inactivity
- Deliver final message at 30 days of inactivity (with audio attachment if available)
Commands Reference
Save Message (text only)
python3 scripts/save_message.py --message "爸爸妈妈我爱你们"Save Message with Audio
# First save the audio file
python3 scripts/audio_manager.py save /path/to/voice-recording.wav
# Or record directly (requires microphone)
python3 scripts/audio_manager.py recordConfigure Delivery
python3 scripts/configure_delivery.py --method email --contact "parent@example.com"
# Methods: email, wechat, phoneAudio Management
python3 scripts/audio_manager.py save /path/to/audio.wav # Save existing audio
python3 scripts/audio_manager.py record # Record from mic
python3 scripts/audio_manager.py play # Play saved audio
python3 scripts/audio_manager.py list # List all audio filesCheck Activity (run daily)
python3 scripts/check_activity.pyGet Status
python3 scripts/get_status.pyReset/Clear Data
python3 scripts/reset.pyDebug Mode (Testing)
Enable debug mode to bypass the 30-day wait and test immediate delivery:
Enable/Disable debug mode:
python3 scripts/debug_mode.py on # Enable debug mode
python3 scripts/debug_mode.py off # Disable debug mode
python3 scripts/debug_mode.py status # Check debug mode statusImmediate send in debug mode:
python3 scripts/debug_mode.py send # Send message immediately (debug)Or use the check script with debug flag:
python3 scripts/check_activity.py --debug-send # Force immediate sendWhen debug mode is enabled:
- Messages can be sent immediately without waiting 30 days
- Use for testing email delivery, audio attachments, etc.
- The system will still log the delivery as a debug/test delivery
- Disable debug mode for normal operation
Data Storage
All data is stored in SQLite database at ~/.openclaw/last-words/data.db:
message: Stores the final message content and audio pathconfig: Stores delivery method and contact informationactivity_log: Tracks daily check results and deliveries
Security & Privacy
- Messages are stored locally only
- No cloud storage or external API calls for message content
- Email delivery uses user's configured SMTP settings
- All scripts run within OpenClaw sandbox
Setup Daily Check
Add to OpenClaw cron:
openclaw cron add --name "last-words-check" --schedule "0 9 * * *" --command "python3 ~/.openclaw/workspace/last-words/scripts/check_activity.py"# Last Words 邮箱配置示例
# 复制此文件为 .env 并填写你的真实配置
# ⚠️ 永远不要提交此文件到 git!
# ============================================
# 必填项
# ============================================
# 发件人邮箱(用于发送留言的邮箱,目前支持QQ邮箱)
SMTP_USER=your-email@qq.com
# 邮箱授权码(不是登录密码!)
# QQ邮箱获取方式:设置 → 账户 → 开启SMTP服务 → 获取授权码
SMTP_PASS=your-auth-code-here
# 收件人邮箱(父母/亲人的邮箱)
CONTACT_EMAIL=parent@example.com
# ============================================
# 可选项(一般不需要修改)
# ============================================
# SMTP服务器配置
# QQ邮箱: smtp.qq.com:465
# 163邮箱: smtp.163.com:465
# Gmail: smtp.gmail.com:587
# Outlook: smtp.office365.com:587
SMTP_HOST=smtp.qq.com
SMTP_PORT=465
# Environment variables
.env
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
# Database
*.db
*.sqlite3
# Audio files
audio/
*.wav
*.mp3
*.m4a
# Logs
*.log
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
更新日志
所有值得注意的更改都会记录在此文件中。
格式基于 Keep a Changelog, 并且本项目遵循 语义化版本。
[未发布]
新增
- 支持通过聊天交互配置邮箱
- 支持从 .env 文件加载配置
- 调试模式支持立即发送测试
- 支持语音附件
改进
- 使用环境变量存储敏感信息
- 完善文档和示例
[1.0.0] - 2024-03-25
新增
- 初始版本发布
- 基础留言保存功能
- 邮件发送支持
- 自动活动检测(10/20/30天)
- SQLite 数据存储
- OpenClaw skill 集成
贡献指南
感谢你对 last-words 项目的关注!
如何贡献
报告问题
如果你发现了 bug 或有功能建议,请通过 GitHub Issues 提交。
提交 Issue 时请包含:
- 问题描述
- 复现步骤
- 期望行为
- 实际行为
- 环境信息(操作系统、Python版本等)
提交代码
1. Fork 项目
git clone https://github.com/yourusername/last-words.git
cd last-words2. 创建分支
git checkout -b feature/your-feature-name3. 提交更改
git commit -m "feat: add some feature"4. 推送并创建 PR
git push origin feature/your-feature-name代码规范
- 遵循 PEP 8 规范
- 添加适当的注释和文档字符串
- 确保代码能在 Python 3.8+ 运行
提交信息格式
feat:新功能fix:修复 bugdocs:文档更新refactor:代码重构test:测试相关chore:构建/工具相关
开发环境设置
# 克隆项目
git clone https://github.com/yourusername/last-words.git
cd last-words
# 创建虚拟环境(可选)
python3 -m venv venv
source venv/bin/activate
# 安装依赖(本项目无外部依赖,仅使用标准库)
# 测试配置
python3 scripts/configure_delivery.py --help测试
在提交 PR 前,请确保:
1. 所有脚本可以正常执行 2. 邮件发送功能已测试(使用调试模式) 3. 没有引入新的问题
# 测试配置
python3 scripts/get_status.py
# 测试调试模式
python3 scripts/debug_mode.py on
python3 scripts/debug_mode.py send
python3 scripts/debug_mode.py off行为准则
- 尊重他人
- 接受建设性批评
- 关注对社区最有利的事
- 展现同理心
隐私提醒
永远不要在代码或提交中包含:
- 真实邮箱地址
- 邮箱密码或授权码
- 个人身份信息
使用示例数据:
- 邮箱:
example@qq.com - 授权码:
xxxxxxxxxxxxxxxx
需要帮助?
- 查看 README.md
- 查看 GitHub Discussions
- 加入 OpenClaw 社区
感谢你的贡献!
#!/bin/bash
# Deploy script for last-words skill to OpenClaw server
# Usage: ./deploy.sh user@hostname
set -e
# 请修改为你的服务器地址
REMOTE=${1:-"user@your-server.com"}
SKILL_NAME="last-words"
LOCAL_SKILL_PATH="$HOME/.openclaw/workspace/${SKILL_NAME}.skill"
REMOTE_PATH="/tmp/${SKILL_NAME}.skill"
echo "🚀 Deploying ${SKILL_NAME} skill to ${REMOTE}..."
# Check if skill package exists
if [ ! -f "$LOCAL_SKILL_PATH" ]; then
echo "✗ Skill package not found: $LOCAL_SKILL_PATH"
echo " Run package script first:"
echo " python3 /usr/local/lib/node_modules/openclaw/skills/skill-creator/scripts/package_skill.py ~/.openclaw/workspace/last-words ~/.openclaw/workspace/"
exit 1
fi
# Copy skill to server
echo "📦 Copying skill package..."
scp "$LOCAL_SKILL_PATH" "${REMOTE}:${REMOTE_PATH}"
# Install skill on remote server
echo "🔧 Installing skill on remote server..."
ssh "${REMOTE}" << EOF
# Check if openclaw is installed
if ! command -v openclaw &> /dev/null; then
echo "✗ OpenClaw not found on remote server"
exit 1
fi
# Find OpenClaw skills directory
SKILL_DIR="\$HOME/.openclaw/skills"
if [ ! -d "\$SKILL_DIR" ]; then
mkdir -p "\$SKILL_DIR"
fi
# Extract skill
cd "\$SKILL_DIR"
unzip -o "${REMOTE_PATH}" -d "${SKILL_NAME}"
# Clean up
rm "${REMOTE_PATH}"
echo "✓ Skill installed to \$SKILL_DIR/${SKILL_NAME}"
# Show status
openclaw skills list | grep last-words || echo "Skill installed but may need OpenClaw restart to detect"
EOF
echo "✓ Deployment complete!"
echo ""
echo "Next steps on the server:"
echo " 1. Test the skill: openclaw skills info last-words"
echo " 2. Set up daily cron: openclaw cron add --name 'last-words-check' --schedule '0 9 * * *' --command 'python3 ~/.openclaw/skills/last-words/scripts/check_activity.py'"
MIT License
Copyright (c) 2024 OpenClaw Community
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Last Words - 最后留言

一个 OpenClaw skill,用于在长时间无活动后自动向亲人发送最后留言。
✨ 功能特点
- 📝 文字留言 - 记录想对亲人说的话
- 🎙️ 语音留言 - 支持录制或上传语音附件
- 📧 邮件发送 - 自动发送邮件(包含语音附件)
- ⏰ 自动检测 - 10天/20天警告,30天自动发送
- 🐛 调试模式 - 立即测试,无需等待30天
- 💬 交互配置 - 通过聊天即可完成所有配置
📦 安装
方式一:通过 OpenClaw 安装(推荐)
openclaw skill install last-words方式二:手动安装
cd ~/.openclaw/skills
git clone https://github.com/yourusername/last-words.git🔧 配置
方式一:聊天交互配置(最简单)
直接在 OpenClaw 聊天中输入:
配置最后留言邮箱然后按提示一步步输入: 1. 你的发件邮箱(QQ邮箱) 2. 邮箱授权码(不是登录密码,获取方式见下方) 3. 收件人邮箱(父母/亲人的邮箱)
配置完成后会自动测试发送。
方式二:使用 .env 文件
cd last-words
cp .env.example .env
# 编辑 .env 文件填入配置
nano .env.env 文件示例:
# 发件邮箱(QQ邮箱)
SMTP_USER=your-qq@qq.com
# 邮箱授权码(16位,不是登录密码)
SMTP_PASS=xxxxxxxxxxxxxxxx
# 收件人邮箱
CONTACT_EMAIL=parent@example.com然后执行:
python3 scripts/configure_delivery.py --from-env方式三:命令行配置
python3 scripts/configure_delivery.py \
--method email \
--contact "parent@example.com" \
--smtp-host smtp.qq.com \
--smtp-port 465 \
--smtp-user "your-qq@qq.com" \
--smtp-pass "your-auth-code"📝 设置留言
通过聊天(推荐)
我想给我爸妈留下最后一句话然后直接输入你想说的话,支持文字或语音。
通过命令行
# 保存文字留言
python3 scripts/save_message.py --message "爸爸妈妈我爱你们"
# 添加语音附件
python3 scripts/audio_manager.py save /path/to/voice.wav🔑 获取 QQ 邮箱授权码
注意:不是邮箱登录密码!
1. 登录 QQ邮箱网页版 2. 点击顶部「设置」→「账户」 3. 找到「POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务」 4. 开启「POP3/SMTP服务」 5. 按提示获取 16位授权码
<details> <summary>📷 图文教程(点击展开)</summary>
!设置入口 !开启服务 !获取授权码
</details>
🐛 调试模式
测试邮件发送,无需等待30天:
通过聊天
最后留言 开启调试模式
最后留言 立即发送测试
最后留言 关闭调试模式通过命令行
# 开启调试模式
python3 scripts/debug_mode.py on
# 立即发送测试
python3 scripts/debug_mode.py send
# 关闭调试模式
python3 scripts/debug_mode.py off
# 查看状态
python3 scripts/debug_mode.py status⏰ 设置定时检查
# 每天上午9点检查
openclaw cron add \
--name "last-words-check" \
--schedule "0 9 * * *" \
--command "python3 ~/.openclaw/skills/last-words/scripts/check_activity.py"或使用系统 cron:
crontab -e
# 添加:0 9 * * * python3 ~/.openclaw/skills/last-words/scripts/check_activity.py📖 完整使用流程
1. 安装 skill
→ openclaw skill install last-words
2. 配置邮箱
→ 说"配置最后留言邮箱"并按提示输入
3. 设置留言
→ 说"我想给我爸妈留下最后一句话"
→ 输入文字或上传语音
4. 测试发送
→ 说"最后留言 开启调试模式"
→ 说"最后留言 立即发送测试"
5. 关闭调试
→ 说"最后留言 关闭调试模式"
6. 等待生效
→ 系统会自动检测,30天无活动后发送📋 支持的邮箱
| 邮箱 | SMTP服务器 | 端口 | 说明 |
|---|---|---|---|
| QQ邮箱 | smtp.qq.com | 465 | 需要授权码 |
| 163邮箱 | smtp.163.com | 465 | 需要授权码 |
| Gmail | smtp.gmail.com | 587 | 需要应用专用密码 |
| Outlook | smtp.office365.com | 587 | 需要应用密码 |
🛠️ 命令参考
| 命令 | 说明 |
|---|---|
save_message.py --message "内容" | 保存文字留言 |
audio_manager.py save <文件> | 上传语音文件 |
audio_manager.py record | 录制语音 |
audio_manager.py play | 播放已保存语音 |
configure_delivery.py | 配置邮件发送 |
check_activity.py | 手动检查活动状态 |
debug_mode.py on/off/status/send | 调试模式管理 |
get_status.py | 查看当前配置状态 |
reset.py | 重置所有数据 |
🏗️ 项目结构
last-words/
├── SKILL.md # OpenClaw skill 定义
├── README.md # 本文档
├── .env.example # 环境变量示例
├── .gitignore # Git 忽略文件
├── scripts/
│ ├── database.py # 数据库管理
│ ├── save_message.py # 保存留言
│ ├── audio_manager.py # 语音管理
│ ├── configure_delivery.py # 配置邮件
│ ├── check_activity.py # 活动检查
│ ├── debug_mode.py # 调试模式
│ ├── get_status.py # 查看状态
│ └── reset.py # 重置数据
└── docs/ # 文档和图片🔒 隐私与安全
- ✅ 所有数据存储在本地 SQLite 数据库
- ✅ 密码存储在本地,不上传任何服务器
- ✅
.env文件已加入.gitignore,不会被提交 - ✅ 支持通过聊天交互配置,避免密码泄露在命令历史中
🐛 常见问题
<details> <summary><b>Q: 授权码是什么?和邮箱密码有什么区别?</b></summary>
A: 授权码是邮箱提供的第三方应用专用密码,不是登录密码。以 QQ 邮箱为例:
- 登录密码:你平时登录邮箱的密码
- 授权码:16位随机字符,专门用于 SMTP 发送邮件
获取方式:QQ邮箱 → 设置 → 账户 → 开启SMTP服务 → 获取授权码
</details>
<details> <summary><b>Q: 支持哪些邮箱?</b></summary>
A: 目前测试支持:QQ邮箱、163邮箱、Gmail、Outlook。理论上支持所有提供 SMTP 服务的邮箱。
QQ邮箱和163邮箱使用 SSL(465端口),Gmail和Outlook使用 STARTTLS(587端口)。
</details>
<details> <summary><b>Q: 如何确认留言已设置成功?</b></summary>
A: 三种方式: 1. 说"最后留言 调试状态"查看配置 2. 运行 python3 scripts/get_status.py 3. 开启调试模式后立即发送测试
</details>
<details> <summary><b>Q: 30天是怎么计算的?</b></summary>
A: 从你最后一次与 OpenClaw 对话开始计算。每天定时检查:
- 10天无活动:发送第一次警告(给你)
- 20天无活动:发送第二次警告(给你)
- 30天无活动:自动发送最后留言(给父母)
只要你在30天内与 OpenClaw 说过话,计时就会重置。
</details>
<details> <summary><b>Q: 可以修改留言吗?</b></summary>
A: 可以,随时可以:
- 说"我想给我爸妈留下最后一句话"重新设置
- 或直接运行
python3 scripts/save_message.py --message "新内容"
新留言会覆盖旧留言。
</details>
🤝 贡献
欢迎提交 Issue 和 PR!
1. Fork 本项目 2. 创建你的 Feature Branch (git checkout -b feature/AmazingFeature) 3. 提交更改 (git commit -m 'Add some AmazingFeature') 4. 推送到 Branch (git push origin feature/AmazingFeature) 5. 打开 Pull Request
📄 许可证
MIT © 2024 OpenClaw Community
---
⚠️ 重要提醒
本项目涉及敏感的个人告别信息,请:
- 确保亲人知道这个功能的存在
- 定期测试确保邮件能正常发送
- 保持邮箱授权码有效(过期需重新获取)
- 妥善保管你的 OpenClaw 访问权限
愿这份技术能传递爱与牵挂,但更希望它永远不被触发。
#!/usr/bin/env python3
"""
Manage audio/voice message for last-words.
Usage:
python3 audio_manager.py save /path/to/audio.wav
python3 audio_manager.py record # Record from microphone
python3 audio_manager.py list # List saved audio
python3 audio_manager.py play # Play saved audio
"""
import argparse
import sys
import shutil
import subprocess
from pathlib import Path
from datetime import datetime
sys.path.insert(0, str(Path(__file__).parent))
from database import get_message, save_message, init_db
AUDIO_DIR = Path.home() / ".openclaw" / "last-words" / "audio"
def ensure_audio_dir():
"""Create audio directory if not exists."""
AUDIO_DIR.mkdir(parents=True, exist_ok=True)
def save_audio(source_path: str):
"""Save audio file to storage."""
ensure_audio_dir()
source = Path(source_path)
if not source.exists():
print(f"✗ Audio file not found: {source_path}")
return False
# Copy to storage with timestamp
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
dest_name = f"last_words_{timestamp}{source.suffix}"
dest = AUDIO_DIR / dest_name
shutil.copy2(source, dest)
# Update message with audio path
message = get_message()
text = message['content'] if message else "爸爸妈妈我爱你们"
save_message(text, str(dest))
print(f"✓ Audio saved: {dest}")
print(f" Original: {source}")
print(f" Size: {dest.stat().st_size / 1024:.1f} KB")
return True
def record_audio():
"""Record audio from microphone."""
ensure_audio_dir()
# Check for recording tools
recorder = None
if shutil.which("arecord"):
recorder = "arecord"
elif shutil.which("rec"):
recorder = "rec"
elif shutil.which("ffmpeg"):
recorder = "ffmpeg"
else:
print("✗ No audio recording tool found.")
print(" Please install one of: sox, alsa-utils, ffmpeg")
print(" Or use: save /path/to/existing/audio.wav")
return False
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output = AUDIO_DIR / f"last_words_{timestamp}.wav"
print("🎙️ Recording... Press Ctrl+C to stop")
print(" Speak now (说你想对父母说的话)...")
try:
if recorder == "arecord":
subprocess.run(["arecord", "-f", "cd", "-t", "wav", str(output)], check=True)
elif recorder == "rec":
subprocess.run(["rec", str(output)], check=True)
elif recorder == "ffmpeg":
subprocess.run(["ffmpeg", "-f", "alsa", "-i", "default", str(output)], check=True)
print(f"\n✓ Recording saved: {output}")
# Update message
message = get_message()
text = message['content'] if message else "爸爸妈妈我爱你们"
save_message(text, str(output))
print("✓ Audio attached to your message")
return True
except KeyboardInterrupt:
print(f"\n✓ Recording saved: {output}")
message = get_message()
text = message['content'] if message else "爸爸妈妈我爱你们"
save_message(text, str(output))
return True
except Exception as e:
print(f"✗ Recording failed: {e}")
return False
def list_audio():
"""List all saved audio files."""
ensure_audio_dir()
files = sorted(AUDIO_DIR.glob("*.wav")) + sorted(AUDIO_DIR.glob("*.mp3")) + sorted(AUDIO_DIR.glob("*.m4a"))
if not files:
print("No audio files found.")
return
print("Saved audio files:")
for i, f in enumerate(files, 1):
size = f.stat().st_size / 1024
print(f" {i}. {f.name} ({size:.1f} KB)")
def play_audio():
"""Play the saved audio."""
message = get_message()
if not message or not message.get('audio_path'):
print("✗ No audio attached to message.")
return
audio_path = Path(message['audio_path'])
if not audio_path.exists():
print(f"✗ Audio file not found: {audio_path}")
return
player = None
for cmd in ["aplay", "afplay", "mpg123", "ffplay"]:
if shutil.which(cmd):
player = cmd
break
if not player:
print("✗ No audio player found. Install: aplay, afplay, mpg123, or ffplay")
return
print(f"▶️ Playing: {audio_path.name}")
try:
if player == "ffplay":
subprocess.run([player, "-nodisp", "-autoexit", str(audio_path)], check=True)
else:
subprocess.run([player, str(audio_path)], check=True)
except Exception as e:
print(f"✗ Playback failed: {e}")
def main():
parser = argparse.ArgumentParser(description="Manage voice/audio for last-words")
subparsers = parser.add_subparsers(dest='command', help='Command')
# save command
save_parser = subparsers.add_parser('save', help='Save audio file')
save_parser.add_argument('path', help='Path to audio file')
# record command
subparsers.add_parser('record', help='Record from microphone')
# list command
subparsers.add_parser('list', help='List saved audio files')
# play command
subparsers.add_parser('play', help='Play saved audio')
args = parser.parse_args()
init_db()
if args.command == 'save':
save_audio(args.path)
elif args.command == 'record':
record_audio()
elif args.command == 'list':
list_audio()
elif args.command == 'play':
play_audio()
else:
parser.print_help()
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Check user activity and send warnings or deliver message if inactive.
This script should be run daily via cron.
Usage: python3 check_activity.py [--dry-run]
"""
import argparse
import sys
import subprocess
from pathlib import Path
from datetime import datetime, timedelta
sys.path.insert(0, str(Path(__file__).parent))
from database import (
get_last_chat, update_last_chat, log_check,
get_message, get_config, get_last_delivery_status, init_db
)
from debug_mode import is_debug_mode
# Warning thresholds (days)
WARNING_1_DAYS = 10
WARNING_2_DAYS = 20
DELIVERY_DAYS = 30
def send_warning_email(config, message, days):
"""Send warning email to the user (not the recipient)."""
try:
# Import smtplib here to avoid issues if not configured
import smtplib
from email.mime.text import MIMEText
subject = f"[Last Words] Inactivity Warning - {days} Days"
body = f"""
This is an automated warning from your Last Words system.
You have been inactive for {days} days.
If you reach {DELIVERY_DAYS} days of inactivity, your final message will be delivered to:
{config['contact']}
To reset the timer, simply chat with your OpenClaw agent.
---
Last Words System
"""
msg = MIMEText(body)
msg['Subject'] = subject
msg['From'] = config.get('smtp_user', 'last-words@system.local')
msg['To'] = config['contact'] # For now, send to the configured contact
# Send via SMTP if configured
if config.get('smtp_host'):
port = config.get('smtp_port', 587)
if port == 465:
with smtplib.SMTP_SSL(config['smtp_host'], port) as server:
if config.get('smtp_user') and config.get('smtp_pass'):
server.login(config['smtp_user'], config['smtp_pass'])
server.send_message(msg)
else:
with smtplib.SMTP(config['smtp_host'], port) as server:
server.starttls()
if config.get('smtp_user') and config.get('smtp_pass'):
server.login(config['smtp_user'], config['smtp_pass'])
server.send_message(msg)
print(f" ✓ Warning email sent via {config['smtp_host']}")
else:
# Fall back to local mail command
subprocess.run(['mail', '-s', subject, config['contact']],
input=body.encode(), check=False)
print(f" ✓ Warning email sent via local mail")
return True
except Exception as e:
print(f" ✗ Failed to send warning email: {e}", file=sys.stderr)
return False
def deliver_final_message(config, message, dry_run=False):
"""Deliver the final message to the configured recipient."""
if dry_run:
print(f" [DRY RUN] Would deliver message to {config['contact']}")
print(f" [DRY RUN] Message: {message['content'][:100]}...")
return True
try:
if config['method'] == 'email':
return deliver_via_email(config, message)
elif config['method'] == 'wechat':
print(" ⚠ WeChat delivery not yet implemented")
return False
elif config['method'] == 'phone':
print(" ⚠ Phone/SMS delivery not yet implemented")
return False
else:
print(f" ✗ Unknown delivery method: {config['method']}")
return False
except Exception as e:
print(f" ✗ Failed to deliver message: {e}", file=sys.stderr)
return False
def deliver_via_email(config, message):
"""Deliver message via email."""
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
subject = "[Last Words] A Message From Your Loved One"
body = f"""
Dear loved one,
You are receiving this message because it was set as a final message to be delivered
in the event that your loved one has been unreachable for an extended period.
---
{message['content']}
---
This message was prepared with care and love.
With deepest sincerity,
Last Words System
"""
msg = MIMEMultipart()
msg['Subject'] = subject
msg['From'] = config.get('smtp_user', 'last-words@system.local')
msg['To'] = config['contact']
msg.attach(MIMEText(body, 'plain'))
# Attach audio if available
if message.get('audio_path') and Path(message['audio_path']).exists():
from email.mime.audio import MIMEAudio
with open(message['audio_path'], 'rb') as f:
audio = MIMEAudio(f.read())
audio.add_header('Content-Disposition', 'attachment',
filename=Path(message['audio_path']).name)
msg.attach(audio)
print(f" ✓ Audio attachment included")
# Send via SMTP if configured
if config.get('smtp_host'):
port = config.get('smtp_port', 587)
# Use SSL for port 465, STARTTLS for port 587
if port == 465:
import smtplib
with smtplib.SMTP_SSL(config['smtp_host'], port) as server:
if config.get('smtp_user') and config.get('smtp_pass'):
server.login(config['smtp_user'], config['smtp_pass'])
server.send_message(msg)
else:
with smtplib.SMTP(config['smtp_host'], port) as server:
server.starttls()
if config.get('smtp_user') and config.get('smtp_pass'):
server.login(config['smtp_user'], config['smtp_pass'])
server.send_message(msg)
print(f" ✓ Final message delivered via {config['smtp_host']}")
else:
# Fall back to local mail command
subprocess.run(['mail', '-s', subject, config['contact']],
input=body.encode(), check=False)
print(f" ✓ Final message delivered via local mail")
return True
def check_sessions():
"""Check OpenClaw sessions for recent activity."""
try:
sessions_dir = Path.home() / ".openclaw" / "agents" / "main" / "sessions"
if not sessions_dir.exists():
return None
# Find the most recently modified session file
latest_time = None
for session_file in sessions_dir.glob("*.jsonl"):
mtime = datetime.fromtimestamp(session_file.stat().st_mtime)
if latest_time is None or mtime > latest_time:
latest_time = mtime
return latest_time
except Exception as e:
print(f" ⚠ Could not check sessions: {e}", file=sys.stderr)
return None
def main():
parser = argparse.ArgumentParser(description="Check activity and send warnings or deliver message")
parser.add_argument("--dry-run", "-n", action="store_true",
help="Show what would happen without taking action")
parser.add_argument("--debug-send", "-d", action="store_true",
help="Force immediate send (debug mode)")
args = parser.parse_args()
try:
init_db()
# Check for debug mode
debug_mode = is_debug_mode() or args.debug_send
print(f"\n⏰ Last Words Activity Check - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("=" * 50)
if debug_mode:
print("\n🐛 DEBUG MODE ENABLED")
print(" Messages can be sent immediately without 30-day wait")
print("=" * 50)
# Check for recent session activity
last_session = check_sessions()
if last_session:
print(f" Last session activity: {last_session.strftime('%Y-%m-%d %H:%M:%S')}")
# Update last_chat if session is more recent
last_chat = get_last_chat()
if last_session > last_chat:
if not args.dry_run:
update_last_chat()
print(" ✓ Updated last activity from session data")
else:
print(" [DRY RUN] Would update last activity from session data")
# Get current inactivity period
last_chat = get_last_chat()
days_inactive = (datetime.now() - last_chat).days
print(f" Days since last chat: {days_inactive}")
# Get message and config
message = get_message()
config = get_config()
last_status = get_last_delivery_status()
if not message:
print("\n ⚠ No message configured. Skipping.")
log_check(days_inactive, warning_sent=False, warning_level=0, delivered=False)
return
if not config:
print("\n ⚠ No delivery configuration. Skipping.")
log_check(days_inactive, warning_sent=False, warning_level=0, delivered=False)
return
# Determine action based on inactivity OR debug mode
action_taken = False
warning_sent = False
warning_level = 0
delivered = False
if debug_mode:
# Debug mode: allow immediate send
print(f"\n 🐛 DEBUG MODE: Bypassing 30-day wait")
print(f" Ready to deliver message to {config['contact']}...")
if args.dry_run:
print(f" [DRY RUN] Would deliver message immediately in debug mode")
else:
if deliver_final_message(config, message, args.dry_run):
delivered = True
action_taken = True
print(f"\n ✓ Message delivered via DEBUG MODE")
elif days_inactive >= DELIVERY_DAYS:
# Time to deliver
print(f"\n 🚨 INACTIVITY THRESHOLD REACHED ({days_inactive} days)")
print(f" Delivering final message to {config['contact']}...")
if deliver_final_message(config, message, args.dry_run):
delivered = True
action_taken = True
elif days_inactive >= WARNING_2_DAYS:
# Second warning
if not last_status or last_status.get('warning_level', 0) < 2:
print(f"\n ⚠️ SECOND WARNING ({days_inactive} days inactive)")
print(f" Sending warning to {config['contact']}...")
if not args.dry_run:
warning_sent = send_warning_email(config, message, days_inactive)
else:
print(f" [DRY RUN] Would send second warning")
warning_sent = True
warning_level = 2
action_taken = True
else:
print(f" Second warning already sent")
elif days_inactive >= WARNING_1_DAYS:
# First warning
if not last_status or last_status.get('warning_level', 0) < 1:
print(f"\n ⚠️ FIRST WARNING ({days_inactive} days inactive)")
print(f" Sending warning to {config['contact']}...")
if not args.dry_run:
warning_sent = send_warning_email(config, message, days_inactive)
else:
print(f" [DRY RUN] Would send first warning")
warning_sent = True
warning_level = 1
action_taken = True
else:
print(f" First warning already sent")
else:
print(f"\n ✓ Activity normal ({days_inactive} days)")
# Log the check
if not args.dry_run:
log_check(days_inactive, warning_sent, warning_level, delivered)
print("\n" + "=" * 50)
if delivered:
print(" ✓ FINAL MESSAGE DELIVERED")
elif action_taken:
print(f" ✓ Action taken (warning level: {warning_level})")
else:
print(" ✓ Check complete")
except Exception as e:
print(f"\n ✗ Error during check: {e}", file=sys.stderr)
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Configure delivery settings for last-words.
Usage: python3 configure_delivery.py --method email|wechat|phone --contact "value"
[--smtp-host host] [--smtp-port port] [--smtp-user user] [--smtp-pass pass]
Or use environment variables (recommended for security):
SMTP_USER, SMTP_PASS, CONTACT_EMAIL, SMTP_HOST, SMTP_PORT
Or create a .env file from .env.example
"""
import argparse
import os
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from database import save_config, init_db
def load_env_file():
"""Load environment variables from .env file if it exists."""
env_path = Path(__file__).parent.parent / ".env"
if env_path.exists():
with open(env_path) as f:
for line in f:
line = line.strip()
if not line or line.startswith('#') or '=' not in line:
continue
key, value = line.split('=', 1)
# Only set if not already in environment
if key not in os.environ:
os.environ[key] = value
def get_env_config():
"""Get configuration from environment variables."""
return {
'smtp_user': os.environ.get('SMTP_USER'),
'smtp_pass': os.environ.get('SMTP_PASS'),
'contact': os.environ.get('CONTACT_EMAIL'),
'smtp_host': os.environ.get('SMTP_HOST', 'smtp.qq.com'),
'smtp_port': int(os.environ.get('SMTP_PORT', '465')),
}
def main():
parser = argparse.ArgumentParser(description="Configure delivery settings")
parser.add_argument("--method", "-m",
choices=["email", "wechat", "phone"],
help="Delivery method (default: email)")
parser.add_argument("--contact", "-c",
help="Contact address (email, wechat id, or phone number)")
parser.add_argument("--smtp-host", help="SMTP server host (for email)")
parser.add_argument("--smtp-port", type=int, help="SMTP server port")
parser.add_argument("--smtp-user", help="SMTP username")
parser.add_argument("--smtp-pass", help="SMTP password")
parser.add_argument("--from-env", "-e", action="store_true",
help="Load configuration from environment variables or .env file")
args = parser.parse_args()
try:
init_db()
# Load from env file if exists
load_env_file()
# If --from-env flag is set, use environment variables
if args.from_env:
env_config = get_env_config()
if not env_config['smtp_user'] or not env_config['smtp_pass'] or not env_config['contact']:
print("✗ Missing required environment variables:", file=sys.stderr)
print(" Required: SMTP_USER, SMTP_PASS, CONTACT_EMAIL", file=sys.stderr)
print(" Optional: SMTP_HOST (default: smtp.qq.com), SMTP_PORT (default: 465)", file=sys.stderr)
sys.exit(1)
save_config(
method="email",
contact=env_config['contact'],
smtp_host=env_config['smtp_host'],
smtp_port=env_config['smtp_port'],
smtp_user=env_config['smtp_user'],
smtp_pass=env_config['smtp_pass']
)
print(f"✓ Configuration loaded from environment/.env file")
print(f" SMTP User: {env_config['smtp_user']}")
print(f" Contact: {env_config['contact']}")
print(f" SMTP: {env_config['smtp_host']}:{env_config['smtp_port']}")
return
# Otherwise require manual arguments
if not args.method or not args.contact:
parser.print_help()
print("\n✗ Error: --method and --contact are required (or use --from-env)", file=sys.stderr)
sys.exit(1)
# For non-email methods, warn that they're not fully implemented
if args.method == "wechat":
print("⚠ WeChat delivery is planned but not yet implemented.")
print(" Configuration saved but delivery will not work until implemented.")
elif args.method == "phone":
print("⚠ Phone/SMS delivery is planned but not yet implemented.")
print(" Configuration saved but delivery will not work until implemented.")
save_config(
method=args.method,
contact=args.contact,
smtp_host=args.smtp_host,
smtp_port=args.smtp_port,
smtp_user=args.smtp_user,
smtp_pass=args.smtp_pass
)
print(f"✓ Delivery configuration saved")
print(f" Method: {args.method}")
print(f" Contact: {args.contact}")
if args.smtp_host:
print(f" SMTP: {args.smtp_host}:{args.smtp_port}")
except Exception as e:
print(f"✗ Error saving configuration: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Database management for last-words skill.
Stores messages, configuration, and activity logs in SQLite.
"""
import sqlite3
import os
from datetime import datetime
from pathlib import Path
DB_DIR = Path.home() / ".openclaw" / "last-words"
DB_PATH = DB_DIR / "data.db"
def init_db():
"""Initialize the database with required tables."""
DB_DIR.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# Main message table
cursor.execute("""
CREATE TABLE IF NOT EXISTS message (
id INTEGER PRIMARY KEY AUTOINCREMENT,
content TEXT NOT NULL,
audio_path TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# Delivery configuration table
cursor.execute("""
CREATE TABLE IF NOT EXISTS config (
id INTEGER PRIMARY KEY AUTOINCREMENT,
method TEXT CHECK(method IN ('email', 'wechat', 'phone')),
contact TEXT NOT NULL,
smtp_host TEXT,
smtp_port INTEGER,
smtp_user TEXT,
smtp_pass TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# Activity log table
cursor.execute("""
CREATE TABLE IF NOT EXISTS activity_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
last_chat_at TIMESTAMP,
days_inactive INTEGER,
warning_sent INTEGER DEFAULT 0,
warning_level INTEGER DEFAULT 0,
delivered INTEGER DEFAULT 0,
delivered_at TIMESTAMP,
checked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# Status tracking table
cursor.execute("""
CREATE TABLE IF NOT EXISTS status (
key TEXT PRIMARY KEY,
value TEXT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# Initialize last_chat_at if not exists
cursor.execute("INSERT OR IGNORE INTO status (key, value) VALUES ('last_chat_at', ?)",
(datetime.now().isoformat(),))
conn.commit()
conn.close()
def save_message(content: str, audio_path: str = None):
"""Save or update the final message."""
init_db()
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# Clear old message, keep only one
cursor.execute("DELETE FROM message")
cursor.execute(
"INSERT INTO message (content, audio_path) VALUES (?, ?)",
(content, audio_path)
)
conn.commit()
conn.close()
return True
def get_message():
"""Get the stored message."""
init_db()
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("SELECT content, audio_path, created_at FROM message ORDER BY id DESC LIMIT 1")
row = cursor.fetchone()
conn.close()
if row:
return {"content": row[0], "audio_path": row[1], "created_at": row[2]}
return None
def save_config(method: str, contact: str, smtp_host: str = None, smtp_port: int = None,
smtp_user: str = None, smtp_pass: str = None):
"""Save delivery configuration."""
init_db()
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("DELETE FROM config")
cursor.execute("""
INSERT INTO config (method, contact, smtp_host, smtp_port, smtp_user, smtp_pass)
VALUES (?, ?, ?, ?, ?, ?)
""", (method, contact, smtp_host, smtp_port, smtp_user, smtp_pass))
conn.commit()
conn.close()
return True
def get_config():
"""Get delivery configuration."""
init_db()
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("SELECT method, contact, smtp_host, smtp_port, smtp_user, smtp_pass FROM config LIMIT 1")
row = cursor.fetchone()
conn.close()
if row:
return {
"method": row[0],
"contact": row[1],
"smtp_host": row[2],
"smtp_port": row[3],
"smtp_user": row[4],
"smtp_pass": row[5]
}
return None
def update_last_chat():
"""Update the last chat timestamp to now."""
init_db()
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
now = datetime.now().isoformat()
cursor.execute(
"INSERT OR REPLACE INTO status (key, value) VALUES ('last_chat_at', ?)",
(now,)
)
conn.commit()
conn.close()
def get_last_chat():
"""Get the last chat timestamp."""
init_db()
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("SELECT value FROM status WHERE key = 'last_chat_at'")
row = cursor.fetchone()
conn.close()
if row and row[0]:
return datetime.fromisoformat(row[0])
return datetime.now()
def log_check(days_inactive: int, warning_sent: bool = False, warning_level: int = 0,
delivered: bool = False):
"""Log an activity check."""
init_db()
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
last_chat = get_last_chat()
delivered_at = datetime.now().isoformat() if delivered else None
cursor.execute("""
INSERT INTO activity_log (last_chat_at, days_inactive, warning_sent, warning_level, delivered, delivered_at)
VALUES (?, ?, ?, ?, ?, ?)
""", (last_chat.isoformat(), days_inactive, int(warning_sent), warning_level,
int(delivered), delivered_at))
conn.commit()
conn.close()
def get_last_delivery_status():
"""Get the last delivery/warning status."""
init_db()
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("""
SELECT days_inactive, warning_sent, warning_level, delivered, checked_at
FROM activity_log
ORDER BY id DESC LIMIT 1
""")
row = cursor.fetchone()
conn.close()
if row:
return {
"days_inactive": row[0],
"warning_sent": bool(row[1]),
"warning_level": row[2],
"delivered": bool(row[3]),
"checked_at": row[4]
}
return None
def reset_all():
"""Reset all data."""
init_db()
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("DELETE FROM message")
cursor.execute("DELETE FROM config")
cursor.execute("DELETE FROM activity_log")
cursor.execute("DELETE FROM status")
conn.commit()
conn.close()
#!/usr/bin/env python3
"""
Debug mode management for last-words skill.
When debug mode is enabled, messages can be sent immediately without waiting for 30 days.
Usage:
python3 debug_mode.py on # Enable debug mode
python3 debug_mode.py off # Disable debug mode
python3 debug_mode.py status # Check debug mode status
python3 debug_mode.py send # Immediate send in debug mode
"""
import argparse
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from database import init_db, get_config, get_message
import sqlite3
def get_db_path():
return Path.home() / ".openclaw" / "last-words" / "data.db"
def set_debug_mode(enabled: bool):
"""Enable or disable debug mode."""
init_db()
conn = sqlite3.connect(get_db_path())
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS debug_config (
key TEXT PRIMARY KEY,
value TEXT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
cursor.execute(
"INSERT OR REPLACE INTO debug_config (key, value) VALUES ('debug_mode', ?)",
("1" if enabled else "0",)
)
conn.commit()
conn.close()
status = "enabled" if enabled else "disabled"
print(f"✓ Debug mode {status}")
if enabled:
print(" ⚠️ Warning: Messages can now be sent immediately without waiting 30 days")
print(" 💡 Use 'debug_mode.py send' to trigger immediate delivery")
def is_debug_mode():
"""Check if debug mode is enabled."""
init_db()
conn = sqlite3.connect(get_db_path())
cursor = conn.cursor()
try:
cursor.execute("SELECT value FROM debug_config WHERE key = 'debug_mode'")
row = cursor.fetchone()
conn.close()
return row is not None and row[0] == "1"
except:
conn.close()
return False
def get_status():
"""Get debug mode status."""
debug = is_debug_mode()
print("=" * 50)
print("Debug Mode Status")
print("=" * 50)
if debug:
print("\n🐛 Debug Mode: ENABLED")
print(" ⚠️ Messages can be sent immediately")
print(" 💡 Use 'python3 debug_mode.py send' to test delivery")
else:
print("\n✓ Debug Mode: DISABLED")
print(" Normal operation: 30-day inactivity required")
# Show current config
config = get_config()
message = get_message()
print(f"\n📋 Current Setup:")
if message:
print(f" Message: ✓ Set ({len(message['content'])} chars)")
if message.get('audio_path'):
print(f" Audio: ✓ Attached")
else:
print(f" Message: ✗ Not set")
if config:
print(f" Delivery: {config['method']} → {config['contact']}")
else:
print(f" Delivery: ✗ Not configured")
print("\n" + "=" * 50)
def immediate_send():
"""Trigger immediate send in debug mode."""
if not is_debug_mode():
print("✗ Debug mode is not enabled!")
print(" Run: python3 debug_mode.py on")
return False
config = get_config()
message = get_message()
if not message:
print("✗ No message set!")
print(" Run: python3 save_message.py --message \"your message\"")
return False
if not config:
print("✗ No delivery configuration!")
print(" Run: python3 configure_delivery.py --method email --contact \"email@example.com\"")
return False
print("🐛 Debug Mode: Triggering immediate send...")
print("")
# Import here to avoid circular imports
from check_activity import deliver_via_email, log_check
from datetime import datetime
try:
success = deliver_via_email(config, message)
if success:
# Log the delivery
log_check(days_inactive=999, warning_sent=False, warning_level=0, delivered=True)
print("")
print("✓ Message delivered successfully in DEBUG mode!")
print(f" Sent to: {config['contact']}")
print(f" Method: {config['method']}")
if message.get('audio_path'):
print(f" Audio: Included")
else:
print("✗ Delivery failed")
return success
except Exception as e:
print(f"✗ Delivery failed: {e}")
import traceback
traceback.print_exc()
return False
def main():
parser = argparse.ArgumentParser(description="Debug mode for last-words skill")
parser.add_argument("command", choices=["on", "off", "status", "send"],
help="Command: on/off (toggle), status (check), send (immediate delivery)")
args = parser.parse_args()
try:
if args.command == "on":
set_debug_mode(True)
elif args.command == "off":
set_debug_mode(False)
elif args.command == "status":
get_status()
elif args.command == "send":
immediate_send()
except Exception as e:
print(f"✗ Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Get current status of last-words configuration.
Usage: python3 get_status.py
"""
import sys
from pathlib import Path
from datetime import datetime
sys.path.insert(0, str(Path(__file__).parent))
from database import get_message, get_config, get_last_chat, get_last_delivery_status, init_db
def main():
try:
init_db()
print("=" * 50)
print("Last Words - 最后留言 状态")
print("=" * 50)
# Message status
message = get_message()
if message:
print(f"\n📝 Message: ✓ Saved")
print(f" Content: {message['content'][:60]}{'...' if len(message['content']) > 60 else ''}")
print(f" Saved at: {message['created_at']}")
if message['audio_path']:
print(f" Audio: {message['audio_path']}")
else:
print("\n📝 Message: ✗ Not set")
# Config status
config = get_config()
if config:
print(f"\n📬 Delivery: ✓ Configured")
print(f" Method: {config['method']}")
print(f" Contact: {config['contact']}")
if config['smtp_host']:
print(f" SMTP: {config['smtp_host']}:{config['smtp_port']}")
else:
print("\n📬 Delivery: ✗ Not configured")
# Activity status
last_chat = get_last_chat()
days_inactive = (datetime.now() - last_chat).days
print(f"\n⏰ Activity:")
print(f" Last chat: {last_chat.strftime('%Y-%m-%d %H:%M:%S')}")
print(f" Days inactive: {days_inactive}")
# Warning levels
status = get_last_delivery_status()
if status:
print(f"\n📊 Last Check:")
print(f" Warning sent: {'Yes' if status['warning_sent'] else 'No'}")
if status['warning_level'] > 0:
print(f" Warning level: Day {status['warning_level'] * 10}")
print(f" Delivered: {'Yes' if status['delivered'] else 'No'}")
if status['delivered']:
print(f" Delivered at: {status['checked_at']}")
# Thresholds
print(f"\n⚠️ Warning Thresholds:")
print(f" 10 days: First warning")
print(f" 20 days: Second warning")
print(f" 30 days: Auto-delivery")
print("\n" + "=" * 50)
except Exception as e:
print(f"✗ Error getting status: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Reset all last-words data.
Usage: python3 reset.py [--force]
"""
import argparse
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from database import reset_all, init_db
def main():
parser = argparse.ArgumentParser(description="Reset all last-words data")
parser.add_argument("--force", "-f", action="store_true",
help="Skip confirmation prompt")
args = parser.parse_args()
if not args.force:
response = input("⚠️ This will delete all messages and configuration. Continue? [y/N]: ")
if response.lower() not in ["y", "yes"]:
print("Cancelled.")
sys.exit(0)
try:
init_db()
reset_all()
print("✓ All data reset successfully")
except Exception as e:
print(f"✗ Error resetting data: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Save or update the final message.
Usage: python3 save_message.py --message "content" [--audio-path /path/to/audio]
"""
import argparse
import sys
from pathlib import Path
# Add script directory to path
sys.path.insert(0, str(Path(__file__).parent))
from database import save_message, init_db
def main():
parser = argparse.ArgumentParser(description="Save final message")
parser.add_argument("--message", "-m", required=True, help="Message content (text)")
parser.add_argument("--audio-path", "-a", help="Path to audio file (optional)")
args = parser.parse_args()
try:
init_db()
save_message(args.message, args.audio_path)
print(f"✓ Message saved successfully")
print(f" Content: {args.message[:50]}{'...' if len(args.message) > 50 else ''}")
if args.audio_path:
print(f" Audio: {args.audio_path}")
except Exception as e:
print(f"✗ Error saving message: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Secure storage for sensitive data using encryption.
Uses Fernet symmetric encryption from cryptography library.
"""
import os
import base64
import hashlib
from pathlib import Path
try:
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2
HAS_CRYPTO = True
except ImportError:
HAS_CRYPTO = False
print("Warning: cryptography not installed. Sensitive data will be stored as plaintext.")
print("Install: pip3 install cryptography")
SALT_FILE = Path.home() / ".openclaw" / "last-words" / ".salt"
def _get_or_create_salt():
"""Get or create a random salt for key derivation."""
if SALT_FILE.exists():
return SALT_FILE.read_bytes()
salt = os.urandom(16)
SALT_FILE.parent.mkdir(parents=True, exist_ok=True)
SALT_FILE.write_bytes(salt)
os.chmod(SALT_FILE, 0o600) # Owner read/write only
return salt
def _derive_key(password: str) -> bytes:
"""Derive encryption key from password."""
if not HAS_CRYPTO:
return None
salt = _get_or_create_salt()
kdf = PBKDF2(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=480000,
)
key = base64.urlsafe_b64encode(kdf.derive(password.encode()))
return key
def encrypt_sensitive_data(data: str, master_password: str) -> str:
"""Encrypt sensitive data using master password."""
if not HAS_CRYPTO or not master_password:
return data # Fallback to plaintext
key = _derive_key(master_password)
f = Fernet(key)
encrypted = f.encrypt(data.encode())
return base64.urlsafe_b64encode(encrypted).decode()
def decrypt_sensitive_data(encrypted_data: str, master_password: str) -> str:
"""Decrypt sensitive data using master password."""
if not HAS_CRYPTO or not master_password:
return encrypted_data # Assume plaintext fallback
try:
key = _derive_key(master_password)
f = Fernet(key)
decrypted = f.decrypt(base64.urlsafe_b64decode(encrypted_data.encode()))
return decrypted.decode()
except Exception:
# Decryption failed - wrong password or corrupted data
return None
def check_master_password_set():
"""Check if master password environment variable is set."""
return bool(os.environ.get('LAST_WORDS_MASTER_PASSWORD'))
def secure_store(smtp_pass: str) -> str:
"""Store SMTP password securely if master password is set."""
master = os.environ.get('LAST_WORDS_MASTER_PASSWORD')
if master:
return encrypt_sensitive_data(smtp_pass, master)
return smtp_pass # Store as plaintext (fallback)
def secure_retrieve(stored_value: str) -> str:
"""Retrieve and decrypt SMTP password."""
master = os.environ.get('LAST_WORDS_MASTER_PASSWORD')
if master and HAS_CRYPTO:
decrypted = decrypt_sensitive_data(stored_value, master)
if decrypted is None:
raise ValueError("Failed to decrypt: wrong master password or corrupted data")
return decrypted
return stored_value # Assume plaintext
#!/usr/bin/env python3
"""
Update the last chat timestamp to now.
This should be called whenever the user interacts with the agent.
Usage: python3 update_activity.py
"""
import sys
from pathlib import Path
from datetime import datetime
sys.path.insert(0, str(Path(__file__).parent))
from database import update_last_chat, init_db
def main():
try:
init_db()
update_last_chat()
print(f"✓ Activity timestamp updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
except Exception as e:
print(f"✗ Error updating activity: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()