backup-claw 1.0.1

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 +47 -0
  2. package/backup-claw.js +198 -0
  3. package/package.json +30 -0
package/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # BackupClaw 🦞
2
+
3
+ **Privacy-first, zero-knowledge backup tool for OpenClaw bots.**
4
+
5
+ BackupClaw allow you to securely encrypt and archive your OpenClaw bot's memory and configuration without the data ever leaving your machine (unless you explicitly push it to our encrypted cloud).
6
+
7
+ [Visit BackupClaw.com](http://backupclaw.com) for more details.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install -g backup-claw
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ### 1. Local Backup
18
+ Navigate to your OpenClaw bot directory and run:
19
+
20
+ ```bash
21
+ backup-claw backup
22
+ ```
23
+
24
+ You will be prompted to set a **password**.
25
+ > **IMPORTANT:** We do not store this password. If you lose it, your backup is unrecoverable.
26
+
27
+ ### 2. Restore
28
+ To restore a backup file:
29
+
30
+ ```bash
31
+ backup-claw restore -f my-backup.enc -d /path/to/restore
32
+ ```
33
+
34
+ ### Automation (Bots)
35
+ You can run BackupClaw non-interactively by setting the `BACKUP_PASSWORD` environment variable.
36
+
37
+ ```bash
38
+ export BACKUP_PASSWORD="my-super-secret-password"
39
+ backup-claw backup -d .openclaw
40
+ ```
41
+
42
+ ## Cloud Backup (Pro)
43
+ Support for encrypted cloud storage is coming soon.
44
+ Visit our [website](http://backupclaw.com) to upgrade.
45
+
46
+ ## License
47
+ MIT
package/backup-claw.js ADDED
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const crypto = require('crypto');
6
+ const os = require('os');
7
+ const { pipeline } = require('stream');
8
+ const { promisify } = require('util');
9
+ const pipe = promisify(pipeline);
10
+
11
+ const program = require('commander');
12
+ const inquirer = require('inquirer');
13
+ const chalk = require('chalk');
14
+ const ora = require('ora');
15
+ const tar = require('tar');
16
+
17
+ const ALGORITHM = 'aes-256-ctr';
18
+ const DEFAULT_DIR = path.join(os.homedir(), '.openclaw');
19
+
20
+ console.log(chalk.red(`
21
+ ____ _ _ ________
22
+ | _ \\| | | | / ____| |
23
+ | |_) | | __ _ ___| | ___ _ _ __ | | | | __ ___ __
24
+ | _ <| |/ _\` |/ __| |/ / | | | '_ \\ | | | |/ _\` \\ \\ /\\ / /
25
+ | |_) | | (_| | (__| <| |_| | |_) | | |____| | (_| |\\ V V /
26
+ |____/|_|\\__,_|\\___|_|\\_\\\\__,_| .__/ \\______|_|\\__,_| \\_/\\_/
27
+ | |
28
+ |_|
29
+ `));
30
+ console.log(chalk.gray(' Privacy-First Backup for OpenClaw Bots\n'));
31
+
32
+ // --- Utils ---
33
+
34
+ function deriveKey(password, salt) {
35
+ return crypto.scryptSync(password, salt, 32);
36
+ }
37
+
38
+ // --- Commands ---
39
+
40
+ program
41
+ .version('1.0.0')
42
+ .description('Secure backup tool for OpenClaw');
43
+
44
+ program
45
+ .command('backup')
46
+ .description('Encrypt and backup your OpenClaw bot')
47
+ .option('-d, --dir <path>', 'Path to .openclaw directory', DEFAULT_DIR)
48
+ .option('-o, --output <path>', 'Output file path', './backup-claw.enc')
49
+ .action(async (cmd) => {
50
+ try {
51
+ if (!fs.existsSync(cmd.dir)) {
52
+ console.error(chalk.red(`Error: Directory not found at ${cmd.dir}`));
53
+ process.exit(1);
54
+ }
55
+
56
+ let answers;
57
+ if (process.env.BACKUP_PASSWORD) {
58
+ console.log(chalk.yellow('Using password from environment variable BACKUP_PASSWORD'));
59
+ answers = {
60
+ password: process.env.BACKUP_PASSWORD,
61
+ confirm: process.env.BACKUP_PASSWORD
62
+ };
63
+ } else {
64
+ answers = await inquirer.prompt([
65
+ {
66
+ type: 'password',
67
+ name: 'password',
68
+ message: 'Set a strong password for this backup:',
69
+ mask: '*',
70
+ validate: (input) => input.length > 0
71
+ },
72
+ {
73
+ type: 'password',
74
+ name: 'confirm',
75
+ message: 'Confirm password:',
76
+ mask: '*',
77
+ validate: (input, answers) => input === answers.password || 'Passwords do not match'
78
+ }
79
+ ]);
80
+ }
81
+
82
+ const spinner = ora('Preparing backup...').start();
83
+
84
+ const salt = crypto.randomBytes(16);
85
+ const iv = crypto.randomBytes(16);
86
+ const key = deriveKey(answers.password, salt);
87
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
88
+
89
+ const output = fs.createWriteStream(cmd.output);
90
+
91
+ // Header: Salt (16) + IV (16)
92
+ output.write(salt);
93
+ output.write(iv);
94
+
95
+ const sourceDir = path.dirname(cmd.dir);
96
+ const folderName = path.basename(cmd.dir);
97
+
98
+ spinner.text = 'Compressing and Encrypting...';
99
+
100
+ await pipe(
101
+ tar.c({
102
+ gzip: true,
103
+ cwd: sourceDir
104
+ }, [folderName]),
105
+ cipher,
106
+ output
107
+ );
108
+
109
+ spinner.succeed(chalk.green('Backup successful!'));
110
+ console.log(chalk.white(`\nFile saved to: ${chalk.bold(path.resolve(cmd.output))}`));
111
+ console.log(chalk.yellow(`IMPORTANT: Do not lose your password. We cannot recover it.`));
112
+
113
+ // Stub for analytics
114
+ // trackEvent('backup_success', { size: fs.statSync(cmd.output).size });
115
+
116
+ } catch (err) {
117
+ if (spinner) spinner.fail('Backup failed');
118
+ console.error(err);
119
+ }
120
+ });
121
+
122
+ program
123
+ .command('restore')
124
+ .description('Restore from an encrypted backup')
125
+ .requiredOption('-f, --file <path>', 'Path to backup file')
126
+ .option('-d, --dest <path>', 'Restore destination directory', os.homedir())
127
+ .action(async (cmd) => {
128
+ let spinner;
129
+ try {
130
+ if (!fs.existsSync(cmd.file)) {
131
+ console.error(chalk.red(`Error: File not found at ${cmd.file}`));
132
+ process.exit(1);
133
+ }
134
+
135
+ let answers;
136
+ if (process.env.BACKUP_PASSWORD) {
137
+ console.log(chalk.yellow('Using password from environment variable BACKUP_PASSWORD'));
138
+ answers = { password: process.env.BACKUP_PASSWORD };
139
+ } else {
140
+ answers = await inquirer.prompt([
141
+ {
142
+ type: 'password',
143
+ name: 'password',
144
+ message: 'Enter backup password:',
145
+ mask: '*'
146
+ }
147
+ ]);
148
+ }
149
+
150
+ spinner = ora('Reading backup file...').start();
151
+
152
+ // We need to read the header (salt+iv) first.
153
+ // Since we are streaming, we can't just slice the stream easily without packages.
154
+ // For MVP simplicity and robustness, using fs.readSync for header is safe
155
+ // as long as we stream the rest.
156
+
157
+ const fd = fs.openSync(cmd.file, 'r');
158
+ const header = Buffer.alloc(32); // 16 salt + 16 iv
159
+ fs.readSync(fd, header, 0, 32, 0);
160
+ fs.closeSync(fd);
161
+
162
+ const salt = header.slice(0, 16);
163
+ const iv = header.slice(16, 32);
164
+
165
+ // Create read stream starting AFTER the header
166
+ const input = fs.createReadStream(cmd.file, { start: 32 });
167
+
168
+ spinner.text = 'Deriving key...';
169
+ const key = deriveKey(answers.password, salt);
170
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
171
+
172
+ spinner.text = 'Decrypting and Extracting...';
173
+
174
+ if (!fs.existsSync(cmd.dest)) {
175
+ fs.mkdirSync(cmd.dest, { recursive: true });
176
+ }
177
+
178
+ await pipe(
179
+ input,
180
+ decipher,
181
+ tar.x({
182
+ cwd: cmd.dest
183
+ })
184
+ );
185
+
186
+ spinner.succeed(chalk.green('Restore successful!'));
187
+ console.log(chalk.white(`Files restored to: ${chalk.bold(cmd.dest)}`));
188
+
189
+ } catch (err) {
190
+ if (spinner) spinner.fail('Restore failed');
191
+ console.error(chalk.red('Error: ' + err.message));
192
+ if (err.message.includes('bad decrypt') || err.code === 'ERR_OSSL_EVP_BAD_DECRYPT') {
193
+ console.error(chalk.red('Most likely cause: Incorrect password.'));
194
+ }
195
+ }
196
+ });
197
+
198
+ program.parse(process.argv);
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "backup-claw",
3
+ "version": "1.0.1",
4
+ "description": "Secure, local backup tool for OpenClaw bots",
5
+ "main": "backup-claw.js",
6
+ "bin": {
7
+ "backup-claw": "./backup-claw.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1",
11
+ "backup": "node backup-claw.js backup",
12
+ "restore": "node backup-claw.js restore"
13
+ },
14
+ "keywords": [
15
+ "backup",
16
+ "openclaw",
17
+ "security",
18
+ "cli"
19
+ ],
20
+ "author": "BackupClaw Team",
21
+ "license": "MIT",
22
+ "dependencies": {
23
+ "axios": "^1.6.0",
24
+ "chalk": "^4.1.2",
25
+ "commander": "^11.0.0",
26
+ "inquirer": "^8.2.6",
27
+ "ora": "^5.4.1",
28
+ "tar": "^6.2.0"
29
+ }
30
+ }