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.
Files changed (3) hide show
  1. package/README.md +50 -7
  2. package/dist/index.js +299 -46
  3. 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 never leave your machine.** The entire `credentials/` directory and `openclaw.json` are excluded by default. You can verify this by running `npx clawon discover` before any backup.
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 and extract them with standard tools:
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 readline from "readline";
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
- function shouldInclude(relativePath, includeMemoryDb = false, includeSessions = false) {
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 = readline.createInterface({ input: process.stdin, output: process.stdout });
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
- const content = fs2.readFileSync(fullPath);
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
- const content = Buffer.from(await res.arrayBuffer());
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 command = resolveCliCommand(`backup --scheduled`);
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
- const archiveSize = fs2.statSync(filePath).size;
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: ${filePath}`);
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
- try {
5643
- const meta = await readArchiveMeta(filePath);
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)} | ${date.padEnd(25)} | ${String(meta.file_count).padEnd(5)} | ${sizeStr.padEnd(10)} | ${tagStr} | ${filePath}`
5875
+ `${String(i + 1).padStart(2)} | [encrypted] | ??? | ${sizeStr.padEnd(10)} | [encrypted] | ${filePath}`
5650
5876
  );
5651
- } catch {
5652
- console.log(`${String(i + 1).padStart(2)} | ${entries[i].padEnd(25)} | ??? | ??? | | ${filePath}`);
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
- const meta = await readArchiveMeta(archivePath);
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(archivePath, OPENCLAW_DIR);
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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawon",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "Backup and restore your OpenClaw workspace",
5
5
  "type": "module",
6
6
  "bin": {