clawon 0.1.17 → 0.1.19
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/README.md +50 -7
- package/dist/index.js +299 -46
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,13 +23,16 @@ npx clawon local backup
|
|
|
23
23
|
npx clawon local backup --tag "before migration"
|
|
24
24
|
npx clawon local backup --include-memory-db # Include SQLite memory index
|
|
25
25
|
npx clawon local backup --include-sessions # Include chat history
|
|
26
|
+
npx clawon local backup --include-secrets # Include credentials and auth files
|
|
27
|
+
npx clawon local backup --encrypt # Encrypt with AES-256-GCM
|
|
28
|
+
npx clawon local backup --include-secrets --encrypt # Encrypted with secrets
|
|
26
29
|
npx clawon local backup --no-secret-scan # Skip secret scanning
|
|
27
30
|
npx clawon local backup --max-snapshots 10 # Keep only 10 most recent
|
|
28
31
|
|
|
29
32
|
# List all local backups
|
|
30
33
|
npx clawon local list
|
|
31
34
|
|
|
32
|
-
# Restore
|
|
35
|
+
# Restore (detects encryption automatically)
|
|
33
36
|
npx clawon local restore # Latest backup
|
|
34
37
|
npx clawon local restore --pick 2 # Backup #2 from list
|
|
35
38
|
npx clawon local restore --file path.tar.gz # External file
|
|
@@ -45,12 +48,16 @@ npx clawon local schedule on
|
|
|
45
48
|
npx clawon local schedule on --every 6h --max-snapshots 10
|
|
46
49
|
npx clawon local schedule on --include-memory-db
|
|
47
50
|
npx clawon local schedule on --include-sessions
|
|
51
|
+
npx clawon local schedule on --include-secrets
|
|
52
|
+
npx clawon local schedule on --encrypt # Requires CLAWON_ENCRYPT_PASSPHRASE env var
|
|
48
53
|
|
|
49
54
|
# Disable local schedule
|
|
50
55
|
npx clawon local schedule off
|
|
51
56
|
|
|
52
57
|
# Schedule cloud backups (requires Hobby or Pro account)
|
|
53
58
|
npx clawon schedule on
|
|
59
|
+
npx clawon schedule on --encrypt # Encrypted cloud backups
|
|
60
|
+
npx clawon schedule on --encrypt --include-secrets # With secrets
|
|
54
61
|
npx clawon schedule off
|
|
55
62
|
|
|
56
63
|
# Check schedule status
|
|
@@ -94,11 +101,13 @@ npx clawon backup --dry-run # Preview without uploading
|
|
|
94
101
|
npx clawon backup --include-memory-db # Requires Hobby or Pro
|
|
95
102
|
npx clawon backup --include-sessions # Requires Hobby or Pro
|
|
96
103
|
npx clawon backup --no-secret-scan # Skip secret scanning
|
|
104
|
+
npx clawon backup --encrypt # Encrypt before uploading
|
|
105
|
+
npx clawon backup --include-secrets --encrypt # Secrets + encryption
|
|
97
106
|
|
|
98
107
|
# List cloud backups
|
|
99
108
|
npx clawon list
|
|
100
109
|
|
|
101
|
-
# Restore from cloud
|
|
110
|
+
# Restore from cloud (decrypts automatically if encrypted)
|
|
102
111
|
npx clawon restore
|
|
103
112
|
npx clawon restore --snapshot <id> # Specific snapshot
|
|
104
113
|
npx clawon restore --dry-run # Preview without extracting
|
|
@@ -116,6 +125,7 @@ npx clawon activity # Recent events
|
|
|
116
125
|
npx clawon discover # Show exactly which files would be backed up
|
|
117
126
|
npx clawon discover --include-memory-db # Include SQLite memory index
|
|
118
127
|
npx clawon discover --include-sessions # Include chat history
|
|
128
|
+
npx clawon discover --include-secrets # Preview with credentials included
|
|
119
129
|
npx clawon discover --scan # Scan for secrets in discovered files
|
|
120
130
|
npx clawon schedule status # Show active schedules
|
|
121
131
|
npx clawon status # Connection status, workspace, and file count
|
|
@@ -149,15 +159,15 @@ These are **always excluded**, even if they match an include pattern:
|
|
|
149
159
|
|---------|-----|
|
|
150
160
|
| `credentials/**` | API keys, tokens, auth files |
|
|
151
161
|
| `openclaw.json` | May contain credentials |
|
|
152
|
-
| `agents/*/auth.json` | Authentication data |
|
|
153
|
-
| `agents/*/auth-profiles.json` | Auth profiles |
|
|
162
|
+
| `agents/*/agent/auth.json` | Authentication data |
|
|
163
|
+
| `agents/*/agent/auth-profiles.json` | Auth profiles |
|
|
154
164
|
| `agents/*/sessions/**` | Chat history (large, use `--include-sessions` to include) |
|
|
155
165
|
| `memory/lancedb/**` | Vector database (binary, large) |
|
|
156
166
|
| `memory/*.sqlite` | SQLite databases (use `--include-memory-db` to include) |
|
|
157
167
|
| `*.lock`, `*.wal`, `*.shm` | Database lock files |
|
|
158
168
|
| `node_modules/**` | Dependencies |
|
|
159
169
|
|
|
160
|
-
**Credentials
|
|
170
|
+
**Credentials are excluded by default.** The `credentials/` directory, `openclaw.json`, and agent auth files are excluded unless you use `--include-secrets`. For local backups, `--include-secrets` works standalone. For cloud backups, it requires `--encrypt`. Verify what gets included by running `npx clawon discover --include-secrets`.
|
|
161
171
|
|
|
162
172
|
## Secret Scanning
|
|
163
173
|
|
|
@@ -178,9 +188,42 @@ npx clawon local backup --no-secret-scan # Disable scanning for a backup
|
|
|
178
188
|
npx clawon backup --no-secret-scan # Same for cloud backups
|
|
179
189
|
```
|
|
180
190
|
|
|
191
|
+
## Encryption
|
|
192
|
+
|
|
193
|
+
The `--encrypt` flag encrypts backups with **AES-256-GCM** using a passphrase you provide. Available for both local and cloud backups.
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
# Local encrypted backup
|
|
197
|
+
npx clawon local backup --encrypt
|
|
198
|
+
|
|
199
|
+
# Cloud encrypted backup
|
|
200
|
+
npx clawon backup --encrypt
|
|
201
|
+
|
|
202
|
+
# Cloud backup with secrets (requires --encrypt)
|
|
203
|
+
npx clawon backup --include-secrets --encrypt
|
|
204
|
+
|
|
205
|
+
# Scheduled encrypted backups (requires env var)
|
|
206
|
+
export CLAWON_ENCRYPT_PASSPHRASE=your-passphrase
|
|
207
|
+
npx clawon local schedule on --encrypt
|
|
208
|
+
npx clawon schedule on --encrypt
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Key derivation:** PBKDF2 with 100,000 iterations, SHA-512, 16-byte random salt → 256-bit key.
|
|
212
|
+
|
|
213
|
+
**Local format:** encrypted archives use `.tar.gz.enc` extension with a binary header:
|
|
214
|
+
```
|
|
215
|
+
[4B "CLWN"][1B version=1][16B salt][12B IV][...ciphertext...][16B authTag]
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**Cloud format:** each file is encrypted individually before upload. Salt is stored in the manifest, per-file IVs are recorded at confirm time.
|
|
219
|
+
|
|
220
|
+
**Restore:** encryption is detected automatically — you'll be prompted for the passphrase.
|
|
221
|
+
|
|
222
|
+
> **Warning:** there is no passphrase recovery. If you forget your passphrase, encrypted backups cannot be decrypted. Store your passphrase securely.
|
|
223
|
+
|
|
181
224
|
## Archive Format
|
|
182
225
|
|
|
183
|
-
Local backups are standard gzip-compressed tar archives (`.tar.gz`). You can inspect
|
|
226
|
+
Local backups are standard gzip-compressed tar archives (`.tar.gz`). Encrypted backups use `.tar.gz.enc`. You can inspect unencrypted archives with standard tools:
|
|
184
227
|
|
|
185
228
|
```bash
|
|
186
229
|
# List contents
|
|
@@ -202,7 +245,7 @@ Each archive contains:
|
|
|
202
245
|
| | Local | Cloud |
|
|
203
246
|
|---|---|---|
|
|
204
247
|
| **Location** | `~/.clawon/backups/` | Clawon servers (Supabase Storage) |
|
|
205
|
-
| **Format** | `.tar.gz` | Individual files with signed URLs |
|
|
248
|
+
| **Format** | `.tar.gz` (or `.tar.gz.enc` if encrypted) | Individual files with signed URLs |
|
|
206
249
|
| **Limit** | Unlimited | 2 snapshots (Starter), more on paid plans |
|
|
207
250
|
| **Account required** | No | Yes |
|
|
208
251
|
| **Cross-machine** | No (manual file transfer) | Yes |
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Command } from "commander";
|
|
|
5
5
|
import fs2 from "fs";
|
|
6
6
|
import path2 from "path";
|
|
7
7
|
import os from "os";
|
|
8
|
-
import
|
|
8
|
+
import readline2 from "readline";
|
|
9
9
|
import { execSync } from "child_process";
|
|
10
10
|
import * as tar from "tar";
|
|
11
11
|
|
|
@@ -4647,6 +4647,126 @@ function formatFindings(findings) {
|
|
|
4647
4647
|
return lines.join("\n");
|
|
4648
4648
|
}
|
|
4649
4649
|
|
|
4650
|
+
// src/crypto.ts
|
|
4651
|
+
import crypto from "crypto";
|
|
4652
|
+
import readline from "readline";
|
|
4653
|
+
var ENC_HEADER = Buffer.from("CLWN");
|
|
4654
|
+
var ENC_VERSION = 1;
|
|
4655
|
+
var PBKDF2_ITERATIONS = 1e5;
|
|
4656
|
+
var PBKDF2_DIGEST = "sha512";
|
|
4657
|
+
var SALT_LENGTH = 16;
|
|
4658
|
+
var IV_LENGTH = 12;
|
|
4659
|
+
var KEY_LENGTH = 32;
|
|
4660
|
+
var AUTH_TAG_LENGTH = 16;
|
|
4661
|
+
function deriveKey(passphrase, salt) {
|
|
4662
|
+
return crypto.pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, KEY_LENGTH, PBKDF2_DIGEST);
|
|
4663
|
+
}
|
|
4664
|
+
function encryptBuffer(plaintext, key) {
|
|
4665
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
4666
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
|
4667
|
+
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
4668
|
+
const authTag = cipher.getAuthTag();
|
|
4669
|
+
return { ciphertext: encrypted, iv, authTag };
|
|
4670
|
+
}
|
|
4671
|
+
function decryptBuffer(ciphertext, key, iv, authTag) {
|
|
4672
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
|
4673
|
+
decipher.setAuthTag(authTag);
|
|
4674
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
4675
|
+
}
|
|
4676
|
+
function encryptFile(plaintext, passphrase) {
|
|
4677
|
+
const salt = crypto.randomBytes(SALT_LENGTH);
|
|
4678
|
+
const key = deriveKey(passphrase, salt);
|
|
4679
|
+
const { ciphertext, iv, authTag } = encryptBuffer(plaintext, key);
|
|
4680
|
+
const header = Buffer.alloc(5);
|
|
4681
|
+
ENC_HEADER.copy(header, 0);
|
|
4682
|
+
header.writeUInt8(ENC_VERSION, 4);
|
|
4683
|
+
const encrypted = Buffer.concat([header, salt, iv, ciphertext, authTag]);
|
|
4684
|
+
return { encrypted, salt };
|
|
4685
|
+
}
|
|
4686
|
+
function decryptFile(encrypted, passphrase) {
|
|
4687
|
+
if (encrypted.length < 5 + SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH) {
|
|
4688
|
+
throw new Error("Invalid encrypted file: too short");
|
|
4689
|
+
}
|
|
4690
|
+
const magic = encrypted.subarray(0, 4);
|
|
4691
|
+
if (!magic.equals(ENC_HEADER)) {
|
|
4692
|
+
throw new Error("Invalid encrypted file: bad magic header");
|
|
4693
|
+
}
|
|
4694
|
+
const version = encrypted.readUInt8(4);
|
|
4695
|
+
if (version !== ENC_VERSION) {
|
|
4696
|
+
throw new Error(`Unsupported encryption version: ${version}`);
|
|
4697
|
+
}
|
|
4698
|
+
let offset = 5;
|
|
4699
|
+
const salt = encrypted.subarray(offset, offset + SALT_LENGTH);
|
|
4700
|
+
offset += SALT_LENGTH;
|
|
4701
|
+
const iv = encrypted.subarray(offset, offset + IV_LENGTH);
|
|
4702
|
+
offset += IV_LENGTH;
|
|
4703
|
+
const authTag = encrypted.subarray(encrypted.length - AUTH_TAG_LENGTH);
|
|
4704
|
+
const ciphertext = encrypted.subarray(offset, encrypted.length - AUTH_TAG_LENGTH);
|
|
4705
|
+
const key = deriveKey(passphrase, salt);
|
|
4706
|
+
try {
|
|
4707
|
+
return decryptBuffer(ciphertext, key, iv, authTag);
|
|
4708
|
+
} catch {
|
|
4709
|
+
throw new Error("Decryption failed \u2014 wrong passphrase?");
|
|
4710
|
+
}
|
|
4711
|
+
}
|
|
4712
|
+
function encryptCloudFile(plaintext, key) {
|
|
4713
|
+
const { ciphertext, iv, authTag } = encryptBuffer(plaintext, key);
|
|
4714
|
+
return {
|
|
4715
|
+
encrypted: Buffer.concat([ciphertext, authTag]),
|
|
4716
|
+
iv: iv.toString("hex")
|
|
4717
|
+
};
|
|
4718
|
+
}
|
|
4719
|
+
function decryptCloudFile(encrypted, key, ivHex) {
|
|
4720
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
4721
|
+
const authTag = encrypted.subarray(encrypted.length - AUTH_TAG_LENGTH);
|
|
4722
|
+
const ciphertext = encrypted.subarray(0, encrypted.length - AUTH_TAG_LENGTH);
|
|
4723
|
+
try {
|
|
4724
|
+
return decryptBuffer(ciphertext, key, iv, authTag);
|
|
4725
|
+
} catch {
|
|
4726
|
+
throw new Error("Decryption failed \u2014 wrong passphrase?");
|
|
4727
|
+
}
|
|
4728
|
+
}
|
|
4729
|
+
function isEncryptedArchive(filePath) {
|
|
4730
|
+
return filePath.endsWith(".tar.gz.enc");
|
|
4731
|
+
}
|
|
4732
|
+
async function getPassphrase(scheduled, action) {
|
|
4733
|
+
const envPassphrase = process.env.CLAWON_ENCRYPT_PASSPHRASE;
|
|
4734
|
+
if (scheduled) {
|
|
4735
|
+
if (!envPassphrase) {
|
|
4736
|
+
throw new Error("CLAWON_ENCRYPT_PASSPHRASE environment variable is required for scheduled encrypted backups.");
|
|
4737
|
+
}
|
|
4738
|
+
return envPassphrase;
|
|
4739
|
+
}
|
|
4740
|
+
if (envPassphrase) return envPassphrase;
|
|
4741
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
4742
|
+
const askHidden = (prompt) => {
|
|
4743
|
+
return new Promise((resolve) => {
|
|
4744
|
+
rl.question(prompt, (answer) => {
|
|
4745
|
+
resolve(answer);
|
|
4746
|
+
});
|
|
4747
|
+
});
|
|
4748
|
+
};
|
|
4749
|
+
const passphrase = await askHidden(`Enter ${action === "encrypt" ? "encryption" : "decryption"} passphrase: `);
|
|
4750
|
+
if (!passphrase) {
|
|
4751
|
+
rl.close();
|
|
4752
|
+
throw new Error("Passphrase cannot be empty.");
|
|
4753
|
+
}
|
|
4754
|
+
if (action === "encrypt") {
|
|
4755
|
+
const confirm = await askHidden("Confirm passphrase: ");
|
|
4756
|
+
rl.close();
|
|
4757
|
+
if (passphrase !== confirm) {
|
|
4758
|
+
throw new Error("Passphrases do not match.");
|
|
4759
|
+
}
|
|
4760
|
+
} else {
|
|
4761
|
+
rl.close();
|
|
4762
|
+
}
|
|
4763
|
+
return passphrase;
|
|
4764
|
+
}
|
|
4765
|
+
function generateSalt() {
|
|
4766
|
+
const salt = crypto.randomBytes(SALT_LENGTH);
|
|
4767
|
+
return { salt, saltHex: salt.toString("hex") };
|
|
4768
|
+
}
|
|
4769
|
+
|
|
4650
4770
|
// src/index.ts
|
|
4651
4771
|
var CONFIG_DIR = path2.join(os.homedir(), ".clawon");
|
|
4652
4772
|
var CONFIG_PATH = path2.join(CONFIG_DIR, "config.json");
|
|
@@ -4708,8 +4828,8 @@ var INCLUDE_PATTERNS = [
|
|
|
4708
4828
|
var EXCLUDE_PATTERNS = [
|
|
4709
4829
|
"credentials/**",
|
|
4710
4830
|
"openclaw.json",
|
|
4711
|
-
"agents/*/auth.json",
|
|
4712
|
-
"agents/*/auth-profiles.json",
|
|
4831
|
+
"agents/*/agent/auth.json",
|
|
4832
|
+
"agents/*/agent/auth-profiles.json",
|
|
4713
4833
|
"agents/*/sessions/**",
|
|
4714
4834
|
"memory/lancedb/**",
|
|
4715
4835
|
"memory/*.sqlite",
|
|
@@ -4725,13 +4845,24 @@ function matchGlob(filePath, pattern) {
|
|
|
4725
4845
|
let regexPattern = pattern.replace(/\./g, "\\.").replace(/\*\*\//g, "(.*/)?").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*");
|
|
4726
4846
|
return new RegExp(`^${regexPattern}$`).test(filePath);
|
|
4727
4847
|
}
|
|
4728
|
-
|
|
4848
|
+
var SECRET_PATTERNS = [
|
|
4849
|
+
"credentials/**",
|
|
4850
|
+
"openclaw.json",
|
|
4851
|
+
"agents/*/agent/auth.json",
|
|
4852
|
+
"agents/*/agent/auth-profiles.json"
|
|
4853
|
+
];
|
|
4854
|
+
function shouldInclude(relativePath, includeMemoryDb = false, includeSessions = false, includeSecrets = false) {
|
|
4729
4855
|
if (includeMemoryDb && matchGlob(relativePath, "memory/*.sqlite")) {
|
|
4730
4856
|
return true;
|
|
4731
4857
|
}
|
|
4732
4858
|
if (includeSessions && matchGlob(relativePath, "agents/*/sessions/**")) {
|
|
4733
4859
|
return true;
|
|
4734
4860
|
}
|
|
4861
|
+
if (includeSecrets) {
|
|
4862
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
4863
|
+
if (matchGlob(relativePath, pattern)) return true;
|
|
4864
|
+
}
|
|
4865
|
+
}
|
|
4735
4866
|
for (const pattern of EXCLUDE_PATTERNS) {
|
|
4736
4867
|
if (matchGlob(relativePath, pattern)) return false;
|
|
4737
4868
|
}
|
|
@@ -4740,7 +4871,7 @@ function shouldInclude(relativePath, includeMemoryDb = false, includeSessions =
|
|
|
4740
4871
|
}
|
|
4741
4872
|
return false;
|
|
4742
4873
|
}
|
|
4743
|
-
function discoverFiles(baseDir, includeMemoryDb = false, includeSessions = false) {
|
|
4874
|
+
function discoverFiles(baseDir, includeMemoryDb = false, includeSessions = false, includeSecrets = false) {
|
|
4744
4875
|
const files = [];
|
|
4745
4876
|
function walk(dir, relativePath = "") {
|
|
4746
4877
|
if (!fs2.existsSync(dir)) return;
|
|
@@ -4751,7 +4882,7 @@ function discoverFiles(baseDir, includeMemoryDb = false, includeSessions = false
|
|
|
4751
4882
|
if (entry.isDirectory()) {
|
|
4752
4883
|
walk(fullPath, relPath);
|
|
4753
4884
|
} else if (entry.isFile()) {
|
|
4754
|
-
if (shouldInclude(relPath, includeMemoryDb, includeSessions)) {
|
|
4885
|
+
if (shouldInclude(relPath, includeMemoryDb, includeSessions, includeSecrets)) {
|
|
4755
4886
|
const stats = fs2.statSync(fullPath);
|
|
4756
4887
|
files.push({
|
|
4757
4888
|
path: relPath,
|
|
@@ -4764,7 +4895,7 @@ function discoverFiles(baseDir, includeMemoryDb = false, includeSessions = false
|
|
|
4764
4895
|
walk(baseDir);
|
|
4765
4896
|
return files;
|
|
4766
4897
|
}
|
|
4767
|
-
async function createLocalArchive(files, openclawDir, outputPath, tag, includeMemoryDb, includeSessions, trigger) {
|
|
4898
|
+
async function createLocalArchive(files, openclawDir, outputPath, tag, includeMemoryDb, includeSessions, includeSecrets, trigger, encrypted) {
|
|
4768
4899
|
const meta = {
|
|
4769
4900
|
version: 2,
|
|
4770
4901
|
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -4772,6 +4903,8 @@ async function createLocalArchive(files, openclawDir, outputPath, tag, includeMe
|
|
|
4772
4903
|
file_count: files.length,
|
|
4773
4904
|
...includeMemoryDb ? { include_memory_db: true } : {},
|
|
4774
4905
|
...includeSessions ? { include_sessions: true } : {},
|
|
4906
|
+
...includeSecrets ? { include_secrets: true } : {},
|
|
4907
|
+
...encrypted ? { encrypted: true } : {},
|
|
4775
4908
|
...trigger ? { trigger } : {}
|
|
4776
4909
|
};
|
|
4777
4910
|
const metaPath = path2.join(openclawDir, "_clawon_meta.json");
|
|
@@ -4833,7 +4966,7 @@ async function promptSecretAction(findings) {
|
|
|
4833
4966
|
console.log(` [a] Abort backup`);
|
|
4834
4967
|
console.log(` [i] Ignore warnings and backup all files`);
|
|
4835
4968
|
console.log("");
|
|
4836
|
-
const rl =
|
|
4969
|
+
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
4837
4970
|
return new Promise((resolve) => {
|
|
4838
4971
|
rl.question(" Choice (s/a/i): ", (answer) => {
|
|
4839
4972
|
rl.close();
|
|
@@ -4941,7 +5074,13 @@ program.command("login").description("Connect to Clawon with your API key").opti
|
|
|
4941
5074
|
process.exit(1);
|
|
4942
5075
|
}
|
|
4943
5076
|
});
|
|
4944
|
-
program.command("backup").description("Backup your OpenClaw workspace to the cloud").option("--dry-run", "Show what would be backed up without uploading").option("--tag <label>", "Add a label to this backup").option("--include-memory-db", "Include SQLite memory index").option("--include-sessions", "Include chat history (sessions)").option("--scheduled", "Internal: triggered by cron (suppresses interactive output)").option("--no-secret-scan", "Skip secret scanning before backup").action(async (opts) => {
|
|
5077
|
+
program.command("backup").description("Backup your OpenClaw workspace to the cloud").option("--dry-run", "Show what would be backed up without uploading").option("--tag <label>", "Add a label to this backup").option("--include-memory-db", "Include SQLite memory index").option("--include-sessions", "Include chat history (sessions)").option("--include-secrets", "Include credentials and auth files in backup").option("--encrypt", "Encrypt files with AES-256-GCM before uploading").option("--scheduled", "Internal: triggered by cron (suppresses interactive output)").option("--no-secret-scan", "Skip secret scanning before backup").action(async (opts) => {
|
|
5078
|
+
if (opts.includeSecrets && !opts.encrypt) {
|
|
5079
|
+
console.error("\u2717 --include-secrets requires --encrypt for cloud backups.");
|
|
5080
|
+
console.error(" Use: clawon backup --include-secrets --encrypt");
|
|
5081
|
+
console.error(" Or use local backups: clawon local backup --include-secrets");
|
|
5082
|
+
process.exit(1);
|
|
5083
|
+
}
|
|
4945
5084
|
const cfg = readConfig();
|
|
4946
5085
|
if (!cfg) {
|
|
4947
5086
|
console.error("\u2717 Not logged in. Run: clawon login --api-key <key>");
|
|
@@ -4957,7 +5096,7 @@ program.command("backup").description("Backup your OpenClaw workspace to the clo
|
|
|
4957
5096
|
process.exit(1);
|
|
4958
5097
|
}
|
|
4959
5098
|
console.log("Discovering files...");
|
|
4960
|
-
let files = discoverFiles(OPENCLAW_DIR, !!opts.includeMemoryDb, !!opts.includeSessions);
|
|
5099
|
+
let files = discoverFiles(OPENCLAW_DIR, !!opts.includeMemoryDb, !!opts.includeSessions, !!(opts.includeSecrets && opts.encrypt));
|
|
4961
5100
|
if (files.length === 0) {
|
|
4962
5101
|
console.error("\u2717 No files found to backup");
|
|
4963
5102
|
process.exit(1);
|
|
@@ -5010,6 +5149,15 @@ program.command("backup").description("Backup your OpenClaw workspace to the clo
|
|
|
5010
5149
|
return;
|
|
5011
5150
|
}
|
|
5012
5151
|
try {
|
|
5152
|
+
let encryptionKey = null;
|
|
5153
|
+
let encryptionSaltHex = null;
|
|
5154
|
+
if (opts.encrypt) {
|
|
5155
|
+
const passphrase = await getPassphrase(!!opts.scheduled, "encrypt");
|
|
5156
|
+
const { salt, saltHex } = generateSalt();
|
|
5157
|
+
encryptionKey = deriveKey(passphrase, salt);
|
|
5158
|
+
encryptionSaltHex = saltHex;
|
|
5159
|
+
console.log("Encryption enabled (AES-256-GCM)");
|
|
5160
|
+
}
|
|
5013
5161
|
console.log("\nCreating backup...");
|
|
5014
5162
|
const { snapshotId, uploadUrls } = await api(
|
|
5015
5163
|
cfg.apiBaseUrl,
|
|
@@ -5020,14 +5168,21 @@ program.command("backup").description("Backup your OpenClaw workspace to the clo
|
|
|
5020
5168
|
profileId: cfg.profileId,
|
|
5021
5169
|
workspaceId: cfg.workspaceId,
|
|
5022
5170
|
files: files.map((f) => ({ path: f.path, size: f.size })),
|
|
5023
|
-
...opts.tag ? { tag: opts.tag } : {}
|
|
5171
|
+
...opts.tag ? { tag: opts.tag } : {},
|
|
5172
|
+
...encryptionSaltHex ? { encryption: { method: "aes-256-gcm", salt: encryptionSaltHex } } : {}
|
|
5024
5173
|
}
|
|
5025
5174
|
);
|
|
5026
5175
|
console.log(`Uploading ${files.length} files...`);
|
|
5027
5176
|
let uploaded = 0;
|
|
5177
|
+
const fileIvs = {};
|
|
5028
5178
|
for (const file of files) {
|
|
5029
5179
|
const fullPath = path2.join(OPENCLAW_DIR, file.path);
|
|
5030
|
-
|
|
5180
|
+
let content = fs2.readFileSync(fullPath);
|
|
5181
|
+
if (encryptionKey) {
|
|
5182
|
+
const { encrypted, iv } = encryptCloudFile(content, encryptionKey);
|
|
5183
|
+
content = encrypted;
|
|
5184
|
+
fileIvs[file.path] = iv;
|
|
5185
|
+
}
|
|
5031
5186
|
const uploadRes = await fetch(uploadUrls[file.path], {
|
|
5032
5187
|
method: "PUT",
|
|
5033
5188
|
headers: { "content-type": "application/octet-stream" },
|
|
@@ -5043,17 +5198,21 @@ program.command("backup").description("Backup your OpenClaw workspace to the clo
|
|
|
5043
5198
|
console.log("");
|
|
5044
5199
|
await api(cfg.apiBaseUrl, "/api/v1/backups/confirm", "POST", cfg.apiKey, {
|
|
5045
5200
|
snapshotId,
|
|
5046
|
-
profileId: cfg.profileId
|
|
5201
|
+
profileId: cfg.profileId,
|
|
5202
|
+
...Object.keys(fileIvs).length > 0 ? { fileIvs } : {}
|
|
5047
5203
|
});
|
|
5048
5204
|
console.log("\n\u2713 Backup complete!");
|
|
5049
5205
|
console.log(` Snapshot ID: ${snapshotId}`);
|
|
5050
5206
|
console.log(` Files: ${files.length}`);
|
|
5051
5207
|
console.log(` Size: ${(totalSize / 1024).toFixed(1)} KB`);
|
|
5208
|
+
if (opts.encrypt) console.log(` Encrypted: yes (AES-256-GCM)`);
|
|
5052
5209
|
trackCliEvent(cfg.profileId, opts.scheduled ? "scheduled_backup_created" : "cloud_backup_created", {
|
|
5053
5210
|
file_count: files.length,
|
|
5054
5211
|
total_bytes: totalSize,
|
|
5055
5212
|
include_memory_db: !!opts.includeMemoryDb,
|
|
5056
5213
|
include_sessions: !!opts.includeSessions,
|
|
5214
|
+
include_secrets: !!opts.includeSecrets,
|
|
5215
|
+
encrypted: !!opts.encrypt,
|
|
5057
5216
|
type: "cloud",
|
|
5058
5217
|
workspace_slug: cfg.workspaceSlug,
|
|
5059
5218
|
trigger: opts.scheduled ? "scheduled" : "manual",
|
|
@@ -5087,7 +5246,7 @@ program.command("restore").description("Restore your OpenClaw workspace from the
|
|
|
5087
5246
|
}
|
|
5088
5247
|
try {
|
|
5089
5248
|
console.log("Fetching backup...");
|
|
5090
|
-
const { snapshot, files, downloadUrls } = await api(
|
|
5249
|
+
const { snapshot, files, downloadUrls, encryption } = await api(
|
|
5091
5250
|
cfg.apiBaseUrl,
|
|
5092
5251
|
"/api/v1/backups/download",
|
|
5093
5252
|
"POST",
|
|
@@ -5101,11 +5260,19 @@ program.command("restore").description("Restore your OpenClaw workspace from the
|
|
|
5101
5260
|
console.log(`Found backup from ${new Date(snapshot.created_at).toLocaleString()}`);
|
|
5102
5261
|
console.log(` Files: ${files.length}`);
|
|
5103
5262
|
console.log(` Size: ${(totalSize / 1024).toFixed(1)} KB`);
|
|
5263
|
+
if (encryption) console.log(` Encrypted: yes (${encryption.method})`);
|
|
5104
5264
|
if (opts.dryRun) {
|
|
5105
5265
|
console.log("\n[Dry run] Files that would be restored:");
|
|
5106
5266
|
files.forEach((f) => console.log(` ${f.path}`));
|
|
5107
5267
|
return;
|
|
5108
5268
|
}
|
|
5269
|
+
let encryptionKey = null;
|
|
5270
|
+
if (encryption) {
|
|
5271
|
+
console.log("\nBackup is encrypted. Decrypting...");
|
|
5272
|
+
const passphrase = await getPassphrase(false, "decrypt");
|
|
5273
|
+
const salt = Buffer.from(encryption.salt, "hex");
|
|
5274
|
+
encryptionKey = deriveKey(passphrase, salt);
|
|
5275
|
+
}
|
|
5109
5276
|
console.log("\nDownloading files...");
|
|
5110
5277
|
let downloaded = 0;
|
|
5111
5278
|
for (const file of files) {
|
|
@@ -5113,7 +5280,10 @@ program.command("restore").description("Restore your OpenClaw workspace from the
|
|
|
5113
5280
|
if (!res.ok) {
|
|
5114
5281
|
throw new Error(`Failed to download ${file.path}: ${res.status}`);
|
|
5115
5282
|
}
|
|
5116
|
-
|
|
5283
|
+
let content = Buffer.from(await res.arrayBuffer());
|
|
5284
|
+
if (encryptionKey && file.iv) {
|
|
5285
|
+
content = decryptCloudFile(content, encryptionKey, file.iv);
|
|
5286
|
+
}
|
|
5117
5287
|
const targetPath = path2.join(OPENCLAW_DIR, file.path);
|
|
5118
5288
|
ensureDir(path2.dirname(targetPath));
|
|
5119
5289
|
fs2.writeFileSync(targetPath, content);
|
|
@@ -5275,12 +5445,12 @@ program.command("delete [id]").description("Delete a snapshot").option("--oldest
|
|
|
5275
5445
|
process.exit(1);
|
|
5276
5446
|
}
|
|
5277
5447
|
});
|
|
5278
|
-
program.command("discover").description("Preview which files would be included in a backup").option("--include-memory-db", "Include SQLite memory index").option("--include-sessions", "Include chat history (sessions)").option("--scan", "Run secret scanning on discovered files").action(async (opts) => {
|
|
5448
|
+
program.command("discover").description("Preview which files would be included in a backup").option("--include-memory-db", "Include SQLite memory index").option("--include-sessions", "Include chat history (sessions)").option("--include-secrets", "Include credentials and auth files").option("--scan", "Run secret scanning on discovered files").action(async (opts) => {
|
|
5279
5449
|
if (!fs2.existsSync(OPENCLAW_DIR)) {
|
|
5280
5450
|
console.error(`\u2717 OpenClaw directory not found: ${OPENCLAW_DIR}`);
|
|
5281
5451
|
process.exit(1);
|
|
5282
5452
|
}
|
|
5283
|
-
const files = discoverFiles(OPENCLAW_DIR, !!opts.includeMemoryDb, !!opts.includeSessions);
|
|
5453
|
+
const files = discoverFiles(OPENCLAW_DIR, !!opts.includeMemoryDb, !!opts.includeSessions, !!opts.includeSecrets);
|
|
5284
5454
|
if (files.length === 0) {
|
|
5285
5455
|
console.log("No files matched the include patterns.");
|
|
5286
5456
|
return;
|
|
@@ -5315,10 +5485,10 @@ Total: ${files.length} files (${(totalSize / 1024).toFixed(1)} KB)`);
|
|
|
5315
5485
|
console.log(` Scanned ${scanResult.filesScanned} files in ${scanResult.durationMs}ms (${scanResult.rulesLoaded} rules loaded)`);
|
|
5316
5486
|
}
|
|
5317
5487
|
const cfg = readConfig();
|
|
5318
|
-
trackCliEvent(cfg?.profileId || "anonymous", "cli_discover", { file_count: files.length, include_memory_db: !!opts.includeMemoryDb, include_sessions: !!opts.includeSessions });
|
|
5488
|
+
trackCliEvent(cfg?.profileId || "anonymous", "cli_discover", { file_count: files.length, include_memory_db: !!opts.includeMemoryDb, include_sessions: !!opts.includeSessions, include_secrets: !!opts.includeSecrets });
|
|
5319
5489
|
});
|
|
5320
5490
|
var schedule = program.command("schedule").description("Manage scheduled cloud backups");
|
|
5321
|
-
schedule.command("on").description("Enable scheduled cloud backups via cron").option("--every <interval>", "Backup interval: 1h, 6h, 12h, 24h", "12h").action(async (opts) => {
|
|
5491
|
+
schedule.command("on").description("Enable scheduled cloud backups via cron").option("--every <interval>", "Backup interval: 1h, 6h, 12h, 24h", "12h").option("--encrypt", "Encrypt backups with AES-256-GCM").option("--include-secrets", "Include credentials and auth files (requires --encrypt)").action(async (opts) => {
|
|
5322
5492
|
assertNotWindows();
|
|
5323
5493
|
const cfg = readConfig();
|
|
5324
5494
|
if (!cfg) {
|
|
@@ -5330,6 +5500,16 @@ schedule.command("on").description("Enable scheduled cloud backups via cron").op
|
|
|
5330
5500
|
console.error(`\u2717 Invalid interval: ${interval}. Valid options: ${VALID_INTERVALS.join(", ")}`);
|
|
5331
5501
|
process.exit(1);
|
|
5332
5502
|
}
|
|
5503
|
+
if (opts.includeSecrets && !opts.encrypt) {
|
|
5504
|
+
console.error("\u2717 --include-secrets requires --encrypt for cloud backups.");
|
|
5505
|
+
console.error(" Use: clawon schedule on --include-secrets --encrypt");
|
|
5506
|
+
process.exit(1);
|
|
5507
|
+
}
|
|
5508
|
+
if (opts.encrypt && !process.env.CLAWON_ENCRYPT_PASSPHRASE) {
|
|
5509
|
+
console.error("\u2717 --encrypt requires the CLAWON_ENCRYPT_PASSPHRASE environment variable for scheduled backups.");
|
|
5510
|
+
console.error(" Set it in your shell profile (~/.bashrc, ~/.zshrc) so cron can access it.");
|
|
5511
|
+
process.exit(1);
|
|
5512
|
+
}
|
|
5333
5513
|
try {
|
|
5334
5514
|
const data = await api(
|
|
5335
5515
|
cfg.apiBaseUrl || "https://clawon.io",
|
|
@@ -5349,22 +5529,29 @@ schedule.command("on").description("Enable scheduled cloud backups via cron").op
|
|
|
5349
5529
|
process.exit(1);
|
|
5350
5530
|
}
|
|
5351
5531
|
const cronExpr = INTERVAL_CRON[interval];
|
|
5352
|
-
const
|
|
5532
|
+
const scheduledArgs = "backup --scheduled" + (opts.encrypt ? " --encrypt" : "") + (opts.includeSecrets ? " --include-secrets" : "");
|
|
5533
|
+
const command = resolveCliCommand(scheduledArgs);
|
|
5353
5534
|
addCronEntry(CRON_MARKER_CLOUD, cronExpr, command);
|
|
5354
5535
|
updateConfig({
|
|
5355
5536
|
schedule: {
|
|
5356
5537
|
cloud: {
|
|
5357
5538
|
enabled: true,
|
|
5358
|
-
intervalHours: parseInt(interval)
|
|
5539
|
+
intervalHours: parseInt(interval),
|
|
5540
|
+
...opts.encrypt ? { encrypt: true } : {},
|
|
5541
|
+
...opts.includeSecrets ? { includeSecrets: true } : {}
|
|
5359
5542
|
}
|
|
5360
5543
|
}
|
|
5361
5544
|
});
|
|
5362
5545
|
console.log(`\u2713 Scheduled cloud backup enabled`);
|
|
5363
5546
|
console.log(` Interval: every ${interval}`);
|
|
5547
|
+
if (opts.encrypt) console.log(` Encryption: enabled (AES-256-GCM)`);
|
|
5548
|
+
if (opts.includeSecrets) console.log(` Secrets: included`);
|
|
5364
5549
|
console.log(` Log: ${SCHEDULE_LOG}`);
|
|
5365
5550
|
trackCliEvent(cfg.profileId, "schedule_enabled", {
|
|
5366
5551
|
type: "cloud",
|
|
5367
|
-
interval_hours: parseInt(interval)
|
|
5552
|
+
interval_hours: parseInt(interval),
|
|
5553
|
+
encrypt: !!opts.encrypt,
|
|
5554
|
+
include_secrets: !!opts.includeSecrets
|
|
5368
5555
|
});
|
|
5369
5556
|
});
|
|
5370
5557
|
schedule.command("off").description("Disable scheduled cloud backups").action(async () => {
|
|
@@ -5395,6 +5582,8 @@ schedule.command("status").description("Show schedule status").action(async () =
|
|
|
5395
5582
|
if (localCfg?.maxSnapshots) console.log(` Max snapshots: ${localCfg.maxSnapshots}`);
|
|
5396
5583
|
if (localCfg?.includeMemoryDb) console.log(` Memory DB: included`);
|
|
5397
5584
|
if (localCfg?.includeSessions) console.log(` Sessions: included`);
|
|
5585
|
+
if (localCfg?.includeSecrets) console.log(` Secrets: included`);
|
|
5586
|
+
if (localCfg?.encrypt) console.log(` Encryption: enabled`);
|
|
5398
5587
|
console.log(` Cron: ${localEntry.trim()}`);
|
|
5399
5588
|
} else {
|
|
5400
5589
|
console.log("\u2717 Local schedule: inactive");
|
|
@@ -5405,6 +5594,8 @@ schedule.command("status").description("Show schedule status").action(async () =
|
|
|
5405
5594
|
const cloudCfg = cfg?.schedule?.cloud;
|
|
5406
5595
|
console.log("\u2713 Cloud schedule: active");
|
|
5407
5596
|
if (cloudCfg?.intervalHours) console.log(` Interval: every ${cloudCfg.intervalHours}h`);
|
|
5597
|
+
if (cloudCfg?.encrypt) console.log(` Encryption: enabled`);
|
|
5598
|
+
if (cloudCfg?.includeSecrets) console.log(` Secrets: included`);
|
|
5408
5599
|
console.log(` Cron: ${cloudEntry.trim()}`);
|
|
5409
5600
|
} else {
|
|
5410
5601
|
console.log("\u2717 Cloud schedule: inactive");
|
|
@@ -5462,17 +5653,22 @@ Total: ${files.length} files`);
|
|
|
5462
5653
|
});
|
|
5463
5654
|
var local = program.command("local").description("Local backup and restore (no cloud required)");
|
|
5464
5655
|
var localSchedule = local.command("schedule").description("Manage scheduled local backups");
|
|
5465
|
-
localSchedule.command("on").description("Enable scheduled local backups via cron").option("--every <interval>", "Backup interval: 1h, 6h, 12h, 24h", "12h").option("--max-snapshots <n>", "Keep only the N most recent local backups").option("--include-memory-db", "Include SQLite memory index").option("--include-sessions", "Include chat history (sessions)").action(async (opts) => {
|
|
5656
|
+
localSchedule.command("on").description("Enable scheduled local backups via cron").option("--every <interval>", "Backup interval: 1h, 6h, 12h, 24h", "12h").option("--max-snapshots <n>", "Keep only the N most recent local backups").option("--include-memory-db", "Include SQLite memory index").option("--include-sessions", "Include chat history (sessions)").option("--include-secrets", "Include credentials and auth files in backup").option("--encrypt", "Encrypt backups with AES-256-GCM").action(async (opts) => {
|
|
5466
5657
|
assertNotWindows();
|
|
5467
5658
|
const interval = opts.every;
|
|
5468
5659
|
if (!VALID_INTERVALS.includes(interval)) {
|
|
5469
5660
|
console.error(`\u2717 Invalid interval: ${interval}. Valid options: ${VALID_INTERVALS.join(", ")}`);
|
|
5470
5661
|
process.exit(1);
|
|
5471
5662
|
}
|
|
5663
|
+
if (opts.encrypt && !process.env.CLAWON_ENCRYPT_PASSPHRASE) {
|
|
5664
|
+
console.error("\u2717 --encrypt requires the CLAWON_ENCRYPT_PASSPHRASE environment variable for scheduled backups.");
|
|
5665
|
+
console.error(" Set it in your shell profile (~/.bashrc, ~/.zshrc) so cron can access it.");
|
|
5666
|
+
process.exit(1);
|
|
5667
|
+
}
|
|
5472
5668
|
const cfg = readConfig();
|
|
5473
5669
|
const wasEnabled = cfg?.schedule?.local?.enabled;
|
|
5474
5670
|
const cronExpr = INTERVAL_CRON[interval];
|
|
5475
|
-
const args = "local backup --scheduled" + (opts.includeMemoryDb ? " --include-memory-db" : "") + (opts.includeSessions ? " --include-sessions" : "") + (opts.maxSnapshots ? ` --max-snapshots ${opts.maxSnapshots}` : "");
|
|
5671
|
+
const args = "local backup --scheduled" + (opts.includeMemoryDb ? " --include-memory-db" : "") + (opts.includeSessions ? " --include-sessions" : "") + (opts.includeSecrets ? " --include-secrets" : "") + (opts.encrypt ? " --encrypt" : "") + (opts.maxSnapshots ? ` --max-snapshots ${opts.maxSnapshots}` : "");
|
|
5476
5672
|
const command = resolveCliCommand(args);
|
|
5477
5673
|
addCronEntry(CRON_MARKER_LOCAL, cronExpr, command);
|
|
5478
5674
|
const maxSnapshots = opts.maxSnapshots ? parseInt(opts.maxSnapshots, 10) : null;
|
|
@@ -5483,7 +5679,9 @@ localSchedule.command("on").description("Enable scheduled local backups via cron
|
|
|
5483
5679
|
intervalHours: parseInt(interval),
|
|
5484
5680
|
...maxSnapshots ? { maxSnapshots } : {},
|
|
5485
5681
|
...opts.includeMemoryDb ? { includeMemoryDb: true } : {},
|
|
5486
|
-
...opts.includeSessions ? { includeSessions: true } : {}
|
|
5682
|
+
...opts.includeSessions ? { includeSessions: true } : {},
|
|
5683
|
+
...opts.includeSecrets ? { includeSecrets: true } : {},
|
|
5684
|
+
...opts.encrypt ? { encrypt: true } : {}
|
|
5487
5685
|
}
|
|
5488
5686
|
}
|
|
5489
5687
|
});
|
|
@@ -5492,8 +5690,10 @@ localSchedule.command("on").description("Enable scheduled local backups via cron
|
|
|
5492
5690
|
if (maxSnapshots) console.log(` Max snapshots: ${maxSnapshots}`);
|
|
5493
5691
|
if (opts.includeMemoryDb) console.log(` Memory DB: included`);
|
|
5494
5692
|
if (opts.includeSessions) console.log(` Sessions: included`);
|
|
5693
|
+
if (opts.includeSecrets) console.log(` Secrets: included`);
|
|
5694
|
+
if (opts.encrypt) console.log(` Encryption: enabled (AES-256-GCM)`);
|
|
5495
5695
|
console.log(` Log: ${SCHEDULE_LOG}`);
|
|
5496
|
-
const firstRunArgs = "local backup" + (opts.includeMemoryDb ? " --include-memory-db" : "") + (opts.includeSessions ? " --include-sessions" : "") + (opts.maxSnapshots ? ` --max-snapshots ${opts.maxSnapshots}` : "");
|
|
5696
|
+
const firstRunArgs = "local backup" + (opts.includeMemoryDb ? " --include-memory-db" : "") + (opts.includeSessions ? " --include-sessions" : "") + (opts.includeSecrets ? " --include-secrets" : "") + (opts.encrypt ? " --encrypt" : "") + (opts.maxSnapshots ? ` --max-snapshots ${opts.maxSnapshots}` : "");
|
|
5497
5697
|
console.log("\nRunning first backup now...\n");
|
|
5498
5698
|
try {
|
|
5499
5699
|
execSync(resolveCliCommand(firstRunArgs), { stdio: "inherit" });
|
|
@@ -5505,7 +5705,9 @@ localSchedule.command("on").description("Enable scheduled local backups via cron
|
|
|
5505
5705
|
interval_hours: parseInt(interval),
|
|
5506
5706
|
max_snapshots: maxSnapshots,
|
|
5507
5707
|
include_memory_db: !!opts.includeMemoryDb,
|
|
5508
|
-
include_sessions: !!opts.includeSessions
|
|
5708
|
+
include_sessions: !!opts.includeSessions,
|
|
5709
|
+
include_secrets: !!opts.includeSecrets,
|
|
5710
|
+
encrypt: !!opts.encrypt
|
|
5509
5711
|
});
|
|
5510
5712
|
});
|
|
5511
5713
|
localSchedule.command("off").description("Disable scheduled local backups").action(async () => {
|
|
@@ -5524,18 +5726,31 @@ localSchedule.command("off").description("Disable scheduled local backups").acti
|
|
|
5524
5726
|
const cfg = readConfig();
|
|
5525
5727
|
trackCliEvent(cfg?.profileId || "anonymous", "schedule_disabled", { type: "local" });
|
|
5526
5728
|
});
|
|
5527
|
-
local.command("backup").description("Save a local backup of your OpenClaw workspace").option("--tag <label>", "Add a label to this backup").option("--include-memory-db", "Include SQLite memory index").option("--include-sessions", "Include chat history (sessions)").option("--scheduled", "Internal: triggered by cron (suppresses interactive output)").option("--no-secret-scan", "Skip secret scanning before backup").option("--max-snapshots <n>", "Keep only the N most recent local backups").action(async (opts) => {
|
|
5729
|
+
local.command("backup").description("Save a local backup of your OpenClaw workspace").option("--tag <label>", "Add a label to this backup").option("--include-memory-db", "Include SQLite memory index").option("--include-sessions", "Include chat history (sessions)").option("--include-secrets", "Include credentials and auth files in backup").option("--encrypt", "Encrypt the backup with AES-256-GCM").option("--scheduled", "Internal: triggered by cron (suppresses interactive output)").option("--no-secret-scan", "Skip secret scanning before backup").option("--max-snapshots <n>", "Keep only the N most recent local backups").action(async (opts) => {
|
|
5528
5730
|
if (!fs2.existsSync(OPENCLAW_DIR)) {
|
|
5529
5731
|
console.error(`\u2717 OpenClaw directory not found: ${OPENCLAW_DIR}`);
|
|
5530
5732
|
process.exit(1);
|
|
5531
5733
|
}
|
|
5532
5734
|
try {
|
|
5533
5735
|
if (!opts.scheduled) console.log("Discovering files...");
|
|
5534
|
-
let files = discoverFiles(OPENCLAW_DIR, !!opts.includeMemoryDb, !!opts.includeSessions);
|
|
5736
|
+
let files = discoverFiles(OPENCLAW_DIR, !!opts.includeMemoryDb, !!opts.includeSessions, !!opts.includeSecrets);
|
|
5535
5737
|
if (files.length === 0) {
|
|
5536
5738
|
console.error("\u2717 No files found to backup");
|
|
5537
5739
|
process.exit(1);
|
|
5538
5740
|
}
|
|
5741
|
+
if (opts.includeSecrets && !opts.scheduled) {
|
|
5742
|
+
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
5743
|
+
const confirmed = await new Promise((resolve) => {
|
|
5744
|
+
rl.question("\u26A0 --include-secrets will back up API keys, tokens, and auth files. Continue? [y/N] ", (answer) => {
|
|
5745
|
+
rl.close();
|
|
5746
|
+
resolve(answer.trim().toLowerCase() === "y");
|
|
5747
|
+
});
|
|
5748
|
+
});
|
|
5749
|
+
if (!confirmed) {
|
|
5750
|
+
console.log("Backup aborted.");
|
|
5751
|
+
process.exit(0);
|
|
5752
|
+
}
|
|
5753
|
+
}
|
|
5539
5754
|
let secretsFound = 0;
|
|
5540
5755
|
let secretsFilesSkipped = 0;
|
|
5541
5756
|
const scanSkipped = !opts.secretScan;
|
|
@@ -5575,20 +5790,31 @@ local.command("backup").description("Save a local backup of your OpenClaw worksp
|
|
|
5575
5790
|
const filename = `backup-${timestamp}.tar.gz`;
|
|
5576
5791
|
const filePath = path2.join(BACKUPS_DIR, filename);
|
|
5577
5792
|
if (!opts.scheduled) console.log("Creating archive...");
|
|
5578
|
-
await createLocalArchive(files, OPENCLAW_DIR, filePath, opts.tag, opts.includeMemoryDb, opts.includeSessions, opts.scheduled ? "scheduled" : "manual");
|
|
5579
|
-
|
|
5793
|
+
await createLocalArchive(files, OPENCLAW_DIR, filePath, opts.tag, opts.includeMemoryDb, opts.includeSessions, opts.includeSecrets, opts.scheduled ? "scheduled" : "manual", opts.encrypt);
|
|
5794
|
+
let finalPath = filePath;
|
|
5795
|
+
if (opts.encrypt) {
|
|
5796
|
+
const passphrase = await getPassphrase(!!opts.scheduled, "encrypt");
|
|
5797
|
+
if (!opts.scheduled) console.log("Encrypting archive...");
|
|
5798
|
+
const plaintext = fs2.readFileSync(filePath);
|
|
5799
|
+
const { encrypted } = encryptFile(plaintext, passphrase);
|
|
5800
|
+
finalPath = filePath + ".enc";
|
|
5801
|
+
fs2.writeFileSync(finalPath, encrypted);
|
|
5802
|
+
fs2.unlinkSync(filePath);
|
|
5803
|
+
}
|
|
5804
|
+
const archiveSize = fs2.statSync(finalPath).size;
|
|
5580
5805
|
if (!opts.scheduled) {
|
|
5581
5806
|
console.log(`
|
|
5582
5807
|
\u2713 Local backup saved!`);
|
|
5583
|
-
console.log(` File: ${
|
|
5808
|
+
console.log(` File: ${finalPath}`);
|
|
5584
5809
|
console.log(` Files: ${files.length}`);
|
|
5585
|
-
console.log(` Size: ${(archiveSize / 1024).toFixed(1)} KB (compressed)`);
|
|
5810
|
+
console.log(` Size: ${(archiveSize / 1024).toFixed(1)} KB (compressed${opts.encrypt ? " + encrypted" : ""})`);
|
|
5586
5811
|
if (opts.tag) console.log(` Tag: ${opts.tag}`);
|
|
5812
|
+
if (opts.encrypt) console.log(` Encrypted: yes (AES-256-GCM)`);
|
|
5587
5813
|
}
|
|
5588
5814
|
const maxSnapshots = opts.maxSnapshots ? parseInt(opts.maxSnapshots, 10) : null;
|
|
5589
5815
|
let rotatedCount = 0;
|
|
5590
5816
|
if (maxSnapshots && maxSnapshots > 0) {
|
|
5591
|
-
const allBackups = fs2.readdirSync(BACKUPS_DIR).filter((f) => f.endsWith(".tar.gz")).sort().reverse();
|
|
5817
|
+
const allBackups = fs2.readdirSync(BACKUPS_DIR).filter((f) => f.endsWith(".tar.gz") || f.endsWith(".tar.gz.enc")).sort().reverse();
|
|
5592
5818
|
if (allBackups.length > maxSnapshots) {
|
|
5593
5819
|
const toDelete = allBackups.slice(maxSnapshots);
|
|
5594
5820
|
for (const old of toDelete) {
|
|
@@ -5606,6 +5832,8 @@ local.command("backup").description("Save a local backup of your OpenClaw worksp
|
|
|
5606
5832
|
total_bytes: totalSize,
|
|
5607
5833
|
include_memory_db: !!opts.includeMemoryDb,
|
|
5608
5834
|
include_sessions: !!opts.includeSessions,
|
|
5835
|
+
include_secrets: !!opts.includeSecrets,
|
|
5836
|
+
encrypted: !!opts.encrypt,
|
|
5609
5837
|
type: "local",
|
|
5610
5838
|
trigger: opts.scheduled ? "scheduled" : "manual",
|
|
5611
5839
|
rotated_count: rotatedCount,
|
|
@@ -5629,7 +5857,7 @@ local.command("list").description("List local backups").action(async () => {
|
|
|
5629
5857
|
console.log("No local backups yet. Run: clawon local backup");
|
|
5630
5858
|
return;
|
|
5631
5859
|
}
|
|
5632
|
-
const entries = fs2.readdirSync(BACKUPS_DIR).filter((f) => f.endsWith(".tar.gz")).sort().reverse();
|
|
5860
|
+
const entries = fs2.readdirSync(BACKUPS_DIR).filter((f) => f.endsWith(".tar.gz") || f.endsWith(".tar.gz.enc")).sort().reverse();
|
|
5633
5861
|
if (entries.length === 0) {
|
|
5634
5862
|
console.log("No local backups yet. Run: clawon local backup");
|
|
5635
5863
|
return;
|
|
@@ -5639,17 +5867,26 @@ local.command("list").description("List local backups").action(async () => {
|
|
|
5639
5867
|
console.log("\u2500".repeat(120));
|
|
5640
5868
|
for (let i = 0; i < entries.length; i++) {
|
|
5641
5869
|
const filePath = path2.join(BACKUPS_DIR, entries[i]);
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
const date = new Date(meta.created).toLocaleString();
|
|
5870
|
+
const encrypted = isEncryptedArchive(filePath);
|
|
5871
|
+
if (encrypted) {
|
|
5645
5872
|
const archiveSize = fs2.statSync(filePath).size;
|
|
5646
5873
|
const sizeStr = `${(archiveSize / 1024).toFixed(1)} KB`;
|
|
5647
|
-
const tagStr = (meta.tag || "").padEnd(20);
|
|
5648
5874
|
console.log(
|
|
5649
|
-
`${String(i + 1).padStart(2)} |
|
|
5875
|
+
`${String(i + 1).padStart(2)} | [encrypted] | ??? | ${sizeStr.padEnd(10)} | [encrypted] | ${filePath}`
|
|
5650
5876
|
);
|
|
5651
|
-
}
|
|
5652
|
-
|
|
5877
|
+
} else {
|
|
5878
|
+
try {
|
|
5879
|
+
const meta = await readArchiveMeta(filePath);
|
|
5880
|
+
const date = new Date(meta.created).toLocaleString();
|
|
5881
|
+
const archiveSize = fs2.statSync(filePath).size;
|
|
5882
|
+
const sizeStr = `${(archiveSize / 1024).toFixed(1)} KB`;
|
|
5883
|
+
const tagStr = (meta.tag || "").padEnd(20);
|
|
5884
|
+
console.log(
|
|
5885
|
+
`${String(i + 1).padStart(2)} | ${date.padEnd(25)} | ${String(meta.file_count).padEnd(5)} | ${sizeStr.padEnd(10)} | ${tagStr} | ${filePath}`
|
|
5886
|
+
);
|
|
5887
|
+
} catch {
|
|
5888
|
+
console.log(`${String(i + 1).padStart(2)} | ${entries[i].padEnd(25)} | ??? | ??? | | ${filePath}`);
|
|
5889
|
+
}
|
|
5653
5890
|
}
|
|
5654
5891
|
}
|
|
5655
5892
|
console.log(`
|
|
@@ -5675,7 +5912,7 @@ local.command("restore").description("Restore from a local backup").option("--fi
|
|
|
5675
5912
|
console.error("\u2717 No local backups found. Run: clawon local backup");
|
|
5676
5913
|
process.exit(1);
|
|
5677
5914
|
}
|
|
5678
|
-
const entries = fs2.readdirSync(BACKUPS_DIR).filter((f) => f.endsWith(".tar.gz")).sort().reverse();
|
|
5915
|
+
const entries = fs2.readdirSync(BACKUPS_DIR).filter((f) => f.endsWith(".tar.gz") || f.endsWith(".tar.gz.enc")).sort().reverse();
|
|
5679
5916
|
if (entries.length === 0) {
|
|
5680
5917
|
console.error("\u2717 No local backups found. Run: clawon local backup");
|
|
5681
5918
|
process.exit(1);
|
|
@@ -5692,13 +5929,24 @@ local.command("restore").description("Restore from a local backup").option("--fi
|
|
|
5692
5929
|
}
|
|
5693
5930
|
}
|
|
5694
5931
|
console.log(`Restoring from: ${archivePath}`);
|
|
5932
|
+
let actualArchivePath = archivePath;
|
|
5933
|
+
const encrypted = isEncryptedArchive(archivePath);
|
|
5934
|
+
const tempDecryptedPath = path2.join(path2.dirname(archivePath), ".restore-tmp.tar.gz");
|
|
5695
5935
|
try {
|
|
5696
|
-
|
|
5936
|
+
if (encrypted) {
|
|
5937
|
+
console.log("Archive is encrypted. Decrypting...");
|
|
5938
|
+
const passphrase = await getPassphrase(false, "decrypt");
|
|
5939
|
+
const encryptedData = fs2.readFileSync(archivePath);
|
|
5940
|
+
const decrypted = decryptFile(encryptedData, passphrase);
|
|
5941
|
+
fs2.writeFileSync(tempDecryptedPath, decrypted);
|
|
5942
|
+
actualArchivePath = tempDecryptedPath;
|
|
5943
|
+
}
|
|
5944
|
+
const meta = await readArchiveMeta(actualArchivePath);
|
|
5697
5945
|
console.log(`Backup date: ${new Date(meta.created).toLocaleString()}`);
|
|
5698
5946
|
console.log(`Files: ${meta.file_count}`);
|
|
5699
5947
|
if (meta.tag) console.log(`Tag: ${meta.tag}`);
|
|
5700
5948
|
console.log("\nExtracting...");
|
|
5701
|
-
await extractLocalArchive(
|
|
5949
|
+
await extractLocalArchive(actualArchivePath, OPENCLAW_DIR);
|
|
5702
5950
|
console.log(`
|
|
5703
5951
|
\u2713 Restore complete!`);
|
|
5704
5952
|
console.log(` Restored to: ${OPENCLAW_DIR}`);
|
|
@@ -5706,12 +5954,17 @@ local.command("restore").description("Restore from a local backup").option("--fi
|
|
|
5706
5954
|
const cfg = readConfig();
|
|
5707
5955
|
trackCliEvent(cfg?.profileId || "anonymous", "local_backup_restored", {
|
|
5708
5956
|
file_count: meta.file_count,
|
|
5709
|
-
source: opts.file ? "file" : "local"
|
|
5957
|
+
source: opts.file ? "file" : "local",
|
|
5958
|
+
encrypted
|
|
5710
5959
|
});
|
|
5711
5960
|
} catch (e) {
|
|
5712
5961
|
console.error(`
|
|
5713
5962
|
\u2717 Restore failed: ${e.message}`);
|
|
5714
5963
|
process.exit(1);
|
|
5964
|
+
} finally {
|
|
5965
|
+
if (encrypted && fs2.existsSync(tempDecryptedPath)) {
|
|
5966
|
+
fs2.unlinkSync(tempDecryptedPath);
|
|
5967
|
+
}
|
|
5715
5968
|
}
|
|
5716
5969
|
});
|
|
5717
5970
|
var workspaces = program.command("workspaces").description("Manage workspaces");
|