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.
Files changed (3) hide show
  1. package/README.md +45 -5
  2. package/dist/index.js +260 -40
  3. 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` (local backups only). You can verify what gets included by running `npx clawon discover` (or `npx clawon discover --include-secrets`) 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`.
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 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:
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 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");
@@ -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 = readline.createInterface({ input: process.stdin, output: process.stdout });
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 is not supported for cloud backups yet. Use local backups, or wait for --encrypt support.");
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
- 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
+ }
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
- 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
+ }
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 command = resolveCliCommand(`backup --scheduled`);
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 = readline.createInterface({ input: process.stdin, output: process.stdout });
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
- 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;
5612
5805
  if (!opts.scheduled) {
5613
5806
  console.log(`
5614
5807
  \u2713 Local backup saved!`);
5615
- console.log(` File: ${filePath}`);
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
- try {
5676
- const meta = await readArchiveMeta(filePath);
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)} | ${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}`
5683
5876
  );
5684
- } catch {
5685
- 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
+ }
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
- 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);
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(archivePath, OPENCLAW_DIR);
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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawon",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "description": "Backup and restore your OpenClaw workspace",
5
5
  "type": "module",
6
6
  "bin": {