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.
- package/README.md +47 -0
- package/backup-claw.js +198 -0
- 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
|
+
}
|