this repo has no description
1#!/usr/bin/env bash
2# Migrate local data to production server
3#
4# Syncs:
5# - SQLite database (assistant.db)
6# - Letta agent state (.af file)
7#
8# Usage:
9# ./migrate-data.sh
10
11set -euo pipefail
12IFS=$'\n\t'
13
14# Colors
15GREEN='\033[0;32m'
16BLUE='\033[0;34m'
17RED='\033[0;31m'
18YELLOW='\033[0;33m'
19NC='\033[0m'
20
21SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
22PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
23SERVER_HOST="assistant-vps"
24
25log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
26log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
27log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
28log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
29die() { log_error "$1"; exit 1; }
30
31#######################################
32# Stop app container
33#######################################
34stop_app() {
35 log_info "Stopping app container..."
36 ssh "$SERVER_HOST" "cd /opt/assistant && docker compose -f docker-compose.yml -f docker-compose.prod.yml stop app"
37 log_success "App stopped"
38}
39
40#######################################
41# Sync SQLite database
42#######################################
43sync_sqlite() {
44 log_info "Syncing SQLite database..."
45
46 local db_file="$PROJECT_DIR/data/assistant.db"
47
48 if [[ ! -f "$db_file" ]]; then
49 log_warn "No local database found: $db_file"
50 return
51 fi
52
53 # Checkpoint WAL to ensure all data is in main db file
54 if command -v sqlite3 &> /dev/null; then
55 sqlite3 "$db_file" "PRAGMA wal_checkpoint(TRUNCATE);" 2>/dev/null || true
56 fi
57
58 # Remove remote WAL/SHM files to prevent stale data
59 ssh "$SERVER_HOST" "rm -f /opt/assistant/data/assistant.db-wal /opt/assistant/data/assistant.db-shm"
60
61 # Copy to server
62 scp "$db_file" "$SERVER_HOST:/opt/assistant/data/assistant.db"
63 ssh "$SERVER_HOST" "chown 1000:1000 /opt/assistant/data/assistant.db"
64
65 log_success "SQLite database synced"
66}
67
68#######################################
69# Export local Letta agent
70#######################################
71export_letta_agent() {
72 log_info "Exporting local Letta agent..."
73
74 local export_file="$SCRIPT_DIR/agent_export.af"
75
76 # Run TypeScript export script
77 cd "$PROJECT_DIR"
78 bun run --silent - << 'TYPESCRIPT'
79import { Letta } from '@letta-ai/letta-client';
80import { writeFileSync } from 'fs';
81
82const AGENT_NAME = 'adhd-support-agent';
83
84async function main() {
85 // Connect to local Letta (dev mode)
86 const client = new Letta({
87 baseURL: process.env.LETTA_BASE_URL || 'http://localhost:8283',
88 });
89
90 // Find agent by name
91 let agentId: string | null = null;
92 for await (const agent of client.agents.list()) {
93 if (agent.name === AGENT_NAME) {
94 agentId = agent.id;
95 break;
96 }
97 }
98
99 if (!agentId) {
100 console.error(`Agent '${AGENT_NAME}' not found locally`);
101 process.exit(1);
102 }
103
104 console.log(`Found agent: ${agentId}`);
105
106 // Export agent to .af file (JSON format)
107 const exported = await client.agents.exportFile(agentId);
108
109 // Write to file as JSON
110 writeFileSync('infra/agent_export.af', JSON.stringify(exported));
111
112 console.log('Agent exported to infra/agent_export.af');
113}
114
115main().catch(err => {
116 console.error('Export failed:', err);
117 process.exit(1);
118});
119TYPESCRIPT
120
121 if [[ ! -f "$export_file" ]]; then
122 log_warn "Agent export failed or no local agent exists"
123 return 1
124 fi
125
126 log_success "Agent exported to $export_file"
127 return 0
128}
129
130#######################################
131# Import Letta agent to production
132#######################################
133import_letta_agent() {
134 log_info "Importing agent to production Letta..."
135
136 local export_file="$SCRIPT_DIR/agent_export.af"
137
138 if [[ ! -f "$export_file" ]]; then
139 log_warn "No agent export file found: $export_file"
140 return
141 fi
142
143 # Copy export file to server's data directory (mounted in container)
144 scp "$export_file" "$SERVER_HOST:/opt/assistant/data/agent_export.af"
145 ssh "$SERVER_HOST" "chown 1000:1000 /opt/assistant/data/agent_export.af"
146
147 # Run import on server
148 ssh "$SERVER_HOST" << 'EOF'
149cd /opt/assistant
150
151# Load env for Letta password
152source .env
153
154# Run import script inside app container
155docker compose -f docker-compose.yml -f docker-compose.prod.yml exec -T app bun run --silent - << 'TYPESCRIPT'
156import { Letta } from '@letta-ai/letta-client';
157import { readFileSync } from 'fs';
158
159const AGENT_NAME = 'adhd-support-agent';
160
161async function main() {
162 const client = new Letta({
163 baseURL: process.env.LETTA_BASE_URL || 'http://letta:8283',
164 apiKey: process.env.LETTA_SERVER_PASSWORD || undefined,
165 });
166
167 // Delete any existing agents with this name (including _copy variants)
168 for await (const agent of client.agents.list()) {
169 if (agent.name === AGENT_NAME || agent.name === `${AGENT_NAME}_copy`) {
170 console.log(`Deleting existing agent '${agent.name}' (${agent.id})...`);
171 await client.agents.delete(agent.id);
172 }
173 }
174
175 // Import agent from .af file (in mounted data directory)
176 const fileData = readFileSync('/app/data/agent_export.af');
177
178 // Create a Blob for the file upload
179 const file = new Blob([fileData], { type: 'application/json' });
180
181 const imported = await client.agents.importFile({ file, append_copy_suffix: false });
182 console.log(`Imported agent IDs: ${imported.agent_ids.join(', ')}`);
183}
184
185main().catch(err => {
186 console.error('Import failed:', err);
187 process.exit(1);
188});
189TYPESCRIPT
190EOF
191
192 # Cleanup
193 ssh "$SERVER_HOST" "rm -f /opt/assistant/data/agent_export.af"
194 rm -f "$export_file"
195
196 log_success "Agent imported to production"
197}
198
199#######################################
200# Restart app to pick up changes
201#######################################
202restart_app() {
203 log_info "Restarting app container..."
204 ssh "$SERVER_HOST" "cd /opt/assistant && docker compose -f docker-compose.yml -f docker-compose.prod.yml restart app"
205 log_success "App restarted"
206}
207
208#######################################
209# Main
210#######################################
211main() {
212 echo -e "${BLUE}========================================${NC}"
213 echo -e "${BLUE} Data Migration Script${NC}"
214 echo -e "${BLUE}========================================${NC}"
215 echo
216
217 # Check SSH connectivity
218 if ! ssh -o ConnectTimeout=5 "$SERVER_HOST" "echo ok" &> /dev/null; then
219 die "Cannot connect to $SERVER_HOST"
220 fi
221
222 # Stop app to safely copy SQLite database
223 stop_app
224 sync_sqlite
225
226 # Start app for Letta import (needs the app container running)
227 restart_app
228
229 if export_letta_agent; then
230 import_letta_agent
231 fi
232
233 echo
234 log_success "Migration complete!"
235}
236
237main "$@"