clawon 0.1.4 → 0.1.6
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 +138 -0
- package/dist/index.js +99 -48
- package/package.json +3 -2
package/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Clawon
|
|
2
|
+
|
|
3
|
+
Backup and restore your [OpenClaw](https://openclaw.ai) workspace. Move your memory, skills, and config between machines in one command.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# No install needed — runs with npx
|
|
9
|
+
npx clawon discover # Preview what will be backed up
|
|
10
|
+
npx clawon local backup # Save a local backup
|
|
11
|
+
npx clawon local restore # Restore from latest backup
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Commands
|
|
15
|
+
|
|
16
|
+
### Local Backups (no account needed)
|
|
17
|
+
|
|
18
|
+
Local backups are stored in `~/.clawon/backups/` as standard `.tar.gz` archives. You can inspect them with `tar tzf` or extract manually with `tar xzf`.
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Create a backup
|
|
22
|
+
npx clawon local backup
|
|
23
|
+
npx clawon local backup --tag "before migration"
|
|
24
|
+
|
|
25
|
+
# List all local backups
|
|
26
|
+
npx clawon local list
|
|
27
|
+
|
|
28
|
+
# Restore
|
|
29
|
+
npx clawon local restore # Latest backup
|
|
30
|
+
npx clawon local restore --pick 2 # Backup #2 from list
|
|
31
|
+
npx clawon local restore --file path.tar.gz # External file
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Cloud Backups (requires account)
|
|
35
|
+
|
|
36
|
+
Cloud backups sync your workspace to Clawon's servers for cross-machine access.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Authenticate
|
|
40
|
+
npx clawon login --api-key <your-key>
|
|
41
|
+
|
|
42
|
+
# Create a cloud backup
|
|
43
|
+
npx clawon backup
|
|
44
|
+
npx clawon backup --tag "stable config"
|
|
45
|
+
npx clawon backup --dry-run # Preview without uploading
|
|
46
|
+
|
|
47
|
+
# List cloud backups
|
|
48
|
+
npx clawon list
|
|
49
|
+
|
|
50
|
+
# Restore from cloud
|
|
51
|
+
npx clawon restore
|
|
52
|
+
npx clawon restore --snapshot <id> # Specific snapshot
|
|
53
|
+
npx clawon restore --dry-run # Preview without extracting
|
|
54
|
+
|
|
55
|
+
# Manage snapshots
|
|
56
|
+
npx clawon delete <id>
|
|
57
|
+
npx clawon delete --oldest
|
|
58
|
+
npx clawon files # List files in a cloud backup
|
|
59
|
+
npx clawon activity # Recent events
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Other Commands
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx clawon discover # Show exactly which files would be backed up
|
|
66
|
+
npx clawon status # Connection status and file count
|
|
67
|
+
npx clawon logout # Remove local credentials
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## What Gets Backed Up
|
|
71
|
+
|
|
72
|
+
Clawon uses an **allowlist** — only files matching these patterns are included:
|
|
73
|
+
|
|
74
|
+
| Pattern | What it captures |
|
|
75
|
+
|---------|-----------------|
|
|
76
|
+
| `workspace/*.md` | Workspace markdown (memory, notes, identity) |
|
|
77
|
+
| `workspace/memory/*.md` | Daily memory files |
|
|
78
|
+
| `workspace/memory/**/*.md` | Nested memory (projects, workflows, experiments) |
|
|
79
|
+
| `workspace/skills/**` | Custom skills |
|
|
80
|
+
| `workspace/canvas/**` | Canvas data |
|
|
81
|
+
| `skills/**` | Top-level skills |
|
|
82
|
+
| `agents/*/config.json` | Agent configurations |
|
|
83
|
+
|
|
84
|
+
Run `npx clawon discover` to see the exact file list for your instance.
|
|
85
|
+
|
|
86
|
+
## What's Excluded
|
|
87
|
+
|
|
88
|
+
These are **always excluded**, even if they match an include pattern:
|
|
89
|
+
|
|
90
|
+
| Pattern | Why |
|
|
91
|
+
|---------|-----|
|
|
92
|
+
| `credentials/**` | API keys, tokens, auth files |
|
|
93
|
+
| `openclaw.json` | May contain credentials |
|
|
94
|
+
| `agents/*/sessions/**` | Ephemeral session data |
|
|
95
|
+
| `memory/lancedb/**` | Vector database (binary, large) |
|
|
96
|
+
| `memory/*.sqlite` | SQLite databases |
|
|
97
|
+
| `*.lock`, `*.wal`, `*.shm` | Database lock files |
|
|
98
|
+
| `node_modules/**` | Dependencies |
|
|
99
|
+
|
|
100
|
+
**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.
|
|
101
|
+
|
|
102
|
+
## Archive Format
|
|
103
|
+
|
|
104
|
+
Local backups are standard gzip-compressed tar archives (`.tar.gz`). You can inspect and extract them with standard tools:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# List contents
|
|
108
|
+
tar tzf ~/.clawon/backups/backup-2026-03-05T1030.tar.gz
|
|
109
|
+
|
|
110
|
+
# Extract manually
|
|
111
|
+
tar xzf ~/.clawon/backups/backup-2026-03-05T1030.tar.gz -C /tmp/inspect
|
|
112
|
+
|
|
113
|
+
# View metadata
|
|
114
|
+
tar xzf backup.tar.gz _clawon_meta.json -O | cat
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Each archive contains:
|
|
118
|
+
- `_clawon_meta.json` — metadata (version, date, tag, file count)
|
|
119
|
+
- Your workspace files in their original directory structure
|
|
120
|
+
|
|
121
|
+
## Data Storage
|
|
122
|
+
|
|
123
|
+
| | Local | Cloud |
|
|
124
|
+
|---|---|---|
|
|
125
|
+
| **Location** | `~/.clawon/backups/` | Clawon servers (Supabase Storage) |
|
|
126
|
+
| **Format** | `.tar.gz` | Individual files with signed URLs |
|
|
127
|
+
| **Limit** | Unlimited | 2 snapshots (Starter), more on paid plans |
|
|
128
|
+
| **Account required** | No | Yes |
|
|
129
|
+
| **Cross-machine** | No (manual file transfer) | Yes |
|
|
130
|
+
|
|
131
|
+
## Configuration
|
|
132
|
+
|
|
133
|
+
Config is stored at `~/.clawon/config.json` after running `clawon login`. Contains your API key, profile ID, and API URL. Run `clawon logout` to remove it.
|
|
134
|
+
|
|
135
|
+
## Requirements
|
|
136
|
+
|
|
137
|
+
- Node.js 18+
|
|
138
|
+
- An OpenClaw installation at `~/.openclaw/`
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Command } from "commander";
|
|
|
5
5
|
import fs from "fs";
|
|
6
6
|
import path from "path";
|
|
7
7
|
import os from "os";
|
|
8
|
-
import
|
|
8
|
+
import * as tar from "tar";
|
|
9
9
|
var CONFIG_DIR = path.join(os.homedir(), ".clawon");
|
|
10
10
|
var CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
11
11
|
var OPENCLAW_DIR = path.join(os.homedir(), ".openclaw");
|
|
@@ -45,6 +45,7 @@ var INCLUDE_PATTERNS = [
|
|
|
45
45
|
];
|
|
46
46
|
var EXCLUDE_PATTERNS = [
|
|
47
47
|
"credentials/**",
|
|
48
|
+
"openclaw.json",
|
|
48
49
|
"agents/*/sessions/**",
|
|
49
50
|
"memory/lancedb/**",
|
|
50
51
|
"memory/*.sqlite",
|
|
@@ -93,24 +94,46 @@ function discoverFiles(baseDir) {
|
|
|
93
94
|
walk(baseDir);
|
|
94
95
|
return files;
|
|
95
96
|
}
|
|
96
|
-
function createLocalArchive(files, openclawDir) {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
const content = fs.readFileSync(fullPath).toString("base64");
|
|
100
|
-
return { path: f.path, size: f.size, content };
|
|
101
|
-
});
|
|
102
|
-
const archive = {
|
|
103
|
-
version: 1,
|
|
97
|
+
async function createLocalArchive(files, openclawDir, outputPath, tag) {
|
|
98
|
+
const meta = {
|
|
99
|
+
version: 2,
|
|
104
100
|
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
105
|
-
|
|
101
|
+
...tag ? { tag } : {},
|
|
102
|
+
file_count: files.length
|
|
106
103
|
};
|
|
107
|
-
|
|
104
|
+
const metaPath = path.join(openclawDir, "_clawon_meta.json");
|
|
105
|
+
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
|
|
106
|
+
try {
|
|
107
|
+
await tar.create(
|
|
108
|
+
{ gzip: true, file: outputPath, cwd: openclawDir },
|
|
109
|
+
["_clawon_meta.json", ...files.map((f) => f.path)]
|
|
110
|
+
);
|
|
111
|
+
} finally {
|
|
112
|
+
fs.unlinkSync(metaPath);
|
|
113
|
+
}
|
|
108
114
|
}
|
|
109
|
-
function
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
115
|
+
async function readArchiveMeta(archivePath) {
|
|
116
|
+
let meta = null;
|
|
117
|
+
await tar.list({
|
|
118
|
+
file: archivePath,
|
|
119
|
+
onReadEntry: (entry) => {
|
|
120
|
+
if (entry.path === "_clawon_meta.json") {
|
|
121
|
+
const chunks = [];
|
|
122
|
+
entry.on("data", (c) => chunks.push(c));
|
|
123
|
+
entry.on("end", () => {
|
|
124
|
+
meta = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
if (!meta) throw new Error("Invalid archive: missing _clawon_meta.json");
|
|
130
|
+
return meta;
|
|
131
|
+
}
|
|
132
|
+
async function extractLocalArchive(archivePath, targetDir) {
|
|
133
|
+
const meta = await readArchiveMeta(archivePath);
|
|
134
|
+
ensureDir(targetDir);
|
|
135
|
+
await tar.extract({ file: archivePath, cwd: targetDir, filter: (p) => p !== "_clawon_meta.json" });
|
|
136
|
+
return meta;
|
|
114
137
|
}
|
|
115
138
|
var POSTHOG_KEY = "phc_LGJC4ZrED6EiK0sC1fusErOhR6gHlFCS5Qs7ou93SmV";
|
|
116
139
|
function trackCliEvent(distinctId, event, properties = {}) {
|
|
@@ -148,7 +171,7 @@ program.command("login").description("Connect to Clawon with your API key").requ
|
|
|
148
171
|
process.exit(1);
|
|
149
172
|
}
|
|
150
173
|
});
|
|
151
|
-
program.command("backup").description("Backup your OpenClaw workspace to the cloud").option("--dry-run", "Show what would be backed up without uploading").action(async (opts) => {
|
|
174
|
+
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").action(async (opts) => {
|
|
152
175
|
const cfg = readConfig();
|
|
153
176
|
if (!cfg) {
|
|
154
177
|
console.error("\u2717 Not logged in. Run: clawon login --api-key <key>");
|
|
@@ -188,7 +211,8 @@ program.command("backup").description("Backup your OpenClaw workspace to the clo
|
|
|
188
211
|
cfg.apiKey,
|
|
189
212
|
{
|
|
190
213
|
profileId: cfg.profileId,
|
|
191
|
-
files: files.map((f) => ({ path: f.path, size: f.size }))
|
|
214
|
+
files: files.map((f) => ({ path: f.path, size: f.size })),
|
|
215
|
+
...opts.tag ? { tag: opts.tag } : {}
|
|
192
216
|
}
|
|
193
217
|
);
|
|
194
218
|
console.log(`Uploading ${files.length} files...`);
|
|
@@ -311,13 +335,14 @@ program.command("list").description("List your backups").option("--limit <n>", "
|
|
|
311
335
|
return;
|
|
312
336
|
}
|
|
313
337
|
console.log("Your backups:\n");
|
|
314
|
-
console.log("ID | Date | Files | Size");
|
|
315
|
-
console.log("\u2500".repeat(
|
|
338
|
+
console.log("ID | Date | Files | Size | Tag");
|
|
339
|
+
console.log("\u2500".repeat(100));
|
|
316
340
|
for (const s of snapshots) {
|
|
317
341
|
const date = new Date(s.created_at).toLocaleString();
|
|
318
342
|
const size = s.size_bytes ? `${(s.size_bytes / 1024).toFixed(1)} KB` : "N/A";
|
|
319
343
|
const files = s.changed_files_count || "N/A";
|
|
320
|
-
|
|
344
|
+
const tag = s.tag || "";
|
|
345
|
+
console.log(`${s.id} | ${date.padEnd(20)} | ${String(files).padEnd(5)} | ${String(size).padEnd(8)} | ${tag}`);
|
|
321
346
|
}
|
|
322
347
|
console.log(`
|
|
323
348
|
Total: ${snapshots.length} backup(s)`);
|
|
@@ -422,6 +447,36 @@ program.command("delete [id]").description("Delete a snapshot").option("--oldest
|
|
|
422
447
|
process.exit(1);
|
|
423
448
|
}
|
|
424
449
|
});
|
|
450
|
+
program.command("discover").description("Preview which files would be included in a backup").action(async () => {
|
|
451
|
+
if (!fs.existsSync(OPENCLAW_DIR)) {
|
|
452
|
+
console.error(`\u2717 OpenClaw directory not found: ${OPENCLAW_DIR}`);
|
|
453
|
+
process.exit(1);
|
|
454
|
+
}
|
|
455
|
+
const files = discoverFiles(OPENCLAW_DIR);
|
|
456
|
+
if (files.length === 0) {
|
|
457
|
+
console.log("No files matched the include patterns.");
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
|
|
461
|
+
const tree = {};
|
|
462
|
+
for (const f of files) {
|
|
463
|
+
const dir = path.dirname(f.path);
|
|
464
|
+
if (!tree[dir]) tree[dir] = [];
|
|
465
|
+
tree[dir].push(f);
|
|
466
|
+
}
|
|
467
|
+
console.log(`Files that would be backed up:
|
|
468
|
+
`);
|
|
469
|
+
for (const dir of Object.keys(tree).sort()) {
|
|
470
|
+
console.log(`\u{1F4C1} ${dir}/`);
|
|
471
|
+
for (const f of tree[dir]) {
|
|
472
|
+
const name = path.basename(f.path);
|
|
473
|
+
console.log(` \u{1F4C4} ${name} (${f.size} bytes)`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
console.log(`
|
|
477
|
+
Total: ${files.length} files (${(totalSize / 1024).toFixed(1)} KB)`);
|
|
478
|
+
console.log(`Source: ${OPENCLAW_DIR}`);
|
|
479
|
+
});
|
|
425
480
|
program.command("files").description("List files in a backup").option("--snapshot <id>", "Snapshot ID (default: latest)").action(async (opts) => {
|
|
426
481
|
const cfg = readConfig();
|
|
427
482
|
if (!cfg) {
|
|
@@ -463,7 +518,7 @@ Total: ${files.length} files`);
|
|
|
463
518
|
}
|
|
464
519
|
});
|
|
465
520
|
var local = program.command("local").description("Local backup and restore (no cloud required)");
|
|
466
|
-
local.command("backup").description("Save a local backup of your OpenClaw workspace").action(async () => {
|
|
521
|
+
local.command("backup").description("Save a local backup of your OpenClaw workspace").option("--tag <label>", "Add a label to this backup").action(async (opts) => {
|
|
467
522
|
if (!fs.existsSync(OPENCLAW_DIR)) {
|
|
468
523
|
console.error(`\u2717 OpenClaw directory not found: ${OPENCLAW_DIR}`);
|
|
469
524
|
process.exit(1);
|
|
@@ -476,18 +531,19 @@ local.command("backup").description("Save a local backup of your OpenClaw worksp
|
|
|
476
531
|
}
|
|
477
532
|
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
|
|
478
533
|
console.log(`Found ${files.length} files (${(totalSize / 1024).toFixed(1)} KB)`);
|
|
479
|
-
console.log("Creating archive...");
|
|
480
|
-
const archive = createLocalArchive(files, OPENCLAW_DIR);
|
|
481
534
|
ensureDir(BACKUPS_DIR);
|
|
482
535
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "").replace("T", "T").slice(0, 15);
|
|
483
536
|
const filename = `backup-${timestamp}.tar.gz`;
|
|
484
537
|
const filePath = path.join(BACKUPS_DIR, filename);
|
|
485
|
-
|
|
538
|
+
console.log("Creating archive...");
|
|
539
|
+
await createLocalArchive(files, OPENCLAW_DIR, filePath, opts.tag);
|
|
540
|
+
const archiveSize = fs.statSync(filePath).size;
|
|
486
541
|
console.log(`
|
|
487
542
|
\u2713 Local backup saved!`);
|
|
488
543
|
console.log(` File: ${filePath}`);
|
|
489
544
|
console.log(` Files: ${files.length}`);
|
|
490
|
-
console.log(` Size: ${(
|
|
545
|
+
console.log(` Size: ${(archiveSize / 1024).toFixed(1)} KB (compressed)`);
|
|
546
|
+
if (opts.tag) console.log(` Tag: ${opts.tag}`);
|
|
491
547
|
const cfg = readConfig();
|
|
492
548
|
trackCliEvent(cfg?.profileId || "anonymous", "local_backup_created", {
|
|
493
549
|
file_count: files.length,
|
|
@@ -505,19 +561,21 @@ local.command("list").description("List local backups").action(async () => {
|
|
|
505
561
|
return;
|
|
506
562
|
}
|
|
507
563
|
console.log("Local backups:\n");
|
|
508
|
-
console.log("# | Date | Files | Size | Path");
|
|
509
|
-
console.log("\u2500".repeat(
|
|
564
|
+
console.log("# | Date | Files | Size | Tag | Path");
|
|
565
|
+
console.log("\u2500".repeat(120));
|
|
510
566
|
for (let i = 0; i < entries.length; i++) {
|
|
511
567
|
const filePath = path.join(BACKUPS_DIR, entries[i]);
|
|
512
568
|
try {
|
|
513
|
-
const
|
|
514
|
-
const
|
|
515
|
-
const
|
|
569
|
+
const meta = await readArchiveMeta(filePath);
|
|
570
|
+
const date = new Date(meta.created).toLocaleString();
|
|
571
|
+
const archiveSize = fs.statSync(filePath).size;
|
|
572
|
+
const sizeStr = `${(archiveSize / 1024).toFixed(1)} KB`;
|
|
573
|
+
const tagStr = (meta.tag || "").padEnd(20);
|
|
516
574
|
console.log(
|
|
517
|
-
`${String(i + 1).padStart(2)} | ${date.padEnd(25)} | ${String(
|
|
575
|
+
`${String(i + 1).padStart(2)} | ${date.padEnd(25)} | ${String(meta.file_count).padEnd(5)} | ${sizeStr.padEnd(10)} | ${tagStr} | ${filePath}`
|
|
518
576
|
);
|
|
519
577
|
} catch {
|
|
520
|
-
console.log(`${String(i + 1).padStart(2)} | ${entries[i].padEnd(25)} | ??? | ??? | ${filePath}`);
|
|
578
|
+
console.log(`${String(i + 1).padStart(2)} | ${entries[i].padEnd(25)} | ??? | ??? | | ${filePath}`);
|
|
521
579
|
}
|
|
522
580
|
}
|
|
523
581
|
console.log(`
|
|
@@ -559,26 +617,19 @@ local.command("restore").description("Restore from a local backup").option("--fi
|
|
|
559
617
|
}
|
|
560
618
|
console.log(`Restoring from: ${archivePath}`);
|
|
561
619
|
try {
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
console.log(`
|
|
565
|
-
console.log(`
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
const targetPath = path.join(OPENCLAW_DIR, file.path);
|
|
569
|
-
ensureDir(path.dirname(targetPath));
|
|
570
|
-
fs.writeFileSync(targetPath, Buffer.from(file.content, "base64"));
|
|
571
|
-
restored++;
|
|
572
|
-
process.stdout.write(`\r Restored: ${restored}/${files.length}`);
|
|
573
|
-
}
|
|
574
|
-
console.log("");
|
|
620
|
+
const meta = await readArchiveMeta(archivePath);
|
|
621
|
+
console.log(`Backup date: ${new Date(meta.created).toLocaleString()}`);
|
|
622
|
+
console.log(`Files: ${meta.file_count}`);
|
|
623
|
+
if (meta.tag) console.log(`Tag: ${meta.tag}`);
|
|
624
|
+
console.log("\nExtracting...");
|
|
625
|
+
await extractLocalArchive(archivePath, OPENCLAW_DIR);
|
|
575
626
|
console.log(`
|
|
576
627
|
\u2713 Restore complete!`);
|
|
577
628
|
console.log(` Restored to: ${OPENCLAW_DIR}`);
|
|
578
|
-
console.log(` Files: ${
|
|
629
|
+
console.log(` Files: ${meta.file_count}`);
|
|
579
630
|
const cfg = readConfig();
|
|
580
631
|
trackCliEvent(cfg?.profileId || "anonymous", "local_backup_restored", {
|
|
581
|
-
file_count:
|
|
632
|
+
file_count: meta.file_count,
|
|
582
633
|
source: opts.file ? "file" : "local"
|
|
583
634
|
});
|
|
584
635
|
} catch (e) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawon",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Backup and restore your OpenClaw workspace",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
],
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"commander": "^12.1.0"
|
|
27
|
+
"commander": "^12.1.0",
|
|
28
|
+
"tar": "^7.5.10"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
31
|
"tsup": "^8.2.4",
|