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.
Files changed (3) hide show
  1. package/README.md +138 -0
  2. package/dist/index.js +99 -48
  3. 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 zlib from "zlib";
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 archiveFiles = files.map((f) => {
98
- const fullPath = path.join(openclawDir, f.path);
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
- files: archiveFiles
101
+ ...tag ? { tag } : {},
102
+ file_count: files.length
106
103
  };
107
- return zlib.gzipSync(JSON.stringify(archive));
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 extractLocalArchive(archivePath) {
110
- const compressed = fs.readFileSync(archivePath);
111
- const json = zlib.gunzipSync(compressed).toString("utf8");
112
- const archive = JSON.parse(json);
113
- return { created: archive.created, files: archive.files };
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(80));
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
- console.log(`${s.id} | ${date.padEnd(20)} | ${String(files).padEnd(5)} | ${size}`);
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
- fs.writeFileSync(filePath, archive);
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: ${(archive.length / 1024).toFixed(1)} KB (compressed)`);
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(100));
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 { created, files } = extractLocalArchive(filePath);
514
- const totalSize = files.reduce((sum, f) => sum + f.size, 0);
515
- const date = new Date(created).toLocaleString();
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(files.length).padEnd(5)} | ${(totalSize / 1024).toFixed(1).padEnd(8)} KB | ${filePath}`
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 { created, files } = extractLocalArchive(archivePath);
563
- const totalSize = files.reduce((sum, f) => sum + f.size, 0);
564
- console.log(`Backup date: ${new Date(created).toLocaleString()}`);
565
- console.log(`Files: ${files.length} (${(totalSize / 1024).toFixed(1)} KB)`);
566
- let restored = 0;
567
- for (const file of files) {
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: ${files.length}`);
629
+ console.log(` Files: ${meta.file_count}`);
579
630
  const cfg = readConfig();
580
631
  trackCliEvent(cfg?.profileId || "anonymous", "local_backup_restored", {
581
- file_count: files.length,
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.4",
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",