clawon 0.1.5 → 0.1.7

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 +154 -0
  2. package/dist/index.js +103 -48
  3. package/package.json +3 -2
package/README.md ADDED
@@ -0,0 +1,154 @@
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
+ ## Telemetry
136
+
137
+ Clawon collects anonymous usage events (e.g. "backup created", "restore completed") to understand which features are used. No file contents, filenames, or personal data are sent.
138
+
139
+ **To opt out**, set either environment variable:
140
+
141
+ ```bash
142
+ # Standard convention (https://consoledonottrack.com)
143
+ export DO_NOT_TRACK=1
144
+
145
+ # Or Clawon-specific
146
+ export CLAWON_NO_TELEMETRY=1
147
+ ```
148
+
149
+ Telemetry is powered by [PostHog](https://posthog.com). The public project key is visible in the source code.
150
+
151
+ ## Requirements
152
+
153
+ - Node.js 18+
154
+ - 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,27 +94,53 @@ 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";
139
+ function telemetryDisabled() {
140
+ return process.env.DO_NOT_TRACK === "1" || process.env.CLAWON_NO_TELEMETRY === "1";
141
+ }
116
142
  function trackCliEvent(distinctId, event, properties = {}) {
143
+ if (telemetryDisabled()) return;
117
144
  fetch("https://us.i.posthog.com/capture/", {
118
145
  method: "POST",
119
146
  headers: { "content-type": "application/json" },
@@ -148,7 +175,7 @@ program.command("login").description("Connect to Clawon with your API key").requ
148
175
  process.exit(1);
149
176
  }
150
177
  });
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) => {
178
+ 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
179
  const cfg = readConfig();
153
180
  if (!cfg) {
154
181
  console.error("\u2717 Not logged in. Run: clawon login --api-key <key>");
@@ -188,7 +215,8 @@ program.command("backup").description("Backup your OpenClaw workspace to the clo
188
215
  cfg.apiKey,
189
216
  {
190
217
  profileId: cfg.profileId,
191
- files: files.map((f) => ({ path: f.path, size: f.size }))
218
+ files: files.map((f) => ({ path: f.path, size: f.size })),
219
+ ...opts.tag ? { tag: opts.tag } : {}
192
220
  }
193
221
  );
194
222
  console.log(`Uploading ${files.length} files...`);
@@ -311,13 +339,14 @@ program.command("list").description("List your backups").option("--limit <n>", "
311
339
  return;
312
340
  }
313
341
  console.log("Your backups:\n");
314
- console.log("ID | Date | Files | Size");
315
- console.log("\u2500".repeat(80));
342
+ console.log("ID | Date | Files | Size | Tag");
343
+ console.log("\u2500".repeat(100));
316
344
  for (const s of snapshots) {
317
345
  const date = new Date(s.created_at).toLocaleString();
318
346
  const size = s.size_bytes ? `${(s.size_bytes / 1024).toFixed(1)} KB` : "N/A";
319
347
  const files = s.changed_files_count || "N/A";
320
- console.log(`${s.id} | ${date.padEnd(20)} | ${String(files).padEnd(5)} | ${size}`);
348
+ const tag = s.tag || "";
349
+ console.log(`${s.id} | ${date.padEnd(20)} | ${String(files).padEnd(5)} | ${String(size).padEnd(8)} | ${tag}`);
321
350
  }
322
351
  console.log(`
323
352
  Total: ${snapshots.length} backup(s)`);
@@ -422,6 +451,36 @@ program.command("delete [id]").description("Delete a snapshot").option("--oldest
422
451
  process.exit(1);
423
452
  }
424
453
  });
454
+ program.command("discover").description("Preview which files would be included in a backup").action(async () => {
455
+ if (!fs.existsSync(OPENCLAW_DIR)) {
456
+ console.error(`\u2717 OpenClaw directory not found: ${OPENCLAW_DIR}`);
457
+ process.exit(1);
458
+ }
459
+ const files = discoverFiles(OPENCLAW_DIR);
460
+ if (files.length === 0) {
461
+ console.log("No files matched the include patterns.");
462
+ return;
463
+ }
464
+ const totalSize = files.reduce((sum, f) => sum + f.size, 0);
465
+ const tree = {};
466
+ for (const f of files) {
467
+ const dir = path.dirname(f.path);
468
+ if (!tree[dir]) tree[dir] = [];
469
+ tree[dir].push(f);
470
+ }
471
+ console.log(`Files that would be backed up:
472
+ `);
473
+ for (const dir of Object.keys(tree).sort()) {
474
+ console.log(`\u{1F4C1} ${dir}/`);
475
+ for (const f of tree[dir]) {
476
+ const name = path.basename(f.path);
477
+ console.log(` \u{1F4C4} ${name} (${f.size} bytes)`);
478
+ }
479
+ }
480
+ console.log(`
481
+ Total: ${files.length} files (${(totalSize / 1024).toFixed(1)} KB)`);
482
+ console.log(`Source: ${OPENCLAW_DIR}`);
483
+ });
425
484
  program.command("files").description("List files in a backup").option("--snapshot <id>", "Snapshot ID (default: latest)").action(async (opts) => {
426
485
  const cfg = readConfig();
427
486
  if (!cfg) {
@@ -463,7 +522,7 @@ Total: ${files.length} files`);
463
522
  }
464
523
  });
465
524
  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 () => {
525
+ 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
526
  if (!fs.existsSync(OPENCLAW_DIR)) {
468
527
  console.error(`\u2717 OpenClaw directory not found: ${OPENCLAW_DIR}`);
469
528
  process.exit(1);
@@ -476,18 +535,19 @@ local.command("backup").description("Save a local backup of your OpenClaw worksp
476
535
  }
477
536
  const totalSize = files.reduce((sum, f) => sum + f.size, 0);
478
537
  console.log(`Found ${files.length} files (${(totalSize / 1024).toFixed(1)} KB)`);
479
- console.log("Creating archive...");
480
- const archive = createLocalArchive(files, OPENCLAW_DIR);
481
538
  ensureDir(BACKUPS_DIR);
482
539
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "").replace("T", "T").slice(0, 15);
483
540
  const filename = `backup-${timestamp}.tar.gz`;
484
541
  const filePath = path.join(BACKUPS_DIR, filename);
485
- fs.writeFileSync(filePath, archive);
542
+ console.log("Creating archive...");
543
+ await createLocalArchive(files, OPENCLAW_DIR, filePath, opts.tag);
544
+ const archiveSize = fs.statSync(filePath).size;
486
545
  console.log(`
487
546
  \u2713 Local backup saved!`);
488
547
  console.log(` File: ${filePath}`);
489
548
  console.log(` Files: ${files.length}`);
490
- console.log(` Size: ${(archive.length / 1024).toFixed(1)} KB (compressed)`);
549
+ console.log(` Size: ${(archiveSize / 1024).toFixed(1)} KB (compressed)`);
550
+ if (opts.tag) console.log(` Tag: ${opts.tag}`);
491
551
  const cfg = readConfig();
492
552
  trackCliEvent(cfg?.profileId || "anonymous", "local_backup_created", {
493
553
  file_count: files.length,
@@ -505,19 +565,21 @@ local.command("list").description("List local backups").action(async () => {
505
565
  return;
506
566
  }
507
567
  console.log("Local backups:\n");
508
- console.log("# | Date | Files | Size | Path");
509
- console.log("\u2500".repeat(100));
568
+ console.log("# | Date | Files | Size | Tag | Path");
569
+ console.log("\u2500".repeat(120));
510
570
  for (let i = 0; i < entries.length; i++) {
511
571
  const filePath = path.join(BACKUPS_DIR, entries[i]);
512
572
  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();
573
+ const meta = await readArchiveMeta(filePath);
574
+ const date = new Date(meta.created).toLocaleString();
575
+ const archiveSize = fs.statSync(filePath).size;
576
+ const sizeStr = `${(archiveSize / 1024).toFixed(1)} KB`;
577
+ const tagStr = (meta.tag || "").padEnd(20);
516
578
  console.log(
517
- `${String(i + 1).padStart(2)} | ${date.padEnd(25)} | ${String(files.length).padEnd(5)} | ${(totalSize / 1024).toFixed(1).padEnd(8)} KB | ${filePath}`
579
+ `${String(i + 1).padStart(2)} | ${date.padEnd(25)} | ${String(meta.file_count).padEnd(5)} | ${sizeStr.padEnd(10)} | ${tagStr} | ${filePath}`
518
580
  );
519
581
  } catch {
520
- console.log(`${String(i + 1).padStart(2)} | ${entries[i].padEnd(25)} | ??? | ??? | ${filePath}`);
582
+ console.log(`${String(i + 1).padStart(2)} | ${entries[i].padEnd(25)} | ??? | ??? | | ${filePath}`);
521
583
  }
522
584
  }
523
585
  console.log(`
@@ -559,26 +621,19 @@ local.command("restore").description("Restore from a local backup").option("--fi
559
621
  }
560
622
  console.log(`Restoring from: ${archivePath}`);
561
623
  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("");
624
+ const meta = await readArchiveMeta(archivePath);
625
+ console.log(`Backup date: ${new Date(meta.created).toLocaleString()}`);
626
+ console.log(`Files: ${meta.file_count}`);
627
+ if (meta.tag) console.log(`Tag: ${meta.tag}`);
628
+ console.log("\nExtracting...");
629
+ await extractLocalArchive(archivePath, OPENCLAW_DIR);
575
630
  console.log(`
576
631
  \u2713 Restore complete!`);
577
632
  console.log(` Restored to: ${OPENCLAW_DIR}`);
578
- console.log(` Files: ${files.length}`);
633
+ console.log(` Files: ${meta.file_count}`);
579
634
  const cfg = readConfig();
580
635
  trackCliEvent(cfg?.profileId || "anonymous", "local_backup_restored", {
581
- file_count: files.length,
636
+ file_count: meta.file_count,
582
637
  source: opts.file ? "file" : "local"
583
638
  });
584
639
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawon",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
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",