clawon 0.1.18 → 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 +45 -5
- package/dist/index.js +260 -40
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,13 +24,15 @@ 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
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
|
|
27
29
|
npx clawon local backup --no-secret-scan # Skip secret scanning
|
|
28
30
|
npx clawon local backup --max-snapshots 10 # Keep only 10 most recent
|
|
29
31
|
|
|
30
32
|
# List all local backups
|
|
31
33
|
npx clawon local list
|
|
32
34
|
|
|
33
|
-
# Restore
|
|
35
|
+
# Restore (detects encryption automatically)
|
|
34
36
|
npx clawon local restore # Latest backup
|
|
35
37
|
npx clawon local restore --pick 2 # Backup #2 from list
|
|
36
38
|
npx clawon local restore --file path.tar.gz # External file
|
|
@@ -47,12 +49,15 @@ npx clawon local schedule on --every 6h --max-snapshots 10
|
|
|
47
49
|
npx clawon local schedule on --include-memory-db
|
|
48
50
|
npx clawon local schedule on --include-sessions
|
|
49
51
|
npx clawon local schedule on --include-secrets
|
|
52
|
+
npx clawon local schedule on --encrypt # Requires CLAWON_ENCRYPT_PASSPHRASE env var
|
|
50
53
|
|
|
51
54
|
# Disable local schedule
|
|
52
55
|
npx clawon local schedule off
|
|
53
56
|
|
|
54
57
|
# Schedule cloud backups (requires Hobby or Pro account)
|
|
55
58
|
npx clawon schedule on
|
|
59
|
+
npx clawon schedule on --encrypt # Encrypted cloud backups
|
|
60
|
+
npx clawon schedule on --encrypt --include-secrets # With secrets
|
|
56
61
|
npx clawon schedule off
|
|
57
62
|
|
|
58
63
|
# Check schedule status
|
|
@@ -96,11 +101,13 @@ npx clawon backup --dry-run # Preview without uploading
|
|
|
96
101
|
npx clawon backup --include-memory-db # Requires Hobby or Pro
|
|
97
102
|
npx clawon backup --include-sessions # Requires Hobby or Pro
|
|
98
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
|
|
99
106
|
|
|
100
107
|
# List cloud backups
|
|
101
108
|
npx clawon list
|
|
102
109
|
|
|
103
|
-
# Restore from cloud
|
|
110
|
+
# Restore from cloud (decrypts automatically if encrypted)
|
|
104
111
|
npx clawon restore
|
|
105
112
|
npx clawon restore --snapshot <id> # Specific snapshot
|
|
106
113
|
npx clawon restore --dry-run # Preview without extracting
|
|
@@ -160,7 +167,7 @@ These are **always excluded**, even if they match an include pattern:
|
|
|
160
167
|
| `*.lock`, `*.wal`, `*.shm` | Database lock files |
|
|
161
168
|
| `node_modules/**` | Dependencies |
|
|
162
169
|
|
|
163
|
-
**Credentials are excluded by default.** The `credentials/` directory, `openclaw.json`, and agent auth files are excluded unless you use `--include-secrets
|
|
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`.
|
|
164
171
|
|
|
165
172
|
## Secret Scanning
|
|
166
173
|
|
|
@@ -181,9 +188,42 @@ npx clawon local backup --no-secret-scan # Disable scanning for a backup
|
|
|
181
188
|
npx clawon backup --no-secret-scan # Same for cloud backups
|
|
182
189
|
```
|
|
183
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
|
+
|
|
184
224
|
## Archive Format
|
|
185
225
|
|
|
186
|
-
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:
|
|
187
227
|
|
|
188
228
|
```bash
|
|
189
229
|
# List contents
|
|
@@ -205,7 +245,7 @@ Each archive contains:
|
|
|
205
245
|
| | Local | Cloud |
|
|
206
246
|
|---|---|---|
|
|
207
247
|
| **Location** | `~/.clawon/backups/` | Clawon servers (Supabase Storage) |
|
|
208
|
-
| **Format** | `.tar.gz` | Individual files with signed URLs |
|
|
248
|
+
| **Format** | `.tar.gz` (or `.tar.gz.enc` if encrypted) | Individual files with signed URLs |
|
|
209
249
|
| **Limit** | Unlimited | 2 snapshots (Starter), more on paid plans |
|
|
210
250
|
| **Account required** | No | Yes |
|
|
211
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");
|
|
@@ -4775,7 +4895,7 @@ function discoverFiles(baseDir, includeMemoryDb = false, includeSessions = false
|
|
|
4775
4895
|
walk(baseDir);
|
|
4776
4896
|
return files;
|
|
4777
4897
|
}
|
|
4778
|
-
async function createLocalArchive(files, openclawDir, outputPath, tag, includeMemoryDb, includeSessions, includeSecrets, trigger) {
|
|
4898
|
+
async function createLocalArchive(files, openclawDir, outputPath, tag, includeMemoryDb, includeSessions, includeSecrets, trigger, encrypted) {
|
|
4779
4899
|
const meta = {
|
|
4780
4900
|
version: 2,
|
|
4781
4901
|
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -4784,6 +4904,7 @@ async function createLocalArchive(files, openclawDir, outputPath, tag, includeMe
|
|
|
4784
4904
|
...includeMemoryDb ? { include_memory_db: true } : {},
|
|
4785
4905
|
...includeSessions ? { include_sessions: true } : {},
|
|
4786
4906
|
...includeSecrets ? { include_secrets: true } : {},
|
|
4907
|
+
...encrypted ? { encrypted: true } : {},
|
|
4787
4908
|
...trigger ? { trigger } : {}
|
|
4788
4909
|
};
|
|
4789
4910
|
const metaPath = path2.join(openclawDir, "_clawon_meta.json");
|
|
@@ -4845,7 +4966,7 @@ async function promptSecretAction(findings) {
|
|
|
4845
4966
|
console.log(` [a] Abort backup`);
|
|
4846
4967
|
console.log(` [i] Ignore warnings and backup all files`);
|
|
4847
4968
|
console.log("");
|
|
4848
|
-
const rl =
|
|
4969
|
+
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
4849
4970
|
return new Promise((resolve) => {
|
|
4850
4971
|
rl.question(" Choice (s/a/i): ", (answer) => {
|
|
4851
4972
|
rl.close();
|
|
@@ -4953,9 +5074,11 @@ program.command("login").description("Connect to Clawon with your API key").opti
|
|
|
4953
5074
|
process.exit(1);
|
|
4954
5075
|
}
|
|
4955
5076
|
});
|
|
4956
|
-
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("--scheduled", "Internal: triggered by cron (suppresses interactive output)").option("--no-secret-scan", "Skip secret scanning before backup").action(async (opts) => {
|
|
4957
|
-
if (opts.includeSecrets) {
|
|
4958
|
-
console.error("--include-secrets
|
|
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");
|
|
4959
5082
|
process.exit(1);
|
|
4960
5083
|
}
|
|
4961
5084
|
const cfg = readConfig();
|
|
@@ -4973,7 +5096,7 @@ program.command("backup").description("Backup your OpenClaw workspace to the clo
|
|
|
4973
5096
|
process.exit(1);
|
|
4974
5097
|
}
|
|
4975
5098
|
console.log("Discovering files...");
|
|
4976
|
-
let files = discoverFiles(OPENCLAW_DIR, !!opts.includeMemoryDb, !!opts.includeSessions);
|
|
5099
|
+
let files = discoverFiles(OPENCLAW_DIR, !!opts.includeMemoryDb, !!opts.includeSessions, !!(opts.includeSecrets && opts.encrypt));
|
|
4977
5100
|
if (files.length === 0) {
|
|
4978
5101
|
console.error("\u2717 No files found to backup");
|
|
4979
5102
|
process.exit(1);
|
|
@@ -5026,6 +5149,15 @@ program.command("backup").description("Backup your OpenClaw workspace to the clo
|
|
|
5026
5149
|
return;
|
|
5027
5150
|
}
|
|
5028
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
|
+
}
|
|
5029
5161
|
console.log("\nCreating backup...");
|
|
5030
5162
|
const { snapshotId, uploadUrls } = await api(
|
|
5031
5163
|
cfg.apiBaseUrl,
|
|
@@ -5036,14 +5168,21 @@ program.command("backup").description("Backup your OpenClaw workspace to the clo
|
|
|
5036
5168
|
profileId: cfg.profileId,
|
|
5037
5169
|
workspaceId: cfg.workspaceId,
|
|
5038
5170
|
files: files.map((f) => ({ path: f.path, size: f.size })),
|
|
5039
|
-
...opts.tag ? { tag: opts.tag } : {}
|
|
5171
|
+
...opts.tag ? { tag: opts.tag } : {},
|
|
5172
|
+
...encryptionSaltHex ? { encryption: { method: "aes-256-gcm", salt: encryptionSaltHex } } : {}
|
|
5040
5173
|
}
|
|
5041
5174
|
);
|
|
5042
5175
|
console.log(`Uploading ${files.length} files...`);
|
|
5043
5176
|
let uploaded = 0;
|
|
5177
|
+
const fileIvs = {};
|
|
5044
5178
|
for (const file of files) {
|
|
5045
5179
|
const fullPath = path2.join(OPENCLAW_DIR, file.path);
|
|
5046
|
-
|
|
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
|
+
}
|
|
5047
5186
|
const uploadRes = await fetch(uploadUrls[file.path], {
|
|
5048
5187
|
method: "PUT",
|
|
5049
5188
|
headers: { "content-type": "application/octet-stream" },
|
|
@@ -5059,17 +5198,21 @@ program.command("backup").description("Backup your OpenClaw workspace to the clo
|
|
|
5059
5198
|
console.log("");
|
|
5060
5199
|
await api(cfg.apiBaseUrl, "/api/v1/backups/confirm", "POST", cfg.apiKey, {
|
|
5061
5200
|
snapshotId,
|
|
5062
|
-
profileId: cfg.profileId
|
|
5201
|
+
profileId: cfg.profileId,
|
|
5202
|
+
...Object.keys(fileIvs).length > 0 ? { fileIvs } : {}
|
|
5063
5203
|
});
|
|
5064
5204
|
console.log("\n\u2713 Backup complete!");
|
|
5065
5205
|
console.log(` Snapshot ID: ${snapshotId}`);
|
|
5066
5206
|
console.log(` Files: ${files.length}`);
|
|
5067
5207
|
console.log(` Size: ${(totalSize / 1024).toFixed(1)} KB`);
|
|
5208
|
+
if (opts.encrypt) console.log(` Encrypted: yes (AES-256-GCM)`);
|
|
5068
5209
|
trackCliEvent(cfg.profileId, opts.scheduled ? "scheduled_backup_created" : "cloud_backup_created", {
|
|
5069
5210
|
file_count: files.length,
|
|
5070
5211
|
total_bytes: totalSize,
|
|
5071
5212
|
include_memory_db: !!opts.includeMemoryDb,
|
|
5072
5213
|
include_sessions: !!opts.includeSessions,
|
|
5214
|
+
include_secrets: !!opts.includeSecrets,
|
|
5215
|
+
encrypted: !!opts.encrypt,
|
|
5073
5216
|
type: "cloud",
|
|
5074
5217
|
workspace_slug: cfg.workspaceSlug,
|
|
5075
5218
|
trigger: opts.scheduled ? "scheduled" : "manual",
|
|
@@ -5103,7 +5246,7 @@ program.command("restore").description("Restore your OpenClaw workspace from the
|
|
|
5103
5246
|
}
|
|
5104
5247
|
try {
|
|
5105
5248
|
console.log("Fetching backup...");
|
|
5106
|
-
const { snapshot, files, downloadUrls } = await api(
|
|
5249
|
+
const { snapshot, files, downloadUrls, encryption } = await api(
|
|
5107
5250
|
cfg.apiBaseUrl,
|
|
5108
5251
|
"/api/v1/backups/download",
|
|
5109
5252
|
"POST",
|
|
@@ -5117,11 +5260,19 @@ program.command("restore").description("Restore your OpenClaw workspace from the
|
|
|
5117
5260
|
console.log(`Found backup from ${new Date(snapshot.created_at).toLocaleString()}`);
|
|
5118
5261
|
console.log(` Files: ${files.length}`);
|
|
5119
5262
|
console.log(` Size: ${(totalSize / 1024).toFixed(1)} KB`);
|
|
5263
|
+
if (encryption) console.log(` Encrypted: yes (${encryption.method})`);
|
|
5120
5264
|
if (opts.dryRun) {
|
|
5121
5265
|
console.log("\n[Dry run] Files that would be restored:");
|
|
5122
5266
|
files.forEach((f) => console.log(` ${f.path}`));
|
|
5123
5267
|
return;
|
|
5124
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
|
+
}
|
|
5125
5276
|
console.log("\nDownloading files...");
|
|
5126
5277
|
let downloaded = 0;
|
|
5127
5278
|
for (const file of files) {
|
|
@@ -5129,7 +5280,10 @@ program.command("restore").description("Restore your OpenClaw workspace from the
|
|
|
5129
5280
|
if (!res.ok) {
|
|
5130
5281
|
throw new Error(`Failed to download ${file.path}: ${res.status}`);
|
|
5131
5282
|
}
|
|
5132
|
-
|
|
5283
|
+
let content = Buffer.from(await res.arrayBuffer());
|
|
5284
|
+
if (encryptionKey && file.iv) {
|
|
5285
|
+
content = decryptCloudFile(content, encryptionKey, file.iv);
|
|
5286
|
+
}
|
|
5133
5287
|
const targetPath = path2.join(OPENCLAW_DIR, file.path);
|
|
5134
5288
|
ensureDir(path2.dirname(targetPath));
|
|
5135
5289
|
fs2.writeFileSync(targetPath, content);
|
|
@@ -5334,7 +5488,7 @@ Total: ${files.length} files (${(totalSize / 1024).toFixed(1)} KB)`);
|
|
|
5334
5488
|
trackCliEvent(cfg?.profileId || "anonymous", "cli_discover", { file_count: files.length, include_memory_db: !!opts.includeMemoryDb, include_sessions: !!opts.includeSessions, include_secrets: !!opts.includeSecrets });
|
|
5335
5489
|
});
|
|
5336
5490
|
var schedule = program.command("schedule").description("Manage scheduled cloud backups");
|
|
5337
|
-
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) => {
|
|
5338
5492
|
assertNotWindows();
|
|
5339
5493
|
const cfg = readConfig();
|
|
5340
5494
|
if (!cfg) {
|
|
@@ -5346,6 +5500,16 @@ schedule.command("on").description("Enable scheduled cloud backups via cron").op
|
|
|
5346
5500
|
console.error(`\u2717 Invalid interval: ${interval}. Valid options: ${VALID_INTERVALS.join(", ")}`);
|
|
5347
5501
|
process.exit(1);
|
|
5348
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
|
+
}
|
|
5349
5513
|
try {
|
|
5350
5514
|
const data = await api(
|
|
5351
5515
|
cfg.apiBaseUrl || "https://clawon.io",
|
|
@@ -5365,22 +5529,29 @@ schedule.command("on").description("Enable scheduled cloud backups via cron").op
|
|
|
5365
5529
|
process.exit(1);
|
|
5366
5530
|
}
|
|
5367
5531
|
const cronExpr = INTERVAL_CRON[interval];
|
|
5368
|
-
const
|
|
5532
|
+
const scheduledArgs = "backup --scheduled" + (opts.encrypt ? " --encrypt" : "") + (opts.includeSecrets ? " --include-secrets" : "");
|
|
5533
|
+
const command = resolveCliCommand(scheduledArgs);
|
|
5369
5534
|
addCronEntry(CRON_MARKER_CLOUD, cronExpr, command);
|
|
5370
5535
|
updateConfig({
|
|
5371
5536
|
schedule: {
|
|
5372
5537
|
cloud: {
|
|
5373
5538
|
enabled: true,
|
|
5374
|
-
intervalHours: parseInt(interval)
|
|
5539
|
+
intervalHours: parseInt(interval),
|
|
5540
|
+
...opts.encrypt ? { encrypt: true } : {},
|
|
5541
|
+
...opts.includeSecrets ? { includeSecrets: true } : {}
|
|
5375
5542
|
}
|
|
5376
5543
|
}
|
|
5377
5544
|
});
|
|
5378
5545
|
console.log(`\u2713 Scheduled cloud backup enabled`);
|
|
5379
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`);
|
|
5380
5549
|
console.log(` Log: ${SCHEDULE_LOG}`);
|
|
5381
5550
|
trackCliEvent(cfg.profileId, "schedule_enabled", {
|
|
5382
5551
|
type: "cloud",
|
|
5383
|
-
interval_hours: parseInt(interval)
|
|
5552
|
+
interval_hours: parseInt(interval),
|
|
5553
|
+
encrypt: !!opts.encrypt,
|
|
5554
|
+
include_secrets: !!opts.includeSecrets
|
|
5384
5555
|
});
|
|
5385
5556
|
});
|
|
5386
5557
|
schedule.command("off").description("Disable scheduled cloud backups").action(async () => {
|
|
@@ -5411,6 +5582,8 @@ schedule.command("status").description("Show schedule status").action(async () =
|
|
|
5411
5582
|
if (localCfg?.maxSnapshots) console.log(` Max snapshots: ${localCfg.maxSnapshots}`);
|
|
5412
5583
|
if (localCfg?.includeMemoryDb) console.log(` Memory DB: included`);
|
|
5413
5584
|
if (localCfg?.includeSessions) console.log(` Sessions: included`);
|
|
5585
|
+
if (localCfg?.includeSecrets) console.log(` Secrets: included`);
|
|
5586
|
+
if (localCfg?.encrypt) console.log(` Encryption: enabled`);
|
|
5414
5587
|
console.log(` Cron: ${localEntry.trim()}`);
|
|
5415
5588
|
} else {
|
|
5416
5589
|
console.log("\u2717 Local schedule: inactive");
|
|
@@ -5421,6 +5594,8 @@ schedule.command("status").description("Show schedule status").action(async () =
|
|
|
5421
5594
|
const cloudCfg = cfg?.schedule?.cloud;
|
|
5422
5595
|
console.log("\u2713 Cloud schedule: active");
|
|
5423
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`);
|
|
5424
5599
|
console.log(` Cron: ${cloudEntry.trim()}`);
|
|
5425
5600
|
} else {
|
|
5426
5601
|
console.log("\u2717 Cloud schedule: inactive");
|
|
@@ -5478,17 +5653,22 @@ Total: ${files.length} files`);
|
|
|
5478
5653
|
});
|
|
5479
5654
|
var local = program.command("local").description("Local backup and restore (no cloud required)");
|
|
5480
5655
|
var localSchedule = local.command("schedule").description("Manage scheduled local backups");
|
|
5481
|
-
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").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) => {
|
|
5482
5657
|
assertNotWindows();
|
|
5483
5658
|
const interval = opts.every;
|
|
5484
5659
|
if (!VALID_INTERVALS.includes(interval)) {
|
|
5485
5660
|
console.error(`\u2717 Invalid interval: ${interval}. Valid options: ${VALID_INTERVALS.join(", ")}`);
|
|
5486
5661
|
process.exit(1);
|
|
5487
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
|
+
}
|
|
5488
5668
|
const cfg = readConfig();
|
|
5489
5669
|
const wasEnabled = cfg?.schedule?.local?.enabled;
|
|
5490
5670
|
const cronExpr = INTERVAL_CRON[interval];
|
|
5491
|
-
const args = "local backup --scheduled" + (opts.includeMemoryDb ? " --include-memory-db" : "") + (opts.includeSessions ? " --include-sessions" : "") + (opts.includeSecrets ? " --include-secrets" : "") + (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}` : "");
|
|
5492
5672
|
const command = resolveCliCommand(args);
|
|
5493
5673
|
addCronEntry(CRON_MARKER_LOCAL, cronExpr, command);
|
|
5494
5674
|
const maxSnapshots = opts.maxSnapshots ? parseInt(opts.maxSnapshots, 10) : null;
|
|
@@ -5500,7 +5680,8 @@ localSchedule.command("on").description("Enable scheduled local backups via cron
|
|
|
5500
5680
|
...maxSnapshots ? { maxSnapshots } : {},
|
|
5501
5681
|
...opts.includeMemoryDb ? { includeMemoryDb: true } : {},
|
|
5502
5682
|
...opts.includeSessions ? { includeSessions: true } : {},
|
|
5503
|
-
...opts.includeSecrets ? { includeSecrets: true } : {}
|
|
5683
|
+
...opts.includeSecrets ? { includeSecrets: true } : {},
|
|
5684
|
+
...opts.encrypt ? { encrypt: true } : {}
|
|
5504
5685
|
}
|
|
5505
5686
|
}
|
|
5506
5687
|
});
|
|
@@ -5510,8 +5691,9 @@ localSchedule.command("on").description("Enable scheduled local backups via cron
|
|
|
5510
5691
|
if (opts.includeMemoryDb) console.log(` Memory DB: included`);
|
|
5511
5692
|
if (opts.includeSessions) console.log(` Sessions: included`);
|
|
5512
5693
|
if (opts.includeSecrets) console.log(` Secrets: included`);
|
|
5694
|
+
if (opts.encrypt) console.log(` Encryption: enabled (AES-256-GCM)`);
|
|
5513
5695
|
console.log(` Log: ${SCHEDULE_LOG}`);
|
|
5514
|
-
const firstRunArgs = "local backup" + (opts.includeMemoryDb ? " --include-memory-db" : "") + (opts.includeSessions ? " --include-sessions" : "") + (opts.includeSecrets ? " --include-secrets" : "") + (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}` : "");
|
|
5515
5697
|
console.log("\nRunning first backup now...\n");
|
|
5516
5698
|
try {
|
|
5517
5699
|
execSync(resolveCliCommand(firstRunArgs), { stdio: "inherit" });
|
|
@@ -5524,7 +5706,8 @@ localSchedule.command("on").description("Enable scheduled local backups via cron
|
|
|
5524
5706
|
max_snapshots: maxSnapshots,
|
|
5525
5707
|
include_memory_db: !!opts.includeMemoryDb,
|
|
5526
5708
|
include_sessions: !!opts.includeSessions,
|
|
5527
|
-
include_secrets: !!opts.includeSecrets
|
|
5709
|
+
include_secrets: !!opts.includeSecrets,
|
|
5710
|
+
encrypt: !!opts.encrypt
|
|
5528
5711
|
});
|
|
5529
5712
|
});
|
|
5530
5713
|
localSchedule.command("off").description("Disable scheduled local backups").action(async () => {
|
|
@@ -5543,7 +5726,7 @@ localSchedule.command("off").description("Disable scheduled local backups").acti
|
|
|
5543
5726
|
const cfg = readConfig();
|
|
5544
5727
|
trackCliEvent(cfg?.profileId || "anonymous", "schedule_disabled", { type: "local" });
|
|
5545
5728
|
});
|
|
5546
|
-
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("--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) => {
|
|
5547
5730
|
if (!fs2.existsSync(OPENCLAW_DIR)) {
|
|
5548
5731
|
console.error(`\u2717 OpenClaw directory not found: ${OPENCLAW_DIR}`);
|
|
5549
5732
|
process.exit(1);
|
|
@@ -5556,7 +5739,7 @@ local.command("backup").description("Save a local backup of your OpenClaw worksp
|
|
|
5556
5739
|
process.exit(1);
|
|
5557
5740
|
}
|
|
5558
5741
|
if (opts.includeSecrets && !opts.scheduled) {
|
|
5559
|
-
const rl =
|
|
5742
|
+
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
5560
5743
|
const confirmed = await new Promise((resolve) => {
|
|
5561
5744
|
rl.question("\u26A0 --include-secrets will back up API keys, tokens, and auth files. Continue? [y/N] ", (answer) => {
|
|
5562
5745
|
rl.close();
|
|
@@ -5607,20 +5790,31 @@ local.command("backup").description("Save a local backup of your OpenClaw worksp
|
|
|
5607
5790
|
const filename = `backup-${timestamp}.tar.gz`;
|
|
5608
5791
|
const filePath = path2.join(BACKUPS_DIR, filename);
|
|
5609
5792
|
if (!opts.scheduled) console.log("Creating archive...");
|
|
5610
|
-
await createLocalArchive(files, OPENCLAW_DIR, filePath, opts.tag, opts.includeMemoryDb, opts.includeSessions, opts.includeSecrets, opts.scheduled ? "scheduled" : "manual");
|
|
5611
|
-
|
|
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;
|
|
5612
5805
|
if (!opts.scheduled) {
|
|
5613
5806
|
console.log(`
|
|
5614
5807
|
\u2713 Local backup saved!`);
|
|
5615
|
-
console.log(` File: ${
|
|
5808
|
+
console.log(` File: ${finalPath}`);
|
|
5616
5809
|
console.log(` Files: ${files.length}`);
|
|
5617
|
-
console.log(` Size: ${(archiveSize / 1024).toFixed(1)} KB (compressed)`);
|
|
5810
|
+
console.log(` Size: ${(archiveSize / 1024).toFixed(1)} KB (compressed${opts.encrypt ? " + encrypted" : ""})`);
|
|
5618
5811
|
if (opts.tag) console.log(` Tag: ${opts.tag}`);
|
|
5812
|
+
if (opts.encrypt) console.log(` Encrypted: yes (AES-256-GCM)`);
|
|
5619
5813
|
}
|
|
5620
5814
|
const maxSnapshots = opts.maxSnapshots ? parseInt(opts.maxSnapshots, 10) : null;
|
|
5621
5815
|
let rotatedCount = 0;
|
|
5622
5816
|
if (maxSnapshots && maxSnapshots > 0) {
|
|
5623
|
-
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();
|
|
5624
5818
|
if (allBackups.length > maxSnapshots) {
|
|
5625
5819
|
const toDelete = allBackups.slice(maxSnapshots);
|
|
5626
5820
|
for (const old of toDelete) {
|
|
@@ -5639,6 +5833,7 @@ local.command("backup").description("Save a local backup of your OpenClaw worksp
|
|
|
5639
5833
|
include_memory_db: !!opts.includeMemoryDb,
|
|
5640
5834
|
include_sessions: !!opts.includeSessions,
|
|
5641
5835
|
include_secrets: !!opts.includeSecrets,
|
|
5836
|
+
encrypted: !!opts.encrypt,
|
|
5642
5837
|
type: "local",
|
|
5643
5838
|
trigger: opts.scheduled ? "scheduled" : "manual",
|
|
5644
5839
|
rotated_count: rotatedCount,
|
|
@@ -5662,7 +5857,7 @@ local.command("list").description("List local backups").action(async () => {
|
|
|
5662
5857
|
console.log("No local backups yet. Run: clawon local backup");
|
|
5663
5858
|
return;
|
|
5664
5859
|
}
|
|
5665
|
-
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();
|
|
5666
5861
|
if (entries.length === 0) {
|
|
5667
5862
|
console.log("No local backups yet. Run: clawon local backup");
|
|
5668
5863
|
return;
|
|
@@ -5672,17 +5867,26 @@ local.command("list").description("List local backups").action(async () => {
|
|
|
5672
5867
|
console.log("\u2500".repeat(120));
|
|
5673
5868
|
for (let i = 0; i < entries.length; i++) {
|
|
5674
5869
|
const filePath = path2.join(BACKUPS_DIR, entries[i]);
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
const date = new Date(meta.created).toLocaleString();
|
|
5870
|
+
const encrypted = isEncryptedArchive(filePath);
|
|
5871
|
+
if (encrypted) {
|
|
5678
5872
|
const archiveSize = fs2.statSync(filePath).size;
|
|
5679
5873
|
const sizeStr = `${(archiveSize / 1024).toFixed(1)} KB`;
|
|
5680
|
-
const tagStr = (meta.tag || "").padEnd(20);
|
|
5681
5874
|
console.log(
|
|
5682
|
-
`${String(i + 1).padStart(2)} |
|
|
5875
|
+
`${String(i + 1).padStart(2)} | [encrypted] | ??? | ${sizeStr.padEnd(10)} | [encrypted] | ${filePath}`
|
|
5683
5876
|
);
|
|
5684
|
-
}
|
|
5685
|
-
|
|
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
|
+
}
|
|
5686
5890
|
}
|
|
5687
5891
|
}
|
|
5688
5892
|
console.log(`
|
|
@@ -5708,7 +5912,7 @@ local.command("restore").description("Restore from a local backup").option("--fi
|
|
|
5708
5912
|
console.error("\u2717 No local backups found. Run: clawon local backup");
|
|
5709
5913
|
process.exit(1);
|
|
5710
5914
|
}
|
|
5711
|
-
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();
|
|
5712
5916
|
if (entries.length === 0) {
|
|
5713
5917
|
console.error("\u2717 No local backups found. Run: clawon local backup");
|
|
5714
5918
|
process.exit(1);
|
|
@@ -5725,13 +5929,24 @@ local.command("restore").description("Restore from a local backup").option("--fi
|
|
|
5725
5929
|
}
|
|
5726
5930
|
}
|
|
5727
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");
|
|
5728
5935
|
try {
|
|
5729
|
-
|
|
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);
|
|
5730
5945
|
console.log(`Backup date: ${new Date(meta.created).toLocaleString()}`);
|
|
5731
5946
|
console.log(`Files: ${meta.file_count}`);
|
|
5732
5947
|
if (meta.tag) console.log(`Tag: ${meta.tag}`);
|
|
5733
5948
|
console.log("\nExtracting...");
|
|
5734
|
-
await extractLocalArchive(
|
|
5949
|
+
await extractLocalArchive(actualArchivePath, OPENCLAW_DIR);
|
|
5735
5950
|
console.log(`
|
|
5736
5951
|
\u2713 Restore complete!`);
|
|
5737
5952
|
console.log(` Restored to: ${OPENCLAW_DIR}`);
|
|
@@ -5739,12 +5954,17 @@ local.command("restore").description("Restore from a local backup").option("--fi
|
|
|
5739
5954
|
const cfg = readConfig();
|
|
5740
5955
|
trackCliEvent(cfg?.profileId || "anonymous", "local_backup_restored", {
|
|
5741
5956
|
file_count: meta.file_count,
|
|
5742
|
-
source: opts.file ? "file" : "local"
|
|
5957
|
+
source: opts.file ? "file" : "local",
|
|
5958
|
+
encrypted
|
|
5743
5959
|
});
|
|
5744
5960
|
} catch (e) {
|
|
5745
5961
|
console.error(`
|
|
5746
5962
|
\u2717 Restore failed: ${e.message}`);
|
|
5747
5963
|
process.exit(1);
|
|
5964
|
+
} finally {
|
|
5965
|
+
if (encrypted && fs2.existsSync(tempDecryptedPath)) {
|
|
5966
|
+
fs2.unlinkSync(tempDecryptedPath);
|
|
5967
|
+
}
|
|
5748
5968
|
}
|
|
5749
5969
|
});
|
|
5750
5970
|
var workspaces = program.command("workspaces").description("Manage workspaces");
|