230 lines
6.9 KiB
Python
230 lines
6.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Build the full Hermes recovery bundle markdown file."""
|
|
import sqlite3, json, datetime, os, subprocess, sys
|
|
|
|
db = sqlite3.connect('/root/.hermes/state.db')
|
|
c = db.cursor()
|
|
|
|
# ===== 1. Get today's user conversations =====
|
|
sessions_data = []
|
|
c.execute("""
|
|
SELECT id, title, started_at, ended_at, end_reason, message_count
|
|
FROM sessions
|
|
WHERE started_at >= 1783180800 AND id LIKE '2026%'
|
|
ORDER BY started_at
|
|
""")
|
|
for row in c.fetchall():
|
|
sessions_data.append({
|
|
'id': row[0],
|
|
'title': row[1] or '(no title)',
|
|
'started': datetime.datetime.fromtimestamp(row[2]).strftime('%H:%M:%S'),
|
|
'ended': datetime.datetime.fromtimestamp(row[3]).strftime('%H:%M:%S') if row[3] else 'active',
|
|
'end_reason': row[4] or 'running',
|
|
'msg_count': row[5]
|
|
})
|
|
|
|
conversation_text = ""
|
|
for sd in sessions_data:
|
|
conversation_text += f"\n\n{'='*70}\n"
|
|
conversation_text += f"SESSION: {sd['id']} | {sd['title']}\n"
|
|
conversation_text += f"Started: {sd['started']} | Ended: {sd['ended']} | Status: {sd['end_reason']}\n"
|
|
conversation_text += f"{'='*70}\n\n"
|
|
|
|
c.execute("SELECT role, content, timestamp FROM messages WHERE session_id = ? ORDER BY id", (sd['id'],))
|
|
for role, content, ts in c.fetchall():
|
|
ts_dt = datetime.datetime.fromtimestamp(ts).strftime('%H:%M:%S')
|
|
text = (content or '(empty)').strip()
|
|
# Truncate very long messages
|
|
if len(text) > 5000:
|
|
text = text[:5000] + f"\n... [truncated, was {len(content)} chars]"
|
|
conversation_text += f"[{ts_dt}] {role.upper()}:\n{text}\n\n"
|
|
|
|
db.close()
|
|
|
|
# ===== 2. Read key files =====
|
|
def readf(path):
|
|
try:
|
|
with open(path) as f:
|
|
return f.read()
|
|
except Exception as e:
|
|
return f"[ERROR: {e}]"
|
|
|
|
soul = readf('/root/.hermes/SOUL.md')
|
|
config_yaml = readf('/root/.hermes/config.yaml')
|
|
dotenv = readf('/root/.hermes/.env')
|
|
aws_creds = readf('/root/.aws/credentials')
|
|
service_file = readf('/etc/systemd/system/hermes.service')
|
|
wisp_pub = readf('/root/.ssh/wisp_rsa.pub')
|
|
wisp_priv = readf('/root/.ssh/wisp_rsa')
|
|
imap_pass = readf('/root/.config/himalaya/g-germainebrown.pass')
|
|
hetzner_token = readf('/root/.hermes/scripts/.hetzner_token')
|
|
netcup_key = readf('/root/.hermes/scripts/.netcup_api_key')
|
|
wisp_config = readf('/root/.hermes/scripts/wisp-backup/config.yaml')
|
|
|
|
# ===== 3. Get cron jobs =====
|
|
result = subprocess.run(['hermes', 'cron', 'list', '--json'], capture_output=True, text=True, timeout=15)
|
|
cron_text = ""
|
|
try:
|
|
cron_data = json.loads(result.stdout)
|
|
cron_jobs = cron_data if isinstance(cron_data, list) else cron_data.get('jobs', cron_data.get('data', []))
|
|
if not cron_jobs:
|
|
cron_text = "(no JSON output — fallback to text list)\n"
|
|
# Fallback: run text mode
|
|
result2 = subprocess.run(['hermes', 'cron', 'list'], capture_output=True, text=True, timeout=15)
|
|
cron_text += result2.stdout
|
|
else:
|
|
for job in cron_jobs:
|
|
cron_text += f"\n### {job.get('name', 'unnamed')}\n"
|
|
cron_text += f"- ID: {job.get('id', '?')}\n"
|
|
cron_text += f"- Schedule: {job.get('schedule', '?')}\n"
|
|
cron_text += f"- Repeat: {job.get('repeat', '∞')}\n"
|
|
cron_text += f"- Deliver: {job.get('deliver', '?')}\n"
|
|
cron_text += f"- Script: {job.get('script', 'none')}\n"
|
|
cron_text += f"- Skills: {job.get('skills', 'none')}\n"
|
|
cron_text += f"- Mode: {'no-agent' if job.get('no_agent') else 'agent-driven'}\n"
|
|
cron_text += f"- Next run: {job.get('next_run', '?')}\n"
|
|
except Exception as e:
|
|
cron_text = f"[Error getting cron jobs: {e}]\n"
|
|
result2 = subprocess.run(['hermes', 'cron', 'list'], capture_output=True, text=True, timeout=15)
|
|
cron_text += result2.stdout
|
|
|
|
# ===== 4. Build the recovery document =====
|
|
doc = f"""# Hermes Full Recovery Bundle — Jul 5, 2026
|
|
|
|
> Save this file. If the server dies, paste the contents of the **BOOTSTRAP** section
|
|
> below into a new Hermes agent to restore me.
|
|
|
|
---
|
|
## BOOTSTRAP INSTRUCTIONS
|
|
|
|
### Step 1 — Prerequisites
|
|
1. Install Hermes Agent v0.18.0+ on a Linux server (Debian/Ubuntu)
|
|
2. Register a Telegram bot with BotFather, get a TELEGRAM_BOT_TOKEN
|
|
3. Make sure python3, git, and systemd are available
|
|
|
|
### Step 2 — Create files
|
|
Create these files with the exact contents shown in their sections below:
|
|
- `/root/.hermes/config.yaml`
|
|
- `/root/.hermes/.env`
|
|
- `/root/.aws/credentials`
|
|
- `/root/.ssh/wisp_rsa` (chmod 600)
|
|
- `/root/.hermes/scripts/.hetzner_token` (chmod 600)
|
|
- `/root/.hermes/scripts/.netcup_api_key` (chmod 600)
|
|
- `/root/.hermes/SOUL.md`
|
|
- All scripts in the Scripts section
|
|
|
|
### Step 3 — Start Hermes
|
|
```bash
|
|
hermes gateway start
|
|
```
|
|
|
|
### Step 4 — Recreate cron jobs
|
|
```bash
|
|
# Run these commands after Hermes is online
|
|
# (cron job definitions below)
|
|
```
|
|
|
|
### Step 5 — Verify
|
|
```bash
|
|
hermes gateway status
|
|
hermes cron list
|
|
source /opt/awscli-venv/bin/activate && aws s3 ls s3://hermes-vps-backups/ --endpoint-url https://s3.us-east-1.wasabisys.com/
|
|
```
|
|
|
|
---
|
|
## CREDENTIALS
|
|
|
|
**SMTP Password (g@germainebrown.com):** `{imap_pass.strip()}`
|
|
**Wasabi S3:** endpoint=https://s3.us-east-1.wasabisys.com | Access Key: GYH83FP0KL0K85N60JKQ
|
|
**Hetzner API Token:** `{hetzner_token.strip()}`
|
|
**netcup API Key:** `{netcup_key.strip()}`
|
|
**admin-ai API Key:** In config.yaml (sk-lbhhxIk...bWPA)
|
|
**Telegram Bot Token:** In .env file below
|
|
|
|
---
|
|
## CONFIG FILES
|
|
|
|
### /root/.hermes/SOUL.md
|
|
```
|
|
{soul}
|
|
```
|
|
|
|
### /root/.hermes/config.yaml
|
|
```yaml
|
|
{config_yaml}
|
|
```
|
|
|
|
### /root/.hermes/.env
|
|
```
|
|
{dotenv}
|
|
```
|
|
|
|
### /root/.aws/credentials
|
|
```
|
|
{aws_creds}
|
|
```
|
|
|
|
### /etc/systemd/system/hermes.service
|
|
```
|
|
{service_file}
|
|
```
|
|
|
|
---
|
|
## SSH KEYS
|
|
|
|
### WISP Public Key
|
|
```
|
|
{wisp_pub}
|
|
```
|
|
|
|
### WISP Private Key
|
|
```
|
|
{wisp_priv}
|
|
```
|
|
|
|
---
|
|
## WISP BACKUP CONFIG
|
|
|
|
```yaml
|
|
{wisp_config}
|
|
```
|
|
|
|
---
|
|
## CRON JOBS
|
|
{cron_text}
|
|
|
|
---
|
|
## SCRIPT FILES
|
|
|
|
### imap_triage.py
|
|
"""
|
|
# Read scripts
|
|
script_dir = '/root/.hermes/scripts'
|
|
for fname in sorted(os.listdir(script_dir)):
|
|
fpath = os.path.join(script_dir, fname)
|
|
if os.path.isfile(fpath) and not fname.startswith('.'):
|
|
content = readf(fpath)
|
|
doc += f"\n### {fname}\n```\n{content}\n```\n"
|
|
|
|
# Also read wisp-backup subdirectory scripts
|
|
wisp_dir = os.path.join(script_dir, 'wisp-backup')
|
|
if os.path.isdir(wisp_dir):
|
|
for fname in sorted(os.listdir(wisp_dir)):
|
|
fpath = os.path.join(wisp_dir, fname)
|
|
if os.path.isfile(fpath) and not fname.endswith('.pyc'):
|
|
content = readf(fpath)
|
|
doc += f"\n### wisp-backup/{fname}\n```\n{content}\n```\n"
|
|
|
|
doc += "\n\n---\n\n## TODAY'S CONVERSATION HISTORY\n"
|
|
doc += conversation_text
|
|
|
|
# ===== 5. Write file =====
|
|
output_path = '/root/.hermes/recovery-bundle-2026-07-05.md'
|
|
with open(output_path, 'w') as f:
|
|
f.write(doc)
|
|
|
|
file_size = os.path.getsize(output_path)
|
|
print(f"RECOVERY FILE: {output_path}")
|
|
print(f"SIZE: {file_size} bytes ({file_size/1024:.1f} KB)")
|
|
print(f"TODAY SESSIONS: {len(sessions_data)}")
|