@wipcomputer/wip-ldm-os 0.4.87-alpha.1 → 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.
- package/bin/ldm.js +6 -4
- package/docs/backup/README.md +83 -80
- package/docs/backup/TECHNICAL.md +107 -90
- package/package.json +15 -3
- package/scripts/ldm-backup-engine.py +1176 -0
- package/scripts/ldm-backup-policy.json +1420 -0
- package/scripts/ldm-backup.sh +15 -273
- package/scripts/test-backup-engine.py +609 -0
- package/scripts/test-ldm-install-preserves-foreign-bin.mjs +5 -0
- package/shared/docs/README.md.tmpl +1 -1
- package/shared/docs/directory-map.md.tmpl +1 -1
- package/shared/docs/how-backup-works.md.tmpl +50 -83
- package/shared/docs/system-directories.md.tmpl +1 -1
- package/shared/docs/what-is-dotldm.md.tmpl +1 -1
package/scripts/ldm-backup.sh
CHANGED
|
@@ -1,285 +1,27 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
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" "$@"
|