AI Newsletter Digest improvements: fixed QP soft line break decoding, URL extraction, and content cleaning

This commit is contained in:
Krilly
2026-03-04 13:29:22 +00:00
parent 29a98137a7
commit 57dd294675
13706 changed files with 2114953 additions and 237629 deletions

106
scripts/anthonymau-email-check.sh Executable file
View 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
View 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"

View File

@@ -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"

View 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

View 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
View 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
View 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
View 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

View 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

View File

@@ -0,0 +1,3 @@
#!/bin/bash
# Gateway startup notification disabled
exit 0

View 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"
]
}
}

View 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"
]
}
}

View 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

View 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();

View File

@@ -0,0 +1 @@
/home/openclaw/.openclaw/workspace/scripts/check-krillyclaw-email.sh

50
scripts/log-error.sh Executable file
View 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}"

View 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

View 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

View 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
View 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

View File

@@ -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
View 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
View 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
View 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
View 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
View 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

View 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
View 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
View 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
View 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"