@wipcomputer/wip-ldm-os 0.4.86 → 0.4.87-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,285 +1,27 @@
1
1
  #!/bin/bash
2
- # ldm-backup.sh Unified backup for LDM OS
3
- # Backs up: ~/.ldm/, ~/.openclaw/, ~/.claude/, $WORKSPACE/
4
- # Handles SQLite safely (sqlite3 .backup). Tars to iCloud for offsite.
5
- #
6
- # Source of truth: wip-ldm-os-private/scripts/ldm-backup.sh
7
- # Deployed to: ~/.ldm/bin/ldm-backup.sh (via ldm install)
8
- #
9
- # Usage:
10
- # ldm-backup.sh # run backup
11
- # ldm-backup.sh --dry-run # preview what would be backed up
12
- # ldm-backup.sh --keep 14 # keep last 14 backups (default: 7)
13
- # ldm-backup.sh --include-secrets # include ~/.ldm/secrets/
14
- #
15
- # Config: ~/.ldm/config.json (workspace path, backup settings, iCloud path)
2
+ # LDM OS backup entry point.
3
+ # Source: wip-ldm-os-private/scripts/ldm-backup.sh
4
+ # Installed with ldm-backup-engine.py and ldm-backup-policy.json in ~/.ldm/bin/.
16
5
 
17
6
  set -euo pipefail
18
7
 
19
- export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
8
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
+ ENGINE="${LDM_BACKUP_ENGINE:-$SCRIPT_DIR/ldm-backup-engine.py}"
10
+ POLICY="${LDM_BACKUP_POLICY:-$SCRIPT_DIR/ldm-backup-policy.json}"
20
11
 
21
- LDM_HOME="$HOME/.ldm"
22
- OC_HOME="$HOME/.openclaw"
23
- CLAUDE_HOME="$HOME/.claude"
24
- BACKUP_ROOT="$LDM_HOME/backups"
25
- KEEP=7
26
- INCLUDE_SECRETS=false
27
- DRY_RUN=false
28
-
29
- # Parse flags
30
- while [[ $# -gt 0 ]]; do
31
- case "$1" in
32
- --keep) KEEP="$2"; shift 2 ;;
33
- --include-secrets) INCLUDE_SECRETS=true; shift ;;
34
- --dry-run) DRY_RUN=true; shift ;;
35
- *) echo "Unknown flag: $1" >&2; exit 1 ;;
36
- esac
37
- done
38
-
39
- # Read workspace path from ~/.ldm/config.json
40
- WORKSPACE=""
41
- if [ -f "$LDM_HOME/config.json" ]; then
42
- WORKSPACE=$(python3 -c "import json; print(json.load(open('$LDM_HOME/config.json')).get('workspace',''))" 2>/dev/null || true)
43
- fi
44
- if [ -z "$WORKSPACE" ]; then
45
- echo "WARNING: No workspace in ~/.ldm/config.json. Skipping workspace backup."
46
- fi
47
-
48
- # Read org name from config (used for tar filename)
49
- ORG=""
50
- if [ -f "$LDM_HOME/config.json" ]; then
51
- ORG=$(python3 -c "import json; print(json.load(open('$LDM_HOME/config.json')).get('org',''))" 2>/dev/null || true)
52
- fi
53
- if [ -z "$ORG" ]; then
54
- ORG="workspace"
55
- fi
56
-
57
- # Read iCloud backup path from ~/.ldm/config.json
58
- ICLOUD_BACKUP=""
59
- if [ -f "$LDM_HOME/config.json" ]; then
60
- ICLOUD_BACKUP=$(python3 -c "
61
- import json, os
62
- c = json.load(open('$LDM_HOME/config.json'))
63
- p = c.get('paths',{}).get('icloudBackup','')
64
- print(os.path.expanduser(p))
65
- " 2>/dev/null || true)
66
- fi
67
-
68
- # Read keep from ~/.ldm/config.json (override if set there)
69
- if [ -f "$LDM_HOME/config.json" ]; then
70
- CONFIG_KEEP=$(python3 -c "import json; print(json.load(open('$LDM_HOME/config.json')).get('backup',{}).get('keep',0))" 2>/dev/null || true)
71
- if [ -n "$CONFIG_KEEP" ] && [ "$CONFIG_KEEP" -gt 0 ] 2>/dev/null; then
72
- KEEP="$CONFIG_KEEP"
73
- fi
74
- fi
75
-
76
- DATE=$(date +%Y-%m-%d--%H-%M-%S)
77
- DEST="$BACKUP_ROOT/$DATE"
78
-
79
- echo "=== LDM Backup: $DATE ==="
80
- echo " Local: $DEST"
81
- echo " iCloud: ${ICLOUD_BACKUP:-not configured}"
82
- echo " Keep: $KEEP days"
83
- echo " Workspace: ${WORKSPACE:-not configured}"
84
- echo ""
85
-
86
- if [ "$DRY_RUN" = true ]; then
87
- echo "[DRY RUN] Would back up:"
88
- echo " ~/.ldm/memory/crystal.db (sqlite3 .backup)"
89
- echo " ~/.ldm/agents/ (cp -a)"
90
- echo " ~/.ldm/state/ (cp -a)"
91
- echo " ~/.ldm/config.json (cp)"
92
- [ -f "$OC_HOME/memory/main.sqlite" ] && echo " ~/.openclaw/memory/main.sqlite (sqlite3 .backup) [$(du -sh "$OC_HOME/memory/main.sqlite" | cut -f1)]"
93
- [ -f "$OC_HOME/memory/context-embeddings.sqlite" ] && echo " ~/.openclaw/memory/context-embeddings.sqlite (sqlite3 .backup) [$(du -sh "$OC_HOME/memory/context-embeddings.sqlite" | cut -f1)]"
94
- [ -d "$OC_HOME/workspace" ] && echo " ~/.openclaw/workspace/ (tar) [$(du -sh "$OC_HOME/workspace" | cut -f1)]"
95
- [ -d "$OC_HOME/agents/main/sessions" ] && echo " ~/.openclaw/agents/main/sessions/ (tar) [$(du -sh "$OC_HOME/agents/main/sessions" | cut -f1)]"
96
- [ -f "$OC_HOME/openclaw.json" ] && echo " ~/.openclaw/openclaw.json (cp)"
97
- [ -f "$CLAUDE_HOME/CLAUDE.md" ] && echo " ~/.claude/CLAUDE.md (cp)"
98
- [ -f "$CLAUDE_HOME/settings.json" ] && echo " ~/.claude/settings.json (cp)"
99
- [ -d "$CLAUDE_HOME/projects" ] && echo " ~/.claude/projects/ (tar) [$(du -sh "$CLAUDE_HOME/projects" | cut -f1)]"
100
- if [ -n "$WORKSPACE" ] && [ -d "$WORKSPACE" ]; then
101
- # macOS du uses -I for exclusions (not --exclude)
102
- WS_KB=$(du -sk -I "node_modules" -I ".git" -I "_temp" -I "_trash" "$WORKSPACE" 2>/dev/null | cut -f1 || echo "?")
103
- WS_MB=$((WS_KB / 1024))
104
- echo " $WORKSPACE/ (tar, excludes node_modules/.git/objects/_temp/_trash)"
105
- echo " estimated size: ${WS_MB}MB (${WS_KB}KB)"
106
- if [ "$WS_KB" -gt 10000000 ] 2>/dev/null; then
107
- echo " WARNING: exceeds 10GB limit. Backup would abort."
108
- fi
109
- fi
110
- [ "$INCLUDE_SECRETS" = true ] && echo " ~/.ldm/secrets/ (cp -a)"
111
- echo ""
112
- echo "[DRY RUN] No files modified."
113
- exit 0
114
- fi
115
-
116
- # Preflight
117
- if [ ! -d "$LDM_HOME" ]; then
118
- echo "ERROR: ~/.ldm/ not found" >&2
12
+ if [ ! -f "$ENGINE" ]; then
13
+ echo "Backup failed [engine-missing]: $ENGINE" >&2
119
14
  exit 1
120
15
  fi
121
16
 
122
- mkdir -p "$DEST/ldm/memory" "$DEST/openclaw/memory" "$DEST/claude"
123
-
124
- # ── 1. Back up ~/.ldm/ ──
125
-
126
- echo "--- ~/.ldm/ ---"
127
-
128
- # Crystal DB (safe sqlite3 .backup)
129
- CRYSTAL_DB="$LDM_HOME/memory/crystal.db"
130
- if [ -f "$CRYSTAL_DB" ]; then
131
- if command -v sqlite3 &>/dev/null; then
132
- sqlite3 "$CRYSTAL_DB" ".backup '$DEST/ldm/memory/crystal.db'"
133
- echo " crystal.db: sqlite3 .backup OK"
134
- else
135
- cp "$CRYSTAL_DB" "$DEST/ldm/memory/crystal.db"
136
- [ -f "$CRYSTAL_DB-wal" ] && cp "$CRYSTAL_DB-wal" "$DEST/ldm/memory/crystal.db-wal"
137
- [ -f "$CRYSTAL_DB-shm" ] && cp "$CRYSTAL_DB-shm" "$DEST/ldm/memory/crystal.db-shm"
138
- echo " crystal.db: file copy (no sqlite3)"
139
- fi
140
- else
141
- echo " crystal.db: not found (skipped)"
142
- fi
143
-
144
- # Config
145
- [ -f "$LDM_HOME/config.json" ] && cp "$LDM_HOME/config.json" "$DEST/ldm/config.json" && echo " config.json: OK"
146
-
147
- # State
148
- [ -d "$LDM_HOME/state" ] && cp -a "$LDM_HOME/state" "$DEST/ldm/state" && echo " state/: OK"
149
-
150
- # Agents (identity, journals, daily logs)
151
- [ -d "$LDM_HOME/agents" ] && cp -a "$LDM_HOME/agents" "$DEST/ldm/agents" && echo " agents/: OK"
152
-
153
- # Secrets (optional)
154
- if [ "$INCLUDE_SECRETS" = true ] && [ -d "$LDM_HOME/secrets" ]; then
155
- cp -a "$LDM_HOME/secrets" "$DEST/ldm/secrets"
156
- chmod 700 "$DEST/ldm/secrets"
157
- echo " secrets/: OK"
158
- fi
159
-
160
- # ── 2. Back up ~/.openclaw/ ──
161
-
162
- echo "--- ~/.openclaw/ ---"
163
-
164
- # main.sqlite (safe sqlite3 .backup)
165
- if [ -f "$OC_HOME/memory/main.sqlite" ]; then
166
- if command -v sqlite3 &>/dev/null; then
167
- sqlite3 "$OC_HOME/memory/main.sqlite" ".backup '$DEST/openclaw/memory/main.sqlite'"
168
- echo " main.sqlite: sqlite3 .backup OK"
169
- else
170
- cp "$OC_HOME/memory/main.sqlite" "$DEST/openclaw/memory/main.sqlite"
171
- [ -f "$OC_HOME/memory/main.sqlite-wal" ] && cp "$OC_HOME/memory/main.sqlite-wal" "$DEST/openclaw/memory/main.sqlite-wal"
172
- echo " main.sqlite: file copy"
173
- fi
174
- fi
175
-
176
- # context-embeddings.sqlite
177
- if [ -f "$OC_HOME/memory/context-embeddings.sqlite" ]; then
178
- if command -v sqlite3 &>/dev/null; then
179
- sqlite3 "$OC_HOME/memory/context-embeddings.sqlite" ".backup '$DEST/openclaw/memory/context-embeddings.sqlite'"
180
- echo " context-embeddings: sqlite3 .backup OK"
181
- else
182
- cp "$OC_HOME/memory/context-embeddings.sqlite" "$DEST/openclaw/memory/context-embeddings.sqlite"
183
- echo " context-embeddings: file copy"
184
- fi
185
- fi
186
-
187
- # Workspace
188
- [ -d "$OC_HOME/workspace" ] && tar -cf "$DEST/openclaw/workspace.tar" -C "$OC_HOME" workspace 2>/dev/null && echo " workspace/: tar OK"
189
-
190
- # OC sessions
191
- [ -d "$OC_HOME/agents/main/sessions" ] && tar -cf "$DEST/openclaw/sessions.tar" -C "$OC_HOME/agents/main" sessions 2>/dev/null && echo " sessions/: tar OK"
192
-
193
- # OC config
194
- [ -f "$OC_HOME/openclaw.json" ] && cp "$OC_HOME/openclaw.json" "$DEST/openclaw/openclaw.json" && echo " openclaw.json: OK"
195
-
196
- # State files
197
- for f in session-export-state.json cc-export-watermark.json cc-capture-watermark.json memory-capture-state.json; do
198
- [ -f "$OC_HOME/memory/$f" ] && cp "$OC_HOME/memory/$f" "$DEST/openclaw/memory/$f"
199
- done
200
- echo " state files: OK"
201
-
202
- # ── 3. Back up ~/.claude/ ──
203
-
204
- echo "--- ~/.claude/ ---"
205
-
206
- [ -f "$CLAUDE_HOME/CLAUDE.md" ] && cp "$CLAUDE_HOME/CLAUDE.md" "$DEST/claude/CLAUDE.md" && echo " CLAUDE.md: OK"
207
- [ -f "$CLAUDE_HOME/settings.json" ] && cp "$CLAUDE_HOME/settings.json" "$DEST/claude/settings.json" && echo " settings.json: OK"
208
- [ -d "$CLAUDE_HOME/projects" ] && tar -cf "$DEST/claude/projects.tar" -C "$CLAUDE_HOME" projects 2>/dev/null && echo " projects/: tar OK"
209
-
210
- # ── 4. Back up workspace ──
211
-
212
- if [ -n "$WORKSPACE" ] && [ -d "$WORKSPACE" ]; then
213
- echo "--- $WORKSPACE/ ---"
214
-
215
- # Size guard: estimate workspace size before tarring
216
- # macOS du uses -I for exclusions (not --exclude)
217
- ESTIMATED_KB=$(du -sk -I "node_modules" -I ".git" -I "_temp" -I "_trash" "$WORKSPACE" 2>/dev/null | cut -f1 || echo "0")
218
- MAX_KB=10000000 # 10GB
219
- if [ "$ESTIMATED_KB" -gt "$MAX_KB" ] 2>/dev/null; then
220
- echo " ERROR: Workspace estimated at ${ESTIMATED_KB}KB (>10GB). Aborting tar to prevent disk fill."
221
- echo " Check for large directories: du -sh $WORKSPACE/*/"
222
- else
223
- tar -cf "$DEST/$ORG.tar" \
224
- --exclude "node_modules" \
225
- --exclude ".git/objects" \
226
- --exclude ".DS_Store" \
227
- --exclude "*/staff/cc-mini/documents/backups" \
228
- --exclude "*/_temp/backups" \
229
- --exclude "*/_temp/_archive" \
230
- --exclude "*/_trash" \
231
- -C "$(dirname "$WORKSPACE")" "$(basename "$WORKSPACE")" 2>/dev/null \
232
- && echo " workspace: tar OK (est ${ESTIMATED_KB}KB)" \
233
- || echo " workspace: tar FAILED"
234
- fi
235
- fi
236
-
237
- # ── 5. iCloud offsite ──
238
-
239
- if [ -n "$ICLOUD_BACKUP" ] && [ -d "$(dirname "$ICLOUD_BACKUP")" ]; then
240
- echo "--- iCloud offsite ---"
241
- mkdir -p "$ICLOUD_BACKUP"
242
- ORG=$(python3 -c "import json; print(json.load(open('$LDM_HOME/config.json')).get('org','ldmos'))" 2>/dev/null || echo "ldmos")
243
- DEVICE=$(hostname -s)
244
- TAR_NAME="${ORG}-${DEVICE}-${DATE}.tar.gz"
245
- tar -czf "$ICLOUD_BACKUP/$TAR_NAME" -C "$BACKUP_ROOT" "$DATE" 2>/dev/null \
246
- && echo " $TAR_NAME: OK" \
247
- || echo " iCloud tar: FAILED"
248
-
249
- # Rotate iCloud tars
250
- ICLOUD_COUNT=$(ls -1 "$ICLOUD_BACKUP"/*.tar.gz 2>/dev/null | wc -l | tr -d ' ')
251
- if [ "$ICLOUD_COUNT" -gt "$KEEP" ]; then
252
- REMOVE_COUNT=$((ICLOUD_COUNT - KEEP))
253
- ls -1t "$ICLOUD_BACKUP"/*.tar.gz | tail -n "$REMOVE_COUNT" | while read OLD; do
254
- rm -f "$OLD"
255
- echo " Rotated: $(basename "$OLD")"
256
- done
257
- fi
17
+ if [ ! -f "$POLICY" ]; then
18
+ echo "Backup failed [policy-missing]: $POLICY" >&2
19
+ exit 1
258
20
  fi
259
21
 
260
- # ── 6. Rotate local backups ──
261
-
262
- echo "--- Rotation ---"
263
- BACKUP_COUNT=$(ls -1d "$BACKUP_ROOT"/20??-??-??--* 2>/dev/null | wc -l | tr -d ' ')
264
- if [ "$BACKUP_COUNT" -gt "$KEEP" ]; then
265
- REMOVE_COUNT=$((BACKUP_COUNT - KEEP))
266
- ls -1d "$BACKUP_ROOT"/20??-??-??--* | head -n "$REMOVE_COUNT" | while read OLD; do
267
- # Skip pinned backups
268
- if [ -f "$OLD/.pinned" ]; then
269
- echo " Skipped (pinned): $(basename "$OLD")"
270
- continue
271
- fi
272
- rm -rf "$OLD"
273
- echo " Removed: $(basename "$OLD")"
274
- done
22
+ if ! command -v python3 >/dev/null 2>&1; then
23
+ echo "Backup failed [python3-missing]: Python 3 is required for the stdlib-only backup engine" >&2
24
+ exit 1
275
25
  fi
276
26
 
277
- # ── Summary ──
278
-
279
- TOTAL_SIZE=$(du -sh "$DEST" | cut -f1)
280
- echo ""
281
- echo "=== Backup complete ==="
282
- echo " Location: $DEST"
283
- echo " Size: $TOTAL_SIZE"
284
- echo " Backups: $BACKUP_COUNT total (keeping $KEEP)"
285
- [ -n "$ICLOUD_BACKUP" ] && echo " iCloud: $ICLOUD_BACKUP/"
27
+ exec python3 "$ENGINE" --policy "$POLICY" "$@"