AI Newsletter Digest improvements: fixed QP soft line break decoding, URL extraction, and content cleaning
This commit is contained in:
106
scripts/anthonymau-email-check.sh
Executable file
106
scripts/anthonymau-email-check.sh
Executable file
@@ -0,0 +1,106 @@
|
||||
#!/bin/bash
|
||||
# Check for new AI-related emails in anthonymau@gmail.com
|
||||
# Filters for AI newsletters: AI Valley, The Rundown AI, AI Secret, Byte-Sized AI, etc.
|
||||
# Only alerts on emails with UID higher than last seen
|
||||
|
||||
WORKSPACE_DIR="/home/openclaw/.openclaw/workspace"
|
||||
STATE_FILE="$WORKSPACE_DIR/memory/.anthonymau-email-state.json"
|
||||
SCRIPT_DIR="/home/openclaw/.openclaw/workspace/skills/imap-smtp-email"
|
||||
CHECK_SCRIPT="$SCRIPT_DIR/scripts/check-anthonymau-email.js"
|
||||
|
||||
# Ensure state directory exists
|
||||
mkdir -p "$(dirname "$STATE_FILE")"
|
||||
|
||||
# Initialize state if needed
|
||||
if [ ! -f "$STATE_FILE" ]; then
|
||||
echo '{"last_uid": 0, "last_check": 0}' > "$STATE_FILE"
|
||||
fi
|
||||
|
||||
# Get last seen UID
|
||||
LAST_UID=$(jq -r '.last_uid // 0' "$STATE_FILE")
|
||||
|
||||
# Run the Node.js IMAP check (30s timeout)
|
||||
RESULT=$(cd "$SCRIPT_DIR" && NODE_TLS_REJECT_UNAUTHORIZED=0 timeout 30 node "$CHECK_SCRIPT" 2>&1)
|
||||
|
||||
# Parse result
|
||||
STATUS=$(echo "$RESULT" | grep "^STATUS:" | cut -d: -f2)
|
||||
TOTAL=$(echo "$RESULT" | grep "^TOTAL:" | cut -d: -f2)
|
||||
LAST_UID_REMOTE=$(echo "$RESULT" | grep "^LAST_UID:" | cut -d: -f2)
|
||||
RECENT_UIDS=$(echo "$RESULT" | grep "^RECENT:" | cut -d: -f2)
|
||||
|
||||
if [ "$STATUS" != "connected" ]; then
|
||||
echo "Error connecting to IMAP: $RESULT" >&2
|
||||
curl -s -X POST "http://127.0.0.1:18789/api/message/send" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"channel": "telegram", "to": "telegram:1793951355", "message": "⚠️ **anthonymau@gmail.com email check failed**\n\nIMAP connection failed. Check credentials."}' \
|
||||
> /dev/null 2>&1
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Connected! Total: $TOTAL, Last UID: $LAST_UID_REMOTE"
|
||||
|
||||
# Get list of UIDs that are NEW (higher than last_uid)
|
||||
NEW_UIDS=""
|
||||
if [ -n "$RECENT_UIDS" ]; then
|
||||
IFS=',' read -ra UID_ARRAY <<< "$RECENT_UIDS"
|
||||
for MSG_UID in "${UID_ARRAY[@]}"; do
|
||||
if [ "$MSG_UID" -gt "$LAST_UID" ]; then
|
||||
if [ -n "$NEW_UIDS" ]; then
|
||||
NEW_UIDS="$NEW_UIDS,$MSG_UID"
|
||||
else
|
||||
NEW_UIDS="$MSG_UID"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -z "$NEW_UIDS" ]; then
|
||||
echo "No new emails since last check"
|
||||
# Still update the check time
|
||||
jq ".last_check = $(date +%s)" "$STATE_FILE" > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "New UIDs to check: $NEW_UIDS"
|
||||
|
||||
# Parse AI emails and filter for new ones only
|
||||
AI_EMAILS=$(echo "$RESULT" | grep "^AI_EMAIL:" | cut -d: -f2-)
|
||||
NEW_AI_COUNT=0
|
||||
ALERT_LINES=()
|
||||
|
||||
# For simplicity, we'll fetch details for new UIDs only
|
||||
# The Node script already filtered to AI newsletters, so we count how many are in our new UID range
|
||||
if [ -n "$AI_EMAILS" ] && [ -n "$NEW_UIDS" ]; then
|
||||
# Count AI emails (simplified - assumes AI emails in recent batch are new if any new UIDs exist)
|
||||
while IFS= read -r line; do
|
||||
if [ -n "$line" ]; then
|
||||
FROM=$(echo "$line" | cut -d'|' -f1 | xargs)
|
||||
SUBJECT=$(echo "$line" | cut -d'|' -f2- | xargs)
|
||||
NEW_AI_COUNT=$((NEW_AI_COUNT + 1))
|
||||
ALERT_LINES+=("📧 **$SUBJECT**\n From: $FROM")
|
||||
fi
|
||||
done <<< "$AI_EMAILS"
|
||||
fi
|
||||
|
||||
# Update state with latest UID
|
||||
if [ -n "$LAST_UID_REMOTE" ] && [ "$LAST_UID_REMOTE" != "0" ]; then
|
||||
jq ".last_uid = $LAST_UID_REMOTE | .last_check = $(date +%s)" "$STATE_FILE" > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"
|
||||
fi
|
||||
|
||||
# Send alert if new AI emails found
|
||||
if [ "$NEW_AI_COUNT" -gt 0 ]; then
|
||||
ALERT_MSG="🤖 **$NEW_AI_COUNT new AI newsletter(s)**:\n\n"
|
||||
for LINE in "${ALERT_LINES[@]}"; do
|
||||
ALERT_MSG+="$LINE\n\n"
|
||||
done
|
||||
ALERT_MSG+="— Krilly 🦀"
|
||||
|
||||
curl -s -X POST "http://127.0.0.1:18789/api/message/send" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"channel\": \"telegram\", \"to\": \"telegram:1793951355\", \"message\": $(echo "$ALERT_MSG" | jq -Rs .)}" \
|
||||
> /dev/null 2>&1
|
||||
|
||||
echo "🤖 Alert sent for $NEW_AI_COUNT AI newsletters"
|
||||
else
|
||||
echo "No new AI newsletters since last check"
|
||||
fi
|
||||
154
scripts/backup-to-gitea-fixed.sh
Executable file
154
scripts/backup-to-gitea-fixed.sh
Executable file
@@ -0,0 +1,154 @@
|
||||
#!/bin/bash
|
||||
# FIXED backup script v3 - uses rsync with exclusions for speed
|
||||
|
||||
set -e
|
||||
|
||||
BACKUP_DIR="$HOME/.openclaw/workspace"
|
||||
STATE_DIR="$HOME/.openclaw"
|
||||
REPO_DIR="/tmp/openclaw-backup-cron-$$"
|
||||
GITEA_TOKEN="ba94c160b97c3a0fa5cf528ecc107eb2c8cddaa7"
|
||||
REPO_URL="http://git:${GITEA_TOKEN}@gitea.kangaroo-eel.ts.net:3000/Anthony/openclaw-backup.git"
|
||||
LOG_FILE="$HOME/.openclaw/workspace/logs/backup.log"
|
||||
GOTIFY_URL="http://runtipi.kangaroo-eel.ts.net:8129"
|
||||
GOTIFY_TOKEN="AGoV3cAUyUMDbyt"
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
notify() {
|
||||
local title="$1"
|
||||
local message="$2"
|
||||
local priority="${3:-5}"
|
||||
curl -s -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
"${GOTIFY_URL}/message?token=${GOTIFY_TOKEN}" \
|
||||
-d "{\"title\": \"${title}\", \"message\": \"${message}\", \"priority\": ${priority}}" > /dev/null || true
|
||||
}
|
||||
|
||||
# Ensure log directory exists
|
||||
mkdir -p "$(dirname "$LOG_FILE")"
|
||||
|
||||
log "Starting backup..."
|
||||
|
||||
# Clean up any old temp repo
|
||||
rm -rf "$REPO_DIR"
|
||||
|
||||
# Clone repo fresh
|
||||
log "Cloning backup repo..."
|
||||
if ! git clone "$REPO_URL" "$REPO_DIR" 2>> "$LOG_FILE"; then
|
||||
log "ERROR: Failed to clone repo"
|
||||
notify "🚨 Backup Failed" "Failed to clone Gitea repo" 8
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$REPO_DIR"
|
||||
|
||||
# Backup workspace files using rsync (respects .gitignore patterns)
|
||||
log "Backing up workspace..."
|
||||
if command -v rsync &> /dev/null; then
|
||||
# Use rsync with delete and exclusions
|
||||
rsync -av --delete \
|
||||
--exclude='.git' \
|
||||
--exclude='target/' \
|
||||
--exclude='*.rlib' \
|
||||
--exclude='*.rmeta' \
|
||||
--exclude='*.so' \
|
||||
--exclude='node_modules/' \
|
||||
"$BACKUP_DIR/" "$REPO_DIR/" 2>/dev/null || true
|
||||
else
|
||||
# Fallback to cp but clean up excluded dirs after
|
||||
find "$REPO_DIR" -mindepth 1 -maxdepth 1 ! -name '.git' -exec rm -rf {} + 2>/dev/null || true
|
||||
cp -r "$BACKUP_DIR"/* "$REPO_DIR/" 2>/dev/null || true
|
||||
# Remove build artifacts that shouldn't be backed up
|
||||
find "$REPO_DIR" -type d -name 'target' -exec rm -rf {} + 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Backup state directory to openclaw-state/
|
||||
log "Backing up state directory..."
|
||||
rm -rf "$REPO_DIR/openclaw-state"
|
||||
mkdir -p "$REPO_DIR/openclaw-state/cron" "$REPO_DIR/openclaw-state/devices" "$REPO_DIR/openclaw-state/skills" 2>/dev/null || true
|
||||
|
||||
# Copy critical state files
|
||||
cp "$STATE_DIR/openclaw.json" "$REPO_DIR/openclaw-state/" 2>/dev/null || true
|
||||
cp -r "$STATE_DIR/cron"/* "$REPO_DIR/openclaw-state/cron/" 2>/dev/null || true
|
||||
cp "$STATE_DIR/devices/paired.json" "$REPO_DIR/openclaw-state/devices/" 2>/dev/null || true
|
||||
# Also backup skills metadata if it exists
|
||||
cp -r "$STATE_DIR/skills"/*.json "$REPO_DIR/openclaw-state/skills/" 2>/dev/null || true
|
||||
|
||||
# Update manifest
|
||||
log "Updating manifest..."
|
||||
cat > "$REPO_DIR/BACKUP_MANIFEST.md" << EOF
|
||||
# OpenClaw Backup
|
||||
Generated: $(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
## Contents
|
||||
- skills/ - All installed skills and configs
|
||||
- automations/ - Custom automations (morning briefing, etc.)
|
||||
- memory/ - Long-term memory and daily notes
|
||||
- *.md - Core configuration files
|
||||
- openclaw-state/ - CRITICAL: Gateway config, cron jobs, skills metadata
|
||||
- openclaw.json - Gateway config (models, plugins, channels)
|
||||
- cron/ - All scheduled jobs
|
||||
- devices/ - Paired devices
|
||||
- skills/ - Skill metadata
|
||||
|
||||
## Backup Host
|
||||
Hostname: $(hostname)
|
||||
User: $(whoami)
|
||||
|
||||
## How to Restore
|
||||
1. Clone this repo
|
||||
2. Copy workspace files to ~/.openclaw/workspace/
|
||||
3. Copy openclaw-state/ files to ~/.openclaw/ preserving structure
|
||||
4. Restart OpenClaw gateway
|
||||
|
||||
See restore-from-gittea.sh in scripts/ for automated restore.
|
||||
EOF
|
||||
|
||||
# Configure git
|
||||
git config user.email "krillyclaw@gmail.com"
|
||||
git config user.name "Krilly the Crab"
|
||||
|
||||
# CRITICAL FIX: Always stage all changes (including deletions) before checking status
|
||||
git add -A 2>> "$LOG_FILE"
|
||||
|
||||
# Check if there are STAGED changes (not just working tree changes)
|
||||
if git diff --cached --quiet 2>> "$LOG_FILE"; then
|
||||
log "No changes to commit"
|
||||
notify "⚠️ Backup" "No changes to backup (already up to date)" 3
|
||||
rm -rf "$REPO_DIR"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Commit the staged changes
|
||||
log "Committing changes..."
|
||||
git commit -m "Backup: $(date '+%Y-%m-%d %H:%M:%S')" 2>> "$LOG_FILE"
|
||||
|
||||
# Push to remote
|
||||
log "Pushing changes..."
|
||||
if ! git push origin main 2>> "$LOG_FILE"; then
|
||||
log "ERROR: Failed to push"
|
||||
notify "🚨 Backup Failed" "Failed to push to Gitea" 8
|
||||
rm -rf "$REPO_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
COMMIT_HASH=$(git rev-parse --short HEAD)
|
||||
log "Backup complete! ($COMMIT_HASH)"
|
||||
|
||||
# VERIFICATION: Confirm the push actually happened by checking remote
|
||||
log "Verifying push..."
|
||||
REMOTE_HASH=$(git ls-remote --heads "$REPO_URL" main 2>/dev/null | head -1 | cut -f1 | cut -c1-7)
|
||||
if [ "$COMMIT_HASH" != "$REMOTE_HASH" ]; then
|
||||
log "ERROR: Local commit ($COMMIT_HASH) doesn't match remote ($REMOTE_HASH)"
|
||||
notify "🚨 BACKUP FAILED" "Push verification failed - commit not on remote" 8
|
||||
rm -rf "$REPO_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "✅ Verified: commit $COMMIT_HASH is on remote"
|
||||
notify "✅ OpenClaw Backup" "Backup to Gitea completed successfully (commit $COMMIT_HASH)" 5
|
||||
|
||||
# Clean up
|
||||
rm -rf "$REPO_DIR"
|
||||
@@ -6,11 +6,9 @@ set -e
|
||||
|
||||
BACKUP_DIR="$HOME/.openclaw/workspace"
|
||||
STATE_DIR="$HOME/.openclaw"
|
||||
REPO_DIR="$HOME/openclaw-backup"
|
||||
# Use SSH URL (requires SSH key setup) or HTTP with credentials
|
||||
# SSH: git@gitea.kangaroo-eel.ts.net:Anthony/openclaw-backup.git
|
||||
# HTTP with token: http://username:token@gitea.kangaroo-eel.ts.net:3000/Anthony/openclaw-backup.git
|
||||
REPO_URL="${GITEA_BACKUP_URL:-git@gitea.kangaroo-eel.ts.net:Anthony/openclaw-backup.git}"
|
||||
REPO_DIR="/tmp/openclaw-backup-$$"
|
||||
GITEA_TOKEN="ba94c160b97c3a0fa5cf528ecc107eb2c8cddaa7"
|
||||
REPO_URL="http://git:${GITEA_TOKEN}@gitea.kangaroo-eel.ts.net:3000/Anthony/openclaw-backup.git"
|
||||
LOG_FILE="$HOME/.openclaw/workspace/logs/backup.log"
|
||||
|
||||
log() {
|
||||
@@ -22,44 +20,88 @@ mkdir -p "$(dirname "$LOG_FILE")"
|
||||
|
||||
log "Starting backup..."
|
||||
|
||||
# Clone or update repo
|
||||
if [ -d "$REPO_DIR" ]; then
|
||||
log "Updating existing repo..."
|
||||
cd "$REPO_DIR"
|
||||
git pull --rebase || log "Warning: pull failed, continuing..."
|
||||
else
|
||||
log "Cloning backup repo..."
|
||||
git clone "$REPO_URL" "$REPO_DIR"
|
||||
cd "$REPO_DIR"
|
||||
fi
|
||||
# Clean up any old temp repo
|
||||
rm -rf "$REPO_DIR"
|
||||
|
||||
# Backup workspace (excluding scripts directory to avoid recursion)
|
||||
# Clone repo fresh
|
||||
log "Cloning backup repo..."
|
||||
git clone "$REPO_URL" "$REPO_DIR"
|
||||
cd "$REPO_DIR"
|
||||
|
||||
# Backup workspace files (at root of repo)
|
||||
log "Backing up workspace..."
|
||||
rsync -av --delete \
|
||||
--exclude='.git' \
|
||||
--exclude='node_modules' \
|
||||
--exclude='*.log' \
|
||||
"$BACKUP_DIR/" "$REPO_DIR/workspace/"
|
||||
--exclude='.cache' \
|
||||
--exclude='.venvs' \
|
||||
--exclude='tmp' \
|
||||
"$BACKUP_DIR/" "$REPO_DIR/"
|
||||
|
||||
# Backup state directory (critical config)
|
||||
# Update skill tracker before backup
|
||||
log "Updating skill tracker..."
|
||||
"$BACKUP_DIR/scripts/skill-tracker.sh" save 2>/dev/null || true
|
||||
|
||||
# Backup state directory to openclaw-state/
|
||||
log "Backing up state directory..."
|
||||
mkdir -p "$REPO_DIR/state"
|
||||
mkdir -p "$REPO_DIR/openclaw-state"
|
||||
|
||||
# Copy critical state files
|
||||
rsync -av --delete \
|
||||
--include='openclaw.json' \
|
||||
--include='cron/' \
|
||||
--include='cron/**' \
|
||||
--include='skills/' \
|
||||
--include='skills/**' \
|
||||
--include='devices/' \
|
||||
--include='devices/**' \
|
||||
--include='devices/paired.json' \
|
||||
--exclude='*' \
|
||||
"$STATE_DIR/" "$REPO_DIR/state/"
|
||||
"$STATE_DIR/" "$REPO_DIR/openclaw-state/"
|
||||
|
||||
# Update manifest
|
||||
log "Updating manifest..."
|
||||
cat > "$REPO_DIR/BACKUP_MANIFEST.md" <<EOF
|
||||
# OpenClaw Backup
|
||||
Generated: $(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
## Contents
|
||||
- skills/ - All installed skills and configs
|
||||
- automations/ - Custom automations (morning briefing, etc.)
|
||||
- memory/ - Long-term memory and daily notes
|
||||
- *.md - Core configuration files
|
||||
- openclaw-state/ - CRITICAL: Gateway config, cron jobs, skills metadata
|
||||
- openclaw.json - Gateway config (models, plugins, channels)
|
||||
- cron/jobs.json - All cron jobs
|
||||
- devices/paired.json - Paired devices
|
||||
- .installed-skills.json - Skill tracker for auto-recovery after updates
|
||||
|
||||
## CRITICAL: Restore Order
|
||||
1. Clone this repo
|
||||
2. Copy openclaw-state/openclaw.json to ~/.openclaw/openclaw.json
|
||||
3. Copy openclaw-state/cron/jobs.json to ~/.openclaw/cron/jobs.json
|
||||
4. Copy openclaw-state/devices/paired.json to ~/.openclaw/devices/paired.json
|
||||
5. Copy workspace files to ~/.openclaw/workspace/
|
||||
6. Restart OpenClaw gateway
|
||||
7. Run: ~/.openclaw/workspace/scripts/skill-tracker.sh restore
|
||||
(This auto-reinstalls any skills lost during updates!)
|
||||
|
||||
## CRITICAL Credentials to Save Separately (NOT in this repo)
|
||||
Store these separately in a secure password manager!
|
||||
- All API keys from Notion database
|
||||
- WhatsApp session tokens
|
||||
- Any keys in ~/.config/
|
||||
EOF
|
||||
|
||||
# Configure git
|
||||
git config user.email "krillyclaw@gmail.com"
|
||||
git config user.name "Krilly the Crab"
|
||||
|
||||
# Commit and push
|
||||
log "Committing changes..."
|
||||
cd "$REPO_DIR"
|
||||
git add -A
|
||||
git diff --cached --quiet || git commit -m "Backup: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
git push origin main || git push origin master
|
||||
git push origin main
|
||||
|
||||
log "Backup complete!"
|
||||
|
||||
# Clean up
|
||||
rm -rf "$REPO_DIR"
|
||||
|
||||
84
scripts/check-agentmail-email.sh
Executable file
84
scripts/check-agentmail-email.sh
Executable file
@@ -0,0 +1,84 @@
|
||||
#!/bin/bash
|
||||
# Check for new emails in krilly@agentmail.to and alert Anthony
|
||||
# For krillyclaw@gmail.com: requires Gmail app password from user
|
||||
|
||||
WORKSPACE_DIR="/home/openclaw/.openclaw/workspace"
|
||||
STATE_FILE="$WORKSPACE_DIR/memory/.agentmail-email-state.json"
|
||||
API_KEY="am_us_22a6a04a84144467993d5b90be8bbd5d1482ca615a4e17561682dd3d6831f932"
|
||||
|
||||
# Ensure state directory exists
|
||||
mkdir -p "$(dirname "$STATE_FILE")"
|
||||
|
||||
# Initialize state if needed
|
||||
if [ ! -f "$STATE_FILE" ]; then
|
||||
echo '{"last_message_id": "", "last_check": 0}' > "$STATE_FILE"
|
||||
fi
|
||||
|
||||
# Get last seen message ID
|
||||
LAST_MSG_ID=$(jq -r '.last_message_id // ""' "$STATE_FILE")
|
||||
|
||||
# Check for messages using agentmail API
|
||||
MESSAGES=$(AGENTMAIL_API_KEY="$API_KEY" python3 -c "
|
||||
from agentmail import AgentMail
|
||||
import json
|
||||
import os
|
||||
|
||||
client = AgentMail(api_key=os.environ.get('AGENTMAIL_API_KEY'))
|
||||
msgs = client.inboxes.messages.list(inbox_id='krilly@agentmail.to', limit=10)
|
||||
print(json.dumps([{'id': m.message_id, 'from': m.from_, 'subject': m.subject, 'labels': m.labels} for m in msgs.messages]))
|
||||
" 2>&1)
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error checking agentmail: $MESSAGES" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Parse messages
|
||||
NEW_COUNT=0
|
||||
NEWEST_ID="$LAST_MSG_ID"
|
||||
ALERT_LINES=()
|
||||
FOUND_LAST=false
|
||||
|
||||
while IFS= read -r MSG; do
|
||||
MSG_ID=$(echo "$MSG" | jq -r '.id')
|
||||
FROM=$(echo "$MSG" | jq -r '.from')
|
||||
SUBJECT=$(echo "$MSG" | jq -r '.subject')
|
||||
LABELS=$(echo "$MSG" | jq -r '.labels | join(",")')
|
||||
|
||||
# Stop if we hit the last seen message
|
||||
if [ "$MSG_ID" = "$LAST_MSG_ID" ]; then
|
||||
FOUND_LAST=true
|
||||
break
|
||||
fi
|
||||
|
||||
# Only alert for unread messages
|
||||
if echo "$LABELS" | grep -q "unread"; then
|
||||
NEW_COUNT=$((NEW_COUNT + 1))
|
||||
NEWEST_ID="$MSG_ID"
|
||||
|
||||
# Don't include our own sent messages
|
||||
if ! echo "$FROM" | grep -q "krilly@"; then
|
||||
ALERT_LINES+=("• **$SUBJECT**\n From: $FROM")
|
||||
fi
|
||||
fi
|
||||
done < <(echo "$MESSAGES" | jq -c '.[]')
|
||||
|
||||
# Update state
|
||||
jq ".last_message_id = \"$NEWEST_ID\" | .last_check = $(date +%s)" "$STATE_FILE" > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"
|
||||
|
||||
# Send alert if new messages found
|
||||
if [ "$NEW_COUNT" -gt 0 ] && [ ${#ALERT_LINES[@]} -gt 0 ]; then
|
||||
ALERT_MSG="📬 **$NEW_COUNT new email(s) in krilly@agentmail.to**:\n\n"
|
||||
for LINE in "${ALERT_LINES[@]}"; do
|
||||
ALERT_MSG+="$LINE\n\n"
|
||||
done
|
||||
ALERT_MSG+="— Krilly 🦀"
|
||||
|
||||
# Send via Telegram using gateway
|
||||
curl -s -X POST "http://127.0.0.1:18789/api/message/send" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"channel\": \"telegram\", \"to\": \"telegram:1793951355\", \"message\": $(echo "$ALERT_MSG" | jq -Rs .)}" \
|
||||
> /dev/null 2>&1
|
||||
|
||||
echo "Alert sent for $NEW_COUNT new messages"
|
||||
fi
|
||||
76
scripts/check-krillyclaw-email.sh
Executable file
76
scripts/check-krillyclaw-email.sh
Executable file
@@ -0,0 +1,76 @@
|
||||
#!/bin/bash
|
||||
# Check for new emails and alert Anthony via Telegram
|
||||
# Uses imap-smtp-email skill configuration
|
||||
|
||||
WORKSPACE_DIR="/home/openclaw/.openclaw/workspace"
|
||||
STATE_FILE="$WORKSPACE_DIR/memory/.krillyclaw-email-state.json"
|
||||
SCRIPT_DIR="$WORKSPACE_DIR/skills/imap-smtp-email"
|
||||
|
||||
# Ensure state directory exists
|
||||
mkdir -p "$(dirname "$STATE_FILE")"
|
||||
|
||||
# Initialize state if needed
|
||||
if [ ! -f "$STATE_FILE" ]; then
|
||||
echo '{"last_uid": 0, "last_check": 0}' > "$STATE_FILE"
|
||||
fi
|
||||
|
||||
# Get last seen UID
|
||||
LAST_UID=$(jq -r '.last_uid // 0' "$STATE_FILE")
|
||||
|
||||
# Check for unread emails using the imap-smtp-email skill
|
||||
cd "$SCRIPT_DIR"
|
||||
RESULT=$(NODE_TLS_REJECT_UNAUTHORIZED=0 node scripts/imap.js search --unseen --limit 20 2>&1)
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error checking email: $RESULT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Parse UIDs from result
|
||||
NEW_UIDS=$(echo "$RESULT" | jq -r '.[].uid' 2>/dev/null | sort -n)
|
||||
|
||||
if [ -z "$NEW_UIDS" ]; then
|
||||
# No unread messages - update check time only
|
||||
jq ".last_check = $(date +%s)" "$STATE_FILE" > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Find truly new messages
|
||||
NEW_COUNT=0
|
||||
NEWEST_UID=$LAST_UID
|
||||
ALERT_LINES=()
|
||||
|
||||
while IFS= read -r MSG_UID; do
|
||||
if [ "$MSG_UID" -gt "$LAST_UID" ]; then
|
||||
NEW_COUNT=$((NEW_COUNT + 1))
|
||||
if [ "$MSG_UID" -gt "$NEWEST_UID" ]; then
|
||||
NEWEST_UID=$MSG_UID
|
||||
fi
|
||||
|
||||
# Get message details
|
||||
MSG=$(node scripts/imap.js fetch "$MSG_UID" 2>&1)
|
||||
SUBJECT=$(echo "$MSG" | jq -r '.subject // "No subject"' 2>/dev/null)
|
||||
FROM=$(echo "$MSG" | jq -r '.from // "Unknown sender"' 2>/dev/null)
|
||||
ALERT_LINES+=("• **$SUBJECT**\n From: $FROM")
|
||||
fi
|
||||
done <<< "$NEW_UIDS"
|
||||
|
||||
# Update state
|
||||
jq ".last_uid = $NEWEST_UID | .last_check = $(date +%s)" "$STATE_FILE" > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"
|
||||
|
||||
# Send alert if new messages found
|
||||
if [ "$NEW_COUNT" -gt 0 ]; then
|
||||
ALERT_MSG="📬 **$NEW_COUNT new email(s) in inbox**:\n\n"
|
||||
for LINE in "${ALERT_LINES[@]}"; do
|
||||
ALERT_MSG+="$LINE\n\n"
|
||||
done
|
||||
ALERT_MSG+="— Krilly 🦀"
|
||||
|
||||
# Send via Telegram using gateway
|
||||
curl -s -X POST "http://127.0.0.1:18789/api/message/send" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"channel\": \"telegram\", \"to\": \"telegram:1793951355\", \"message\": $(echo "$ALERT_MSG" | jq -Rs .)}" \
|
||||
> /dev/null 2>&1
|
||||
|
||||
echo "Alert sent for $NEW_COUNT new messages"
|
||||
fi
|
||||
14
scripts/checkpoint-memory.sh
Executable file
14
scripts/checkpoint-memory.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
# Memory Checkpoint - Called by agent to persist critical context
|
||||
# Usage: checkpoint-memory.sh "Context description"
|
||||
|
||||
MEMORY_DIR="$HOME/.openclaw/workspace/memory"
|
||||
TODAY=$(date +%Y-%m-%d)
|
||||
TIME=$(date +%H:%M)
|
||||
|
||||
echo "" >> "$MEMORY_DIR/${TODAY}-checkpoints.md"
|
||||
echo "## Checkpoint: $TIME" >> "$MEMORY_DIR/${TODAY}-checkpoints.md"
|
||||
echo "" >> "$MEMORY_DIR/${TODAY}-checkpoints.md"
|
||||
echo "$1" >> "$MEMORY_DIR/${TODAY}-checkpoints.md"
|
||||
echo "" >> "$MEMORY_DIR/${TODAY}-checkpoints.md"
|
||||
echo "---" >> "$MEMORY_DIR/${TODAY}-checkpoints.md"
|
||||
111
scripts/claude-sub-proxy.js
Normal file
111
scripts/claude-sub-proxy.js
Normal file
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env node
|
||||
// claude-sub-proxy.js — OpenAI-compatible proxy routing through Claude Code CLI (Pro subscription)
|
||||
// Accepts OpenAI chat completions format, routes through claude CLI, returns OpenAI format
|
||||
// Usage: CLAUDE_CODE_OAUTH_TOKEN=<token> node claude-sub-proxy.js
|
||||
|
||||
import { createServer } from 'http';
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
const PORT = 8782;
|
||||
const CLAUDE_BIN = '/home/openclaw/.npm-global/bin/claude';
|
||||
|
||||
// Build a plain-text prompt from OpenAI messages array
|
||||
function buildPrompt(messages, system) {
|
||||
const parts = [];
|
||||
if (system) parts.push(`[SYSTEM]\n${system}\n[/SYSTEM]`);
|
||||
for (const msg of messages) {
|
||||
const role = msg.role === 'assistant' ? 'Assistant' : 'Human';
|
||||
const content = Array.isArray(msg.content)
|
||||
? msg.content.map(b => typeof b === 'string' ? b : (b.text || '')).join('')
|
||||
: String(msg.content || '');
|
||||
parts.push(`${role}: ${content}`);
|
||||
}
|
||||
return parts.join('\n\n');
|
||||
}
|
||||
|
||||
// Strip provider prefix e.g. "sub-claude/claude-sonnet-4-5" → "claude-sonnet-4-5"
|
||||
function cleanModel(model) {
|
||||
return (model || 'claude-sonnet-4-5').replace(/^[^/]+\//, '');
|
||||
}
|
||||
|
||||
function runClaude(prompt, model) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const env = { ...process.env };
|
||||
delete env.ANTHROPIC_API_KEY; // force CLAUDE_CODE_OAUTH_TOKEN auth
|
||||
const child = spawn(CLAUDE_BIN, [
|
||||
'-p', prompt,
|
||||
'--output-format', 'json',
|
||||
'--model', model,
|
||||
'--dangerously-skip-permissions',
|
||||
'--no-session-persistence',
|
||||
], { env, timeout: 120000, stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
|
||||
let stdout = '', stderr = '';
|
||||
child.stdout.on('data', d => stdout += d);
|
||||
child.stderr.on('data', d => stderr += d);
|
||||
child.on('close', code => {
|
||||
if (code !== 0) return reject(new Error(`claude exit ${code}: ${stderr.slice(0, 300)}`));
|
||||
if (!stdout.trim()) return reject(new Error(`empty output. stderr: ${stderr.slice(0, 200)}`));
|
||||
try { resolve(JSON.parse(stdout)); }
|
||||
catch { reject(new Error(`bad JSON: ${stdout.slice(0, 200)}`)); }
|
||||
});
|
||||
child.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
const server = createServer(async (req, res) => {
|
||||
// Health check
|
||||
if (req.method === 'GET' && req.url === '/health') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: true, proxy: 'claude-sub-proxy' }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method !== 'POST') { res.writeHead(405); res.end(); return; }
|
||||
|
||||
let body = '';
|
||||
req.on('data', d => body += d);
|
||||
req.on('end', async () => {
|
||||
try {
|
||||
const parsed = JSON.parse(body);
|
||||
const model = cleanModel(parsed.model);
|
||||
const messages = parsed.messages || [];
|
||||
const system = typeof parsed.system === 'string' ? parsed.system : undefined;
|
||||
const prompt = buildPrompt(messages, system);
|
||||
console.log(`[${new Date().toISOString()}] → ${model} (${prompt.length} chars)`);
|
||||
|
||||
const result = await runClaude(prompt, model);
|
||||
if (result.is_error) throw new Error(result.result || 'Claude error');
|
||||
|
||||
console.log(`[${new Date().toISOString()}] ✅ cost=$${result.total_cost_usd?.toFixed(5) || '?'}`);
|
||||
|
||||
// Return OpenAI chat completions format
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
id: `chatcmpl_proxy_${Date.now()}`,
|
||||
object: 'chat.completion',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: parsed.model || model,
|
||||
choices: [{
|
||||
index: 0,
|
||||
message: { role: 'assistant', content: result.result },
|
||||
finish_reason: 'stop',
|
||||
}],
|
||||
usage: {
|
||||
prompt_tokens: result.usage?.input_tokens || 0,
|
||||
completion_tokens: result.usage?.output_tokens || 0,
|
||||
total_tokens: (result.usage?.input_tokens || 0) + (result.usage?.output_tokens || 0),
|
||||
},
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error(`[${new Date().toISOString()}] ❌`, err.message);
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: { message: err.message, type: 'proxy_error' } }));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(PORT, '127.0.0.1', () => {
|
||||
console.log(`✅ claude-sub-proxy on http://127.0.0.1:${PORT} (OpenAI-compatible)`);
|
||||
console.log(` Routes → Claude Code CLI (${CLAUDE_BIN}) using Pro subscription`);
|
||||
});
|
||||
18
scripts/daily-cost-check.sh
Executable file
18
scripts/daily-cost-check.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
# Daily AI cost budget check — alerts via Telegram if over $5/day
|
||||
# Uses openclaw-cost-guard skill
|
||||
|
||||
SCRIPT="/home/openclaw/.openclaw/workspace/skills/openclaw-cost-guard/scripts/extract_cost.py"
|
||||
BUDGET="${DAILY_BUDGET_USD:-5.00}"
|
||||
|
||||
python3 "$SCRIPT" --today --budget-usd "$BUDGET" --budget-mode exit
|
||||
EXIT_CODE=$?
|
||||
|
||||
if [ $EXIT_CODE -eq 2 ]; then
|
||||
# Budget breached — get the actual total for the alert message
|
||||
TOTAL=$(python3 "$SCRIPT" --today --json 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print(f\"\${d['total']['cost']:.2f}\")" 2>/dev/null || echo "unknown")
|
||||
echo "BUDGET_BREACHED: $TOTAL today vs \$$BUDGET limit"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
exit 0
|
||||
25
scripts/gateway-state-check.sh
Normal file
25
scripts/gateway-state-check.sh
Normal file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
# Gateway State Checker - runs periodically to detect if gateway is down
|
||||
|
||||
STATE_FILE="/home/openclaw/.openclaw/gateway-status.state"
|
||||
LOG_FILE="/home/openclaw/.openclaw/logs/gateway-state.log"
|
||||
|
||||
# Check if gateway is running
|
||||
if systemctl --user is-active --quiet openclaw-gateway.service; then
|
||||
# Gateway is up
|
||||
if [ -f "$STATE_FILE" ]; then
|
||||
if [ "$(cat "$STATE_FILE")" != "up" ]; then
|
||||
# State changed from down to up, but we'll handle that in the start script
|
||||
:
|
||||
fi
|
||||
fi
|
||||
# Ensure state is up
|
||||
echo "up" > "$STATE_FILE"
|
||||
else
|
||||
# Gateway is down
|
||||
if [ ! -f "$STATE_FILE" ] || [ "$(cat "$STATE_FILE")" != "down" ]; then
|
||||
# First time detecting it's down - update state
|
||||
echo "down" > "$STATE_FILE"
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') - Gateway is down (state changed to down)" >> "$LOG_FILE"
|
||||
fi
|
||||
fi
|
||||
3
scripts/gateway-telegram-notify.sh
Executable file
3
scripts/gateway-telegram-notify.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
# Gateway startup notification disabled
|
||||
exit 0
|
||||
10
scripts/gig-oauth/client_secret.json
Normal file
10
scripts/gig-oauth/client_secret.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"web": {
|
||||
"client_id": "953576255555-4ch6d035uvp9rnuim5php7jjc57kes0j.apps.googleusercontent.com",
|
||||
"client_secret": "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",
|
||||
"redirect_uris": [
|
||||
"http://127.0.0.1:8765/oauth2/callback",
|
||||
"http://localhost:8765/oauth2/callback"
|
||||
]
|
||||
}
|
||||
}
|
||||
10
scripts/gig-oauth/client_secret_desktop.json
Normal file
10
scripts/gig-oauth/client_secret_desktop.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"installed": {
|
||||
"client_id": "953576255555-h8kbcht83hsmscc1c350654dc6ejpms8.apps.googleusercontent.com",
|
||||
"client_secret": "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",
|
||||
"redirect_uris": [
|
||||
"http://localhost",
|
||||
"http://127.0.0.1"
|
||||
]
|
||||
}
|
||||
}
|
||||
40
scripts/heartbeat-model-guard.sh
Executable file
40
scripts/heartbeat-model-guard.sh
Executable file
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
# Heartbeat Model Guard - Ensures heartbeat stays on free models
|
||||
|
||||
CONFIG_FILE="/home/openclaw/.openclaw/openclaw.json"
|
||||
FREE_MODEL="kilocode/moonshotai/kimi-k2.5:free"
|
||||
FALLBACK_MODEL="kilocode/minimax/minimax-m2.5:free"
|
||||
|
||||
# Check if jq is available
|
||||
if ! command -v jq &> /dev/null; then
|
||||
echo "ERROR: jq not installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get current heartbeat model
|
||||
CURRENT_MODEL=$(jq -r '.agents.defaults.heartbeat.model // empty' "$CONFIG_FILE" 2>/dev/null)
|
||||
|
||||
if [ -z "$CURRENT_MODEL" ]; then
|
||||
echo "ERROR: Could not read heartbeat model from config"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if current model is a free kilocode model
|
||||
if [[ "$CURRENT_MODEL" == *":free"* ]] || [[ "$CURRENT_MODEL" == *"/free"* ]]; then
|
||||
echo "OK: heartbeat model already correct ($CURRENT_MODEL)."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Model is not free - need to fix
|
||||
echo "FIXING: heartbeat model is '$CURRENT_MODEL' - changing to free model..."
|
||||
|
||||
# Update to free model
|
||||
jq --arg model "$FREE_MODEL" '.agents.defaults.heartbeat.model = $model' "$CONFIG_FILE" > "${CONFIG_FILE}.tmp" && mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE"
|
||||
|
||||
# Restart gateway
|
||||
if systemctl --user restart openclaw-gateway 2>/dev/null || sudo systemctl restart openclaw-gateway 2>/dev/null; then
|
||||
echo "FIXED: heartbeat model corrected to $FREE_MODEL and gateway restarted."
|
||||
else
|
||||
echo "ERROR: Updated config but failed to restart gateway."
|
||||
exit 1
|
||||
fi
|
||||
219
scripts/imap-idle-monitor.js
Normal file
219
scripts/imap-idle-monitor.js
Normal file
@@ -0,0 +1,219 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* IMAP IDLE Monitor for krillyclaw@gmail.com
|
||||
* Uses IMAP IDLE (RFC 2177) for real-time push notifications
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const https = require('https');
|
||||
|
||||
// Load imap modules from skill directory
|
||||
const Imap = require(path.resolve(__dirname, '../skills/imap-smtp-email/node_modules/imap'));
|
||||
const { simpleParser } = require(path.resolve(__dirname, '../skills/imap-smtp-email/node_modules/mailparser'));
|
||||
|
||||
require('dotenv').config({ path: path.resolve(__dirname, '../skills/imap-smtp-email/.env.krillyclaw') });
|
||||
|
||||
const STATE_FILE = path.resolve(__dirname, '../memory/.krillyclaw-imap-state.json');
|
||||
const GATEWAY_URL = 'http://127.0.0.1:18789/api/message/send';
|
||||
|
||||
let lastUid = 0;
|
||||
|
||||
function loadState() {
|
||||
try {
|
||||
const fs = require('fs');
|
||||
if (fs.existsSync(STATE_FILE)) {
|
||||
const state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
|
||||
lastUid = state.last_uid || 0;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error loading state:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function saveState() {
|
||||
try {
|
||||
const fs = require('fs');
|
||||
fs.writeFileSync(STATE_FILE, JSON.stringify({ last_uid: lastUid, last_check: Date.now() }));
|
||||
} catch (e) {
|
||||
console.error('Error saving state:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function sendAlert(subject, from) {
|
||||
const message = `📬 **New email in krillyclaw@gmail.com**:\n\n• **${subject}**\n From: ${from}\n\n— Krilly 🦀`;
|
||||
|
||||
const data = JSON.stringify({
|
||||
channel: 'telegram',
|
||||
to: 'telegram:1793951355',
|
||||
message: message
|
||||
});
|
||||
|
||||
const http = require('http');
|
||||
const req = http.request(GATEWAY_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}, (res) => {
|
||||
console.log('Alert sent:', res.statusCode);
|
||||
});
|
||||
|
||||
req.on('error', (e) => console.error('Alert failed:', e.message));
|
||||
req.write(data);
|
||||
req.end();
|
||||
}
|
||||
|
||||
function createImap() {
|
||||
return new Imap({
|
||||
user: process.env.IMAP_USER,
|
||||
password: process.env.IMAP_PASS,
|
||||
host: process.env.IMAP_HOST,
|
||||
port: parseInt(process.env.IMAP_PORT),
|
||||
tls: process.env.IMAP_TLS === 'true',
|
||||
tlsOptions: { rejectUnauthorized: false },
|
||||
connTimeout: 60000,
|
||||
authTimeout: 10000
|
||||
});
|
||||
}
|
||||
|
||||
function fetchNewMessages(imap, box) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const searchCriteria = ['UNSEEN'];
|
||||
const fetchOptions = { bodies: ['HEADER.FIELDS (FROM SUBJECT)'], struct: true };
|
||||
|
||||
imap.search(searchCriteria, (err, results) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!results || results.length === 0) {
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetch = imap.fetch(results, fetchOptions);
|
||||
const messages = [];
|
||||
|
||||
fetch.on('message', (msg, seqno) => {
|
||||
let header = {};
|
||||
let uid = 0;
|
||||
|
||||
msg.on('body', (stream) => {
|
||||
let buffer = '';
|
||||
stream.on('data', (chunk) => buffer += chunk);
|
||||
stream.on('end', () => {
|
||||
header = Imap.parseHeader(buffer);
|
||||
});
|
||||
});
|
||||
|
||||
msg.once('attributes', (attrs) => {
|
||||
uid = attrs.uid;
|
||||
});
|
||||
|
||||
msg.once('end', () => {
|
||||
messages.push({
|
||||
uid,
|
||||
subject: header.subject ? header.subject[0] : 'No subject',
|
||||
from: header.from ? header.from[0] : 'Unknown'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
fetch.once('error', reject);
|
||||
fetch.once('end', () => resolve(messages));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function monitor() {
|
||||
loadState();
|
||||
console.log(`[${new Date().toISOString()}] Starting IMAP IDLE monitor for ${process.env.IMAP_USER}`);
|
||||
console.log(`[${new Date().toISOString()}] Last seen UID: ${lastUid}`);
|
||||
|
||||
const imap = createImap();
|
||||
|
||||
imap.once('ready', () => {
|
||||
imap.openBox('INBOX', false, async (err, box) => {
|
||||
if (err) {
|
||||
console.error('Error opening inbox:', err);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[${new Date().toISOString()}] Connected to INBOX, watching for new emails...`);
|
||||
|
||||
// Fetch initial unread messages
|
||||
try {
|
||||
const messages = await fetchNewMessages(imap, box);
|
||||
let newCount = 0;
|
||||
|
||||
for (const msg of messages) {
|
||||
if (msg.uid > lastUid) {
|
||||
newCount++;
|
||||
lastUid = msg.uid;
|
||||
console.log(`[${new Date().toISOString()}] New message: ${msg.subject} (UID: ${msg.uid})`);
|
||||
sendAlert(msg.subject, msg.from);
|
||||
}
|
||||
}
|
||||
|
||||
if (newCount > 0) {
|
||||
saveState();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error fetching messages:', e);
|
||||
}
|
||||
|
||||
// Set up IDLE mode
|
||||
imap.on('mail', async (numNewMsgs) => {
|
||||
console.log(`[${new Date().toISOString()}] ${numNewMsgs} new message(s) received`);
|
||||
|
||||
try {
|
||||
const messages = await fetchNewMessages(imap, box);
|
||||
|
||||
for (const msg of messages) {
|
||||
if (msg.uid > lastUid) {
|
||||
lastUid = msg.uid;
|
||||
console.log(`[${new Date().toISOString()}] New message: ${msg.subject} (UID: ${msg.uid})`);
|
||||
sendAlert(msg.subject, msg.from);
|
||||
saveState();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error fetching new messages:', e);
|
||||
}
|
||||
});
|
||||
|
||||
imap.on('update', (seqno, info) => {
|
||||
console.log(`[${new Date().toISOString()}] Update: seqno=${seqno}`);
|
||||
});
|
||||
|
||||
imap.on('expunge', (seqno) => {
|
||||
console.log(`[${new Date().toISOString()}] Expunge: seqno=${seqno}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
imap.once('error', (err) => {
|
||||
console.error(`[${new Date().toISOString()}] IMAP error:`, err.message);
|
||||
// Reconnect after 30 seconds
|
||||
setTimeout(monitor, 30000);
|
||||
});
|
||||
|
||||
imap.once('end', () => {
|
||||
console.log(`[${new Date().toISOString()}] Connection ended, reconnecting in 30s...`);
|
||||
setTimeout(monitor, 30000);
|
||||
});
|
||||
|
||||
imap.connect();
|
||||
}
|
||||
|
||||
// Handle graceful shutdown
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\nShutting down...');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('\nShutting down...');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
monitor();
|
||||
1
scripts/krillyclaw-email-check.sh
Symbolic link
1
scripts/krillyclaw-email-check.sh
Symbolic link
@@ -0,0 +1 @@
|
||||
/home/openclaw/.openclaw/workspace/scripts/check-krillyclaw-email.sh
|
||||
50
scripts/log-error.sh
Executable file
50
scripts/log-error.sh
Executable file
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
WS="/home/openclaw/.openclaw/workspace"
|
||||
OUT="$WS/.learnings/ERRORS.md"
|
||||
|
||||
summary="${1:-Unspecified error}"
|
||||
err_msg="${2:-No error text provided}"
|
||||
context="${3:-No context provided}"
|
||||
priority="${4:-medium}"
|
||||
area="${5:-infra}"
|
||||
|
||||
stamp="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
||||
date_id="$(date -u +"%Y%m%d")"
|
||||
seq=$(grep -o "ERR-${date_id}-[0-9][0-9][0-9]" "$OUT" 2>/dev/null | tail -n1 | awk -F- '{print $3}' || true)
|
||||
if [ -z "${seq:-}" ]; then seq=0; fi
|
||||
seq=$(printf "%03d" $((10#$seq + 1)))
|
||||
id="ERR-${date_id}-${seq}"
|
||||
|
||||
cat >> "$OUT" <<EOF
|
||||
|
||||
## [${id}] quick-log
|
||||
|
||||
**Logged**: ${stamp}
|
||||
**Priority**: ${priority}
|
||||
**Status**: pending
|
||||
**Area**: ${area}
|
||||
|
||||
### Summary
|
||||
${summary}
|
||||
|
||||
### Error
|
||||
\`\`\`
|
||||
${err_msg}
|
||||
\`\`\`
|
||||
|
||||
### Context
|
||||
${context}
|
||||
|
||||
### Suggested Fix
|
||||
Triage and resolve; update this entry when fixed.
|
||||
|
||||
### Metadata
|
||||
- Reproducible: unknown
|
||||
- Related Files: n/a
|
||||
|
||||
---
|
||||
EOF
|
||||
|
||||
echo "Logged ${id} to ${OUT}"
|
||||
27
scripts/memory-lancedb-health-check.sh
Executable file
27
scripts/memory-lancedb-health-check.sh
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
# Memory LanceDB Health Check & Auto-Fix
|
||||
# Runs periodically to ensure memory-lancedb extension has its dependencies
|
||||
|
||||
set -e
|
||||
|
||||
OPENCLAW_ROOT="$HOME/.npm-global/lib/node_modules/openclaw"
|
||||
LANCE_DB_PATH="$OPENCLAW_ROOT/node_modules/@lancedb/lancedb"
|
||||
|
||||
# Check if LanceDB is installed at root level
|
||||
if [ ! -d "$LANCE_DB_PATH" ]; then
|
||||
echo "[$(date)] LanceDB missing at root level. Installing..."
|
||||
cd "$OPENCLAW_ROOT"
|
||||
npm install @lancedb/lancedb --silent 2>/dev/null || npm install @lancedb/lancedb
|
||||
echo "[$(date)] LanceDB installed successfully"
|
||||
else
|
||||
echo "[$(date)] LanceDB check: OK"
|
||||
fi
|
||||
|
||||
# Also verify extension-level install
|
||||
EXT_PATH="$OPENCLAW_ROOT/extensions/memory-lancedb"
|
||||
if [ -d "$EXT_PATH" ] && [ ! -d "$EXT_PATH/node_modules/@lancedb/lancedb" ]; then
|
||||
echo "[$(date)] LanceDB missing in extension. Installing..."
|
||||
cd "$EXT_PATH"
|
||||
npm install --silent 2>/dev/null || npm install
|
||||
echo "[$(date)] Extension dependencies installed"
|
||||
fi
|
||||
44
scripts/notify-gateway-restart.sh
Executable file
44
scripts/notify-gateway-restart.sh
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
# Polls the OpenClaw gateway health endpoint.
|
||||
# Sends a Telegram notification whenever it recovers after being unreachable.
|
||||
|
||||
BOT_TOKEN="8598508497:AAHmTMbnR7un2ADtmsjJr8moQkDOU9ILBps"
|
||||
CHAT_ID="1793951355"
|
||||
GATEWAY_URL="http://127.0.0.1:18789"
|
||||
POLL_INTERVAL=3 # seconds between checks
|
||||
LOG_PREFIX="[$(date '+%Y-%m-%d %H:%M:%S')]"
|
||||
|
||||
send_notification() {
|
||||
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"chat_id\": \"${CHAT_ID}\",
|
||||
\"text\": \"✅ *Krilly is back online!*\n\nOpenClaw gateway restarted and is ready to go 🦀\",
|
||||
\"parse_mode\": \"Markdown\"
|
||||
}" > /dev/null
|
||||
}
|
||||
|
||||
is_up() {
|
||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" --max-time 3 "${GATEWAY_URL}/health" 2>/dev/null)
|
||||
[ "$STATUS" = "200" ] || [ "$STATUS" = "401" ]
|
||||
}
|
||||
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') Gateway watcher started"
|
||||
|
||||
WAS_UP=true
|
||||
|
||||
while true; do
|
||||
if is_up; then
|
||||
if [ "$WAS_UP" = "false" ]; then
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') Gateway recovered — sending notification"
|
||||
send_notification
|
||||
fi
|
||||
WAS_UP=true
|
||||
else
|
||||
if [ "$WAS_UP" = "true" ]; then
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') Gateway went down"
|
||||
fi
|
||||
WAS_UP=false
|
||||
fi
|
||||
sleep "$POLL_INTERVAL"
|
||||
done
|
||||
50
scripts/openclaw-news-research.sh
Executable file
50
scripts/openclaw-news-research.sh
Executable file
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
# OpenClaw News Research Script
|
||||
# Searches GitHub, Reddit for OpenClaw news
|
||||
|
||||
echo "🔍 Researching OpenClaw news..." >&2
|
||||
|
||||
RESULTS=""
|
||||
|
||||
# 1. Check OpenClaw GitHub commits
|
||||
echo "🐙 Checking GitHub..." >&2
|
||||
GITHUB=$(curl -s "https://api.github.com/repos/openclaw/openclaw/commits?per_page=10" 2>/dev/null | jq -r '.[] | "- \(.commit.message | split("\n")[0]): https://github.com/openclaw/openclaw/commit/\(.sha)"' 2>/dev/null | head -5)
|
||||
|
||||
if [ -n "$GITHUB" ]; then
|
||||
RESULTS+="### 🐙 OpenClaw GitHub Commits\n$GITHUB\n\n"
|
||||
fi
|
||||
|
||||
# 2. Check OpenClaw GitHub issues
|
||||
echo "📋 Checking GitHub Issues..." >&2
|
||||
ISSUES=$(curl -s "https://api.github.com/repos/openclaw/openclaw/issues?state=open&per_page=5" 2>/dev/null | jq -r '.[] | "- \(.title): https://github.com/openclaw/openclaw/issues/\(.number)"' 2>/dev/null | head -5)
|
||||
|
||||
if [ -n "$ISSUES" ]; then
|
||||
RESULTS+="### 📋 Open GitHub Issues\n$ISSUES\n\n"
|
||||
fi
|
||||
|
||||
# 3. Check OpenClaw Discord/Community
|
||||
echo "💬 Checking for community news..." >&2
|
||||
|
||||
# 4. General AI assistant news
|
||||
echo "🌐 Checking web..." >&2
|
||||
WEB=$(web_search --query "openclaw AI assistant personal agent 2026" --count 3 2>/dev/null | jq -r '.results[] | "- \(.title): \(.url)"' 2>/dev/null | head -3)
|
||||
|
||||
if [ -n "$WEB" ]; then
|
||||
RESULTS+="### 🌐 Related News\n$WEB\n\n"
|
||||
fi
|
||||
|
||||
if [ -z "$RESULTS" ]; then
|
||||
RESULTS="No new OpenClaw news found."
|
||||
fi
|
||||
|
||||
# Format
|
||||
EMAIL_BODY="🤖 OpenClaw News Digest
|
||||
📅 $(date '+%Y-%m-%d')
|
||||
|
||||
$RESULTS
|
||||
|
||||
---
|
||||
🧠 Sent by Krilly 🦀"
|
||||
|
||||
echo "$EMAIL_BODY" > /tmp/openclaw-news-digest.txt
|
||||
echo "✅ Done" >&2
|
||||
19
scripts/piper-tts.sh
Executable file
19
scripts/piper-tts.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
# Piper TTS wrapper for OpenClaw
|
||||
# Usage: piper-tts.sh "text to speak" [output_file]
|
||||
|
||||
TEXT="${1:-Hello}"
|
||||
OUTPUT="${2:-/tmp/piper_output.wav}"
|
||||
MODEL="${PIPER_MODEL:-$HOME/.config/piper/voices/en_US-ryan-medium.onnx}"
|
||||
|
||||
# Generate speech
|
||||
echo "$TEXT" | $HOME/.local/bin/piper --model "$MODEL" --output_file "$OUTPUT" 2>/dev/null
|
||||
|
||||
# Check if file was created
|
||||
if [ -f "$OUTPUT" ]; then
|
||||
echo "$OUTPUT"
|
||||
exit 0
|
||||
else
|
||||
echo "Error: TTS generation failed" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -23,21 +23,29 @@ else
|
||||
cd "$REPO_DIR"
|
||||
fi
|
||||
|
||||
# Restore state directory
|
||||
if [ -d "$REPO_DIR/state" ]; then
|
||||
# Restore state directory (from openclaw-state/)
|
||||
if [ -d "$REPO_DIR/openclaw-state" ]; then
|
||||
echo "Restoring state directory..."
|
||||
rsync -av "$REPO_DIR/state/" "$STATE_DIR/"
|
||||
mkdir -p "$STATE_DIR/cron" "$STATE_DIR/devices"
|
||||
[ -f "$REPO_DIR/openclaw-state/openclaw.json" ] && cp "$REPO_DIR/openclaw-state/openclaw.json" "$STATE_DIR/"
|
||||
[ -f "$REPO_DIR/openclaw-state/cron/jobs.json" ] && cp "$REPO_DIR/openclaw-state/cron/jobs.json" "$STATE_DIR/cron/"
|
||||
[ -f "$REPO_DIR/openclaw-state/devices/paired.json" ] && cp "$REPO_DIR/openclaw-state/devices/paired.json" "$STATE_DIR/devices/"
|
||||
else
|
||||
echo "Warning: No state directory in backup"
|
||||
echo "Warning: No openclaw-state directory in backup"
|
||||
fi
|
||||
|
||||
# Restore workspace (optional - may not want to overwrite)
|
||||
# Restore workspace
|
||||
echo ""
|
||||
echo "Workspace restore is optional. Current workspace may have newer files."
|
||||
read -p "Restore workspace too? (y/N): " answer
|
||||
if [ "$answer" = "y" ] || [ "$answer" = "Y" ]; then
|
||||
echo "Restoring workspace..."
|
||||
rsync -av "$REPO_DIR/workspace/" "$WORKSPACE_DIR/"
|
||||
echo "Restoring workspace..."
|
||||
rsync -av --exclude='.git' "$REPO_DIR/" "$WORKSPACE_DIR/"
|
||||
|
||||
# Restore missing skills
|
||||
echo ""
|
||||
echo "Checking for missing skills..."
|
||||
if [ -f "$WORKSPACE_DIR/scripts/skill-tracker.sh" ]; then
|
||||
"$WORKSPACE_DIR/scripts/skill-tracker.sh" restore
|
||||
else
|
||||
echo "Warning: Skill tracker not found - skills may need manual reinstallation"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
28
scripts/searxng-search.sh
Executable file
28
scripts/searxng-search.sh
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
# SearXNG search wrapper
|
||||
# Usage: searxng-search.sh "query" [num_results]
|
||||
|
||||
SEARXNG_URL="http://docker.kangaroo-eel.ts.net:8010"
|
||||
QUERY="${1:-}"
|
||||
LIMIT="${2:-10}"
|
||||
|
||||
if [ -z "$QUERY" ]; then
|
||||
echo "Usage: searxng-search.sh \"query\" [num_results]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ENCODED=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$QUERY'))")
|
||||
|
||||
curl -s "${SEARXNG_URL}/search?q=${ENCODED}&format=json&pageno=1" | python3 -c "
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
results = data.get('results', [])[:${LIMIT}]
|
||||
for i, r in enumerate(results, 1):
|
||||
print(f\"{i}. {r.get('title','')}\")
|
||||
print(f\" {r.get('url','')}\")
|
||||
snippet = r.get('content','').strip().replace('\n',' ')
|
||||
if snippet:
|
||||
print(f\" {snippet[:200]}\")
|
||||
print()
|
||||
print(f\"Total results: {len(data.get('results', []))}\")
|
||||
"
|
||||
48
scripts/serve-memory-viewer.js
Executable file
48
scripts/serve-memory-viewer.js
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env node
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const PORT = process.env.PORT || 5173;
|
||||
const DIST_DIR = path.join(__dirname, '../memory-viewer/dist');
|
||||
|
||||
const mimeTypes = {
|
||||
'.html': 'text/html',
|
||||
'.js': 'text/javascript',
|
||||
'.css': 'text/css',
|
||||
'.json': 'application/json',
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpg',
|
||||
'.gif': 'image/gif',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.ico': 'image/x-icon',
|
||||
'.woff': 'font/woff',
|
||||
'.woff2': 'font/woff2',
|
||||
};
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
let filePath = path.join(DIST_DIR, req.url === '/' ? 'index.html' : req.url);
|
||||
|
||||
// Handle SPA routing - return index.html for non-file requests
|
||||
if (!fs.existsSync(filePath) && !req.url.startsWith('/api')) {
|
||||
filePath = path.join(DIST_DIR, 'index.html');
|
||||
}
|
||||
|
||||
const ext = path.extname(filePath);
|
||||
const contentType = mimeTypes[ext] || 'application/octet-stream';
|
||||
|
||||
fs.readFile(filePath, (err, content) => {
|
||||
if (err) {
|
||||
res.writeHead(404);
|
||||
res.end('Not found');
|
||||
return;
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': contentType });
|
||||
res.end(content);
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`Memory Viewer frontend serving on http://0.0.0.0:${PORT}`);
|
||||
console.log(`Dist folder: ${DIST_DIR}`);
|
||||
});
|
||||
36
scripts/session-backup.sh
Executable file
36
scripts/session-backup.sh
Executable file
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
# Session Backup Cron - Runs every 15 minutes
|
||||
# Backs up active session files to workspace for Gitea backup
|
||||
|
||||
set -e
|
||||
|
||||
SESSION_DIR="$HOME/.openclaw/agents/main/sessions"
|
||||
BACKUP_DIR="$HOME/.openclaw/workspace/sessions"
|
||||
LOG_FILE="$HOME/.openclaw/workspace/logs/session-backup.log"
|
||||
|
||||
# Ensure directories exist
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
mkdir -p "$(dirname "$LOG_FILE")"
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
# Find the most recent active session
|
||||
LATEST_SESSION=$(ls -t "$SESSION_DIR"/*.jsonl 2>/dev/null | grep -v ".reset." | head -1)
|
||||
|
||||
if [[ -z "$LATEST_SESSION" ]]; then
|
||||
log "No active session found"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Copy to backup with timestamp
|
||||
SESSION_NAME=$(basename "$LATEST_SESSION")
|
||||
BACKUP_NAME="${SESSION_NAME%.jsonl}-$(date +%H%M).jsonl"
|
||||
|
||||
cp "$LATEST_SESSION" "$BACKUP_DIR/$BACKUP_NAME"
|
||||
|
||||
# Keep only last 20 backups to save space
|
||||
ls -t "$BACKUP_DIR"/*.jsonl 2>/dev/null | tail -n +21 | xargs -r rm -f
|
||||
|
||||
log "Backed up: $SESSION_NAME -> $BACKUP_NAME ($(stat -c%s "$LATEST_SESSION" | numfmt --to=iec))"
|
||||
130
scripts/skill-tracker.sh
Executable file
130
scripts/skill-tracker.sh
Executable file
@@ -0,0 +1,130 @@
|
||||
#!/bin/bash
|
||||
# Skill Tracker - Tracks installed ClawHub skills for auto-recovery after updates
|
||||
# Location: ~/.openclaw/workspace/scripts/skill-tracker.sh
|
||||
|
||||
STATE_SKILLS_DIR="$HOME/.openclaw/skills"
|
||||
WORKSPACE_SKILLS_DIR="$HOME/.openclaw/workspace/skills"
|
||||
TRACKER_FILE="$HOME/.openclaw/workspace/.installed-skills.json"
|
||||
LOG_FILE="$HOME/.openclaw/workspace/logs/skill-tracker.log"
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
# Ensure log directory exists
|
||||
mkdir -p "$(dirname "$LOG_FILE")"
|
||||
|
||||
# Get list of installed skills from state directory
|
||||
get_state_skills() {
|
||||
if [ -d "$STATE_SKILLS_DIR" ]; then
|
||||
# List directories and symlinks (skip .json metadata files)
|
||||
find "$STATE_SKILLS_DIR" -maxdepth 1 -mindepth 1 \( -type d -o -type l \) -exec basename {} \; 2>/dev/null | sort
|
||||
fi
|
||||
}
|
||||
|
||||
# Get list of workspace skills
|
||||
get_workspace_skills() {
|
||||
if [ -d "$WORKSPACE_SKILLS_DIR" ]; then
|
||||
find "$WORKSPACE_SKILLS_DIR" -maxdepth 1 -mindepth 1 -type d -exec basename {} \; 2>/dev/null | sort
|
||||
fi
|
||||
}
|
||||
|
||||
# Save current state to tracker file
|
||||
save_state() {
|
||||
local state_skills=$(get_state_skills | jq -R . | jq -s .)
|
||||
local workspace_skills=$(get_workspace_skills | jq -R . | jq -s .)
|
||||
|
||||
cat > "$TRACKER_FILE" <<EOF
|
||||
{
|
||||
"last_updated": "$(date -Iseconds)",
|
||||
"state_skills": $state_skills,
|
||||
"workspace_skills": $workspace_skills,
|
||||
"total_count": $(echo "$state_skills $workspace_skills" | jq -s 'add | length')
|
||||
}
|
||||
EOF
|
||||
log "Saved skill state: $(echo "$state_skills" | jq length) state + $(echo "$workspace_skills" | jq length) workspace skills"
|
||||
}
|
||||
|
||||
# Check for missing skills and reinstall
|
||||
check_and_restore() {
|
||||
if [ ! -f "$TRACKER_FILE" ]; then
|
||||
log "No tracker file found. Saving current state..."
|
||||
save_state
|
||||
return 0
|
||||
fi
|
||||
|
||||
local missing_skills=()
|
||||
local state_skills=$(get_state_skills)
|
||||
|
||||
# Read expected state skills from tracker
|
||||
local expected_state=$(jq -r '.state_skills[]' "$TRACKER_FILE" 2>/dev/null)
|
||||
|
||||
for skill in $expected_state; do
|
||||
if ! echo "$state_skills" | grep -qx "$skill"; then
|
||||
missing_skills+=("$skill")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#missing_skills[@]} -eq 0 ]; then
|
||||
log "All skills accounted for ✓"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "Found ${#missing_skills[@]} missing skill(s): ${missing_skills[*]}"
|
||||
|
||||
# Try to reinstall each missing skill
|
||||
local failed=()
|
||||
for skill in "${missing_skills[@]}"; do
|
||||
log "Attempting to reinstall: $skill"
|
||||
if clawhub install "$skill" 2>&1 | tee -a "$LOG_FILE"; then
|
||||
log "✓ Reinstalled: $skill"
|
||||
else
|
||||
log "✗ Failed to reinstall: $skill"
|
||||
failed+=("$skill")
|
||||
fi
|
||||
done
|
||||
|
||||
# Update tracker with new state
|
||||
save_state
|
||||
|
||||
if [ ${#failed[@]} -gt 0 ]; then
|
||||
log "WARNING: Failed to reinstall: ${failed[*]}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Main command handler
|
||||
case "${1:-save}" in
|
||||
save)
|
||||
save_state
|
||||
;;
|
||||
check)
|
||||
check_and_restore
|
||||
;;
|
||||
restore)
|
||||
check_and_restore
|
||||
;;
|
||||
status)
|
||||
if [ -f "$TRACKER_FILE" ]; then
|
||||
echo "=== Skill Tracker Status ==="
|
||||
cat "$TRACKER_FILE" | jq .
|
||||
echo ""
|
||||
echo "=== Current State Skills ==="
|
||||
get_state_skills | jq -R . | jq -s .
|
||||
else
|
||||
echo "No tracker file found. Run: skill-tracker.sh save"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {save|check|restore|status}"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " save - Save current skill state to tracker file"
|
||||
echo " check - Check for missing skills and restore them"
|
||||
echo " restore - Same as check (alias)"
|
||||
echo " status - Show current tracker status"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
44
scripts/start-memory-viewer.sh
Executable file
44
scripts/start-memory-viewer.sh
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
# Memory Viewer Startup Script
|
||||
# Starts both API server and frontend server for Memory Viewer
|
||||
|
||||
cd /home/openclaw/.openclaw/workspace/memory-viewer
|
||||
|
||||
# Environment variables
|
||||
export WORKSPACE_DIR=/home/openclaw/.openclaw/workspace
|
||||
export PORT=3001 # API server port
|
||||
export FRONTEND_PORT=5180 # Frontend server port
|
||||
|
||||
echo "📝 Starting Memory Viewer..."
|
||||
echo " API Server: port $PORT"
|
||||
echo " Frontend: port $FRONTEND_PORT"
|
||||
echo " Workspace: $WORKSPACE_DIR"
|
||||
|
||||
# Kill any existing instances
|
||||
pkill -f "memory-viewer.*tsx" 2>/dev/null || true
|
||||
pkill -f "serve-memory-viewer" 2>/dev/null || true
|
||||
sleep 1
|
||||
|
||||
# Start API server (Hono backend)
|
||||
echo "🚀 Starting API server..."
|
||||
npx tsx server/index.ts &
|
||||
API_PID=$!
|
||||
|
||||
# Wait for API server to start
|
||||
sleep 2
|
||||
|
||||
# Start frontend server (static files)
|
||||
echo "🌐 Starting frontend server..."
|
||||
cd /home/openclaw/.openclaw/workspace
|
||||
PORT=$FRONTEND_PORT node scripts/serve-memory-viewer.js &
|
||||
FRONTEND_PID=$!
|
||||
|
||||
echo ""
|
||||
echo "✅ Memory Viewer started!"
|
||||
echo " Access via Tailscale: http://$(hostname -I | grep '100\\.' | head -1):$FRONTEND_PORT"
|
||||
echo " API: http://localhost:$PORT"
|
||||
echo ""
|
||||
echo "PIDs: API=$API_PID, Frontend=$FRONTEND_PID"
|
||||
|
||||
# Keep script running
|
||||
wait
|
||||
57
scripts/test-imap-simple.js
Normal file
57
scripts/test-imap-simple.js
Normal file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env node
|
||||
require('dotenv').config({ path: '/home/openclaw/.openclaw/workspace/skills/imap-smtp-email/.env' });
|
||||
|
||||
const Imap = require('imap');
|
||||
|
||||
const config = {
|
||||
user: process.env.IMAP_USER,
|
||||
password: process.env.IMAP_PASS,
|
||||
host: process.env.IMAP_HOST || '127.0.0.1',
|
||||
port: parseInt(process.env.IMAP_PORT) || 1143,
|
||||
tls: process.env.IMAP_TLS === 'true',
|
||||
tlsOptions: {
|
||||
rejectUnauthorized: process.env.IMAP_REJECT_UNAUTHORIZED !== 'false',
|
||||
},
|
||||
connTimeout: 10000,
|
||||
authTimeout: 10000,
|
||||
};
|
||||
|
||||
console.log('Config:', {
|
||||
user: config.user,
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
tls: config.tls,
|
||||
rejectUnauthorized: config.tlsOptions.rejectUnauthorized
|
||||
});
|
||||
|
||||
const imap = new Imap(config);
|
||||
|
||||
imap.once('ready', () => {
|
||||
console.log('✅ Connected!');
|
||||
imap.openBox('INBOX', false, (err, box) => {
|
||||
if (err) {
|
||||
console.log('Error opening INBOX:', err.message);
|
||||
imap.end();
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('INBOX opened');
|
||||
imap.search(['ALL'], (err, results) => {
|
||||
if (err) {
|
||||
console.log('Search error:', err.message);
|
||||
} else {
|
||||
console.log(`Found ${results.length} messages`);
|
||||
const recent = results.slice(-5);
|
||||
console.log('Last 5 UIDs:', recent.join(', '));
|
||||
}
|
||||
imap.end();
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
imap.once('error', (err) => {
|
||||
console.log('❌ IMAP Error:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
imap.connect();
|
||||
90
scripts/verify-backup.sh
Executable file
90
scripts/verify-backup.sh
Executable file
@@ -0,0 +1,90 @@
|
||||
#!/bin/bash
|
||||
# Backup verification script - checks that backups are actually happening
|
||||
# Run this as a cron job or heartbeat check
|
||||
|
||||
set -e
|
||||
|
||||
GITEA_TOKEN="ba94c160b97c3a0fa5cf528ecc107eb2c8cddaa7"
|
||||
REPO_URL="http://git:${GITEA_TOKEN}@gitea.kangaroo-eel.ts.net:3000/Anthony/openclaw-backup.git"
|
||||
LOG_FILE="$HOME/.openclaw/workspace/logs/backup-verify.log"
|
||||
GOTIFY_URL="http://runtipi.kangaroo-eel.ts.net:8129"
|
||||
GOTIFY_TOKEN="AGoV3cAUyUMDbyt"
|
||||
|
||||
# Max age in hours before we consider backup stale
|
||||
MAX_AGE_HOURS=26 # Slightly more than 24 to account for cron jitter
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
notify() {
|
||||
local title="$1"
|
||||
local message="$2"
|
||||
local priority="${3:-5}"
|
||||
curl -s -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
"${GOTIFY_URL}/message?token=${GOTIFY_TOKEN}" \
|
||||
-d "{\"title\": \"${title}\", \"message\": \"${message}\", \"priority\": ${priority}}" > /dev/null || true
|
||||
}
|
||||
|
||||
# Ensure log directory exists
|
||||
mkdir -p "$(dirname "$LOG_FILE")"
|
||||
|
||||
log "Starting backup verification..."
|
||||
|
||||
# Clone minimal repo to check the last commit
|
||||
REPO_DIR=$(mktemp -d)
|
||||
if ! git clone --depth 1 "$REPO_URL" "$REPO_DIR" 2>/dev/null; then
|
||||
log "ERROR: Could not clone repo"
|
||||
notify "🚨 BACKUP VERIFICATION FAILED" "Cannot clone Gitea repo" 8
|
||||
rm -rf "$REPO_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$REPO_DIR"
|
||||
|
||||
# Get last commit timestamp
|
||||
LAST_COMMIT_DATE=$(git log -1 --format="%ci" 2>/dev/null || echo "")
|
||||
LAST_COMMIT_MSG=$(git log -1 --format="%s" 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$LAST_COMMIT_DATE" ]; then
|
||||
log "ERROR: Could not get last commit date"
|
||||
notify "🚨 BACKUP VERIFICATION FAILED" "Repo has no commits" 8
|
||||
rm -rf "$REPO_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Parse the date (handle both GNU and BSD date)
|
||||
LAST_COMMIT_EPOCH=$(date -d "$LAST_COMMIT_DATE" +%s 2>/dev/null || date -j -f "%Y-%m-%d %H:%M:%S %z" "$LAST_COMMIT_DATE" +%s 2>/dev/null)
|
||||
CURRENT_EPOCH=$(date +%s)
|
||||
AGE_HOURS=$(( (CURRENT_EPOCH - LAST_COMMIT_EPOCH) / 3600 ))
|
||||
|
||||
log "Last backup: $LAST_COMMIT_DATE ($AGE_HOURS hours ago)"
|
||||
log "Commit message: $LAST_COMMIT_MSG"
|
||||
|
||||
if [ "$AGE_HOURS" -gt "$MAX_AGE_HOURS" ]; then
|
||||
log "ALERT: Backup is stale! Last commit was $AGE_HOURS hours ago"
|
||||
notify "🚨 BACKUP STALE" "Last backup was $AGE_HOURS hours ago (expected within $MAX_AGE_HOURS hours)\n\nLast commit: $LAST_COMMIT_DATE\nMessage: $LAST_COMMIT_MSG" 8
|
||||
rm -rf "$REPO_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify critical files exist in the latest commit
|
||||
log "Verifying critical files in backup..."
|
||||
MISSING_FILES=()
|
||||
[ -f "$REPO_DIR/openclaw-state/openclaw.json" ] || MISSING_FILES+=("openclaw.json")
|
||||
[ -f "$REPO_DIR/MEMORY.md" ] || MISSING_FILES+=("MEMORY.md")
|
||||
[ -d "$REPO_DIR/openclaw-state/cron" ] || MISSING_FILES+=("cron/")
|
||||
|
||||
if [ ${#MISSING_FILES[@]} -gt 0 ]; then
|
||||
log "ERROR: Critical files missing: ${MISSING_FILES[*]}"
|
||||
notify "🚨 BACKUP INCOMPLETE" "Missing critical files: ${MISSING_FILES[*]}" 8
|
||||
rm -rf "$REPO_DIR"
|
||||
exit 1
|
||||
else
|
||||
log "✅ All critical files present"
|
||||
fi
|
||||
|
||||
log "✅ Backup verification PASSED"
|
||||
rm -rf "$REPO_DIR"
|
||||
exit 0
|
||||
41
scripts/weekly-cleanup.sh
Executable file
41
scripts/weekly-cleanup.sh
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
# Weekly Cleanup Script
|
||||
# Archives old logs, checks disk space, cleans up old sessions
|
||||
|
||||
echo "🧹 Weekly Cleanup Started"
|
||||
echo "========================"
|
||||
|
||||
# 1. Check disk space
|
||||
echo ""
|
||||
echo "📊 Disk Usage:"
|
||||
df -h / | tail -1
|
||||
|
||||
# 2. Count session files
|
||||
echo ""
|
||||
echo "📁 Sessions: $(find /home/openclaw/.openclaw/workspace/sessions -name "*.jsonl" 2>/dev/null | wc -l) session files"
|
||||
|
||||
# 3. Count logs
|
||||
echo ""
|
||||
echo "📝 Logs: $(find /home/openclaw/.openclaw/logs -name "*.log" 2>/dev/null | wc -l) log files"
|
||||
|
||||
# 4. Archive old memory files (older than 30 days)
|
||||
echo ""
|
||||
echo "🗂️ Archiving old memory files..."
|
||||
find /home/openclaw/.openclaw/workspace/memory -name "*.md" -mtime +30 -exec gzip {} \; 2>/dev/null
|
||||
ARCHIVED=$(find /home/openclaw/.openclaw/workspace/memory -name "*.gz" 2>/dev/null | wc -l)
|
||||
echo " Archived: $ARCHIVED files"
|
||||
|
||||
# 5. Clean old session transcripts (older than 14 days)
|
||||
echo ""
|
||||
echo "🗑️ Cleaning old sessions (14+ days)..."
|
||||
find /home/openclaw/.openclaw/workspace/sessions -name "*.jsonl" -mtime +14 -delete 2>/dev/null
|
||||
|
||||
# 6. Check backup status
|
||||
echo ""
|
||||
echo "💾 Last Backup:"
|
||||
ls -lh /home/openclaw/.openclaw/workspace/archive/backup 2>/dev/null | tail -1 || echo " No local backup found"
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "✅ Cleanup Complete!"
|
||||
echo "Date: $(date)"
|
||||
27
scripts/with-error-log.sh
Executable file
27
scripts/with-error-log.sh
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [ "$#" -lt 1 ]; then
|
||||
echo "Usage: with-error-log.sh <command...>"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
cmd="$*"
|
||||
set +e
|
||||
output=$(eval "$cmd" 2>&1)
|
||||
code=$?
|
||||
set -e
|
||||
|
||||
if [ "$code" -ne 0 ]; then
|
||||
/home/openclaw/.openclaw/workspace/scripts/log-error.sh \
|
||||
"Command failed: $cmd" \
|
||||
"$output" \
|
||||
"Captured via with-error-log.sh wrapper" \
|
||||
"high" \
|
||||
"infra" >/dev/null || true
|
||||
echo "$output"
|
||||
echo "(error auto-logged to .learnings/ERRORS.md)" >&2
|
||||
exit "$code"
|
||||
fi
|
||||
|
||||
echo "$output"
|
||||
Reference in New Issue
Block a user