clawon 0.1.1 → 0.1.3

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 (2) hide show
  1. package/dist/index.js +264 -2
  2. package/package.json +8 -2
package/dist/index.js CHANGED
@@ -5,9 +5,11 @@ 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
9
  var CONFIG_DIR = path.join(os.homedir(), ".clawon");
9
10
  var CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
10
11
  var OPENCLAW_DIR = path.join(os.homedir(), ".openclaw");
12
+ var BACKUPS_DIR = path.join(CONFIG_DIR, "backups");
11
13
  function ensureDir(dir) {
12
14
  fs.mkdirSync(dir, { recursive: true });
13
15
  }
@@ -91,6 +93,39 @@ function discoverFiles(baseDir) {
91
93
  walk(baseDir);
92
94
  return files;
93
95
  }
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,
104
+ created: (/* @__PURE__ */ new Date()).toISOString(),
105
+ files: archiveFiles
106
+ };
107
+ return zlib.gzipSync(JSON.stringify(archive));
108
+ }
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 };
114
+ }
115
+ var POSTHOG_KEY = "phc_LGJC4ZrED6EiK0sC1fusErOhR6gHlFCS5Qs7ou93SmV";
116
+ function trackCliEvent(distinctId, event, properties = {}) {
117
+ fetch("https://us.i.posthog.com/capture/", {
118
+ method: "POST",
119
+ headers: { "content-type": "application/json" },
120
+ body: JSON.stringify({
121
+ api_key: POSTHOG_KEY,
122
+ distinct_id: distinctId,
123
+ event,
124
+ properties: { ...properties, source: "cli" }
125
+ })
126
+ }).catch(() => {
127
+ });
128
+ }
94
129
  var program = new Command();
95
130
  program.name("clawon").description("Backup and restore your OpenClaw workspace").version("0.1.1");
96
131
  program.command("login").description("Connect to Clawon with your API key").requiredOption("--api-key <key>", "Your Clawon API key").option("--api-url <url>", "API base URL", "https://clawon.io").action(async (opts) => {
@@ -182,9 +217,21 @@ program.command("backup").description("Backup your OpenClaw workspace to the clo
182
217
  console.log(` Snapshot ID: ${snapshotId}`);
183
218
  console.log(` Files: ${files.length}`);
184
219
  console.log(` Size: ${(totalSize / 1024).toFixed(1)} KB`);
220
+ trackCliEvent(cfg.profileId, "cloud_backup_created", {
221
+ file_count: files.length,
222
+ total_bytes: totalSize
223
+ });
185
224
  } catch (e) {
186
- console.error(`
187
- \u2717 Backup failed: ${e.message}`);
225
+ const msg = e.message;
226
+ if (msg.includes("Snapshot limit")) {
227
+ console.error("\n\u2717 Snapshot limit reached (2).");
228
+ console.error(" Delete one first: clawon delete <id>");
229
+ console.error(" Delete oldest: clawon delete --oldest");
230
+ console.error(" List snapshots: clawon list");
231
+ } else {
232
+ console.error(`
233
+ \u2717 Backup failed: ${msg}`);
234
+ }
188
235
  process.exit(1);
189
236
  }
190
237
  });
@@ -230,9 +277,16 @@ program.command("restore").description("Restore your OpenClaw workspace from the
230
277
  process.stdout.write(`\r Downloaded: ${downloaded}/${files.length}`);
231
278
  }
232
279
  console.log("");
280
+ await api(cfg.apiBaseUrl, "/api/v1/snapshots/restore", "POST", cfg.apiKey, {
281
+ profileId: cfg.profileId,
282
+ snapshotId: snapshot.id
283
+ });
233
284
  console.log("\n\u2713 Restore complete!");
234
285
  console.log(` Restored to: ${OPENCLAW_DIR}`);
235
286
  console.log(` Files: ${files.length}`);
287
+ trackCliEvent(cfg.profileId, "cloud_backup_restored", {
288
+ file_count: files.length
289
+ });
236
290
  } catch (e) {
237
291
  console.error(`
238
292
  \u2717 Restore failed: ${e.message}`);
@@ -272,6 +326,102 @@ Total: ${snapshots.length} backup(s)`);
272
326
  process.exit(1);
273
327
  }
274
328
  });
329
+ var ACTIVITY_LABELS = {
330
+ BACKUP_CREATED: "Backup created",
331
+ SNAPSHOT_CREATED: "Snapshot created",
332
+ SNAPSHOT_DELETED: "Snapshot deleted",
333
+ SNAPSHOT_RESTORED: "Snapshot restored",
334
+ BACKUP_DOWNLOADED: "Backup downloaded",
335
+ CONNECTED: "Connected",
336
+ DISCONNECTED: "Disconnected"
337
+ };
338
+ function formatEventLabel(type) {
339
+ return ACTIVITY_LABELS[type] || type.replace(/_/g, " ").toLowerCase().replace(/^\w/, (c) => c.toUpperCase());
340
+ }
341
+ function formatEventDetails(payload) {
342
+ if (!payload || typeof payload !== "object") return "";
343
+ const parts = [];
344
+ if (payload.fileCount != null || payload.changedFilesCount != null) {
345
+ parts.push(`${payload.fileCount ?? payload.changedFilesCount} files`);
346
+ }
347
+ if (payload.snapshotId) {
348
+ parts.push(payload.snapshotId);
349
+ }
350
+ if (payload.instanceName) {
351
+ parts.push(payload.instanceName);
352
+ }
353
+ return parts.join(" \xB7 ");
354
+ }
355
+ program.command("activity").description("Show recent activity").option("--limit <n>", "Number of events to show", "10").action(async (opts) => {
356
+ const cfg = readConfig();
357
+ if (!cfg) {
358
+ console.error("\u2717 Not logged in. Run: clawon login --api-key <key>");
359
+ process.exit(1);
360
+ }
361
+ try {
362
+ const { events } = await api(
363
+ cfg.apiBaseUrl,
364
+ `/api/v1/events/list?profileId=${cfg.profileId}&limit=${opts.limit}`,
365
+ "GET",
366
+ cfg.apiKey
367
+ );
368
+ if (!events?.length) {
369
+ console.log("No activity yet. Run: clawon backup");
370
+ return;
371
+ }
372
+ console.log("Recent activity:\n");
373
+ console.log("Date | Event | Details");
374
+ console.log("\u2500".repeat(80));
375
+ for (const ev of events) {
376
+ const date = new Date(ev.created_at).toLocaleString();
377
+ const label = formatEventLabel(ev.type);
378
+ const details = formatEventDetails(ev.payload);
379
+ console.log(`${date.padEnd(20)} | ${label.padEnd(18)} | ${details}`);
380
+ }
381
+ } catch (e) {
382
+ console.error(`\u2717 Failed to load activity: ${e.message}`);
383
+ process.exit(1);
384
+ }
385
+ });
386
+ program.command("delete [id]").description("Delete a snapshot").option("--oldest", "Delete the oldest ready snapshot").action(async (id, opts) => {
387
+ const cfg = readConfig();
388
+ if (!cfg) {
389
+ console.error("\u2717 Not logged in. Run: clawon login --api-key <key>");
390
+ process.exit(1);
391
+ }
392
+ if (!id && !opts.oldest) {
393
+ console.error("\u2717 Provide a snapshot ID or use --oldest");
394
+ console.error(" Usage: clawon delete <id>");
395
+ console.error(" clawon delete --oldest");
396
+ process.exit(1);
397
+ }
398
+ try {
399
+ let snapshotId = id;
400
+ if (opts.oldest) {
401
+ const { snapshots } = await api(
402
+ cfg.apiBaseUrl,
403
+ `/api/v1/snapshots/list?profileId=${cfg.profileId}&limit=50`,
404
+ "GET",
405
+ cfg.apiKey
406
+ );
407
+ const readySnapshots = (snapshots || []).filter((s) => s.status === "ready");
408
+ if (readySnapshots.length === 0) {
409
+ console.error("\u2717 No ready snapshots to delete");
410
+ process.exit(1);
411
+ }
412
+ snapshotId = readySnapshots[readySnapshots.length - 1].id;
413
+ console.log(`Oldest ready snapshot: ${snapshotId}`);
414
+ }
415
+ await api(cfg.apiBaseUrl, "/api/v1/snapshots/delete", "POST", cfg.apiKey, {
416
+ profileId: cfg.profileId,
417
+ snapshotId
418
+ });
419
+ console.log(`\u2713 Deleted snapshot ${snapshotId}`);
420
+ } catch (e) {
421
+ console.error(`\u2717 Delete failed: ${e.message}`);
422
+ process.exit(1);
423
+ }
424
+ });
275
425
  program.command("files").description("List files in a backup").option("--snapshot <id>", "Snapshot ID (default: latest)").action(async (opts) => {
276
426
  const cfg = readConfig();
277
427
  if (!cfg) {
@@ -312,6 +462,118 @@ Total: ${files.length} files`);
312
462
  process.exit(1);
313
463
  }
314
464
  });
465
+ 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 () => {
467
+ if (!fs.existsSync(OPENCLAW_DIR)) {
468
+ console.error(`\u2717 OpenClaw directory not found: ${OPENCLAW_DIR}`);
469
+ process.exit(1);
470
+ }
471
+ console.log("Discovering files...");
472
+ const files = discoverFiles(OPENCLAW_DIR);
473
+ if (files.length === 0) {
474
+ console.error("\u2717 No files found to backup");
475
+ process.exit(1);
476
+ }
477
+ const totalSize = files.reduce((sum, f) => sum + f.size, 0);
478
+ console.log(`Found ${files.length} files (${(totalSize / 1024).toFixed(1)} KB)`);
479
+ console.log("Creating archive...");
480
+ const archive = createLocalArchive(files, OPENCLAW_DIR);
481
+ ensureDir(BACKUPS_DIR);
482
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "").replace("T", "T").slice(0, 15);
483
+ const filename = `backup-${timestamp}.tar.gz`;
484
+ const filePath = path.join(BACKUPS_DIR, filename);
485
+ fs.writeFileSync(filePath, archive);
486
+ console.log(`
487
+ \u2713 Local backup saved!`);
488
+ console.log(` File: ${filePath}`);
489
+ console.log(` Files: ${files.length}`);
490
+ console.log(` Size: ${(archive.length / 1024).toFixed(1)} KB (compressed)`);
491
+ const cfg = readConfig();
492
+ trackCliEvent(cfg?.profileId || "anonymous", "local_backup_created", {
493
+ file_count: files.length,
494
+ total_bytes: totalSize
495
+ });
496
+ });
497
+ local.command("list").description("List local backups").action(async () => {
498
+ if (!fs.existsSync(BACKUPS_DIR)) {
499
+ console.log("No local backups yet. Run: clawon local backup");
500
+ return;
501
+ }
502
+ const entries = fs.readdirSync(BACKUPS_DIR).filter((f) => f.endsWith(".tar.gz")).sort().reverse();
503
+ if (entries.length === 0) {
504
+ console.log("No local backups yet. Run: clawon local backup");
505
+ return;
506
+ }
507
+ console.log("Local backups:\n");
508
+ console.log("# | Date | Files | Size");
509
+ console.log("\u2500".repeat(60));
510
+ for (let i = 0; i < entries.length; i++) {
511
+ const filePath = path.join(BACKUPS_DIR, entries[i]);
512
+ 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();
516
+ console.log(
517
+ `${String(i + 1).padStart(2)} | ${date.padEnd(25)} | ${String(files.length).padEnd(5)} | ${(totalSize / 1024).toFixed(1)} KB`
518
+ );
519
+ } catch {
520
+ console.log(`${String(i + 1).padStart(2)} | ${entries[i].padEnd(25)} | ??? | ???`);
521
+ }
522
+ }
523
+ console.log(`
524
+ Total: ${entries.length} backup(s)`);
525
+ console.log(`Location: ${BACKUPS_DIR}`);
526
+ });
527
+ local.command("restore").description("Restore from a local backup").option("--file <path>", "Path to an external backup file").action(async (opts) => {
528
+ let archivePath;
529
+ if (opts.file) {
530
+ archivePath = path.resolve(opts.file);
531
+ if (!fs.existsSync(archivePath)) {
532
+ console.error(`\u2717 File not found: ${archivePath}`);
533
+ process.exit(1);
534
+ }
535
+ } else {
536
+ if (!fs.existsSync(BACKUPS_DIR)) {
537
+ console.error("\u2717 No local backups found. Run: clawon local backup");
538
+ process.exit(1);
539
+ }
540
+ const entries = fs.readdirSync(BACKUPS_DIR).filter((f) => f.endsWith(".tar.gz")).sort().reverse();
541
+ if (entries.length === 0) {
542
+ console.error("\u2717 No local backups found. Run: clawon local backup");
543
+ process.exit(1);
544
+ }
545
+ archivePath = path.join(BACKUPS_DIR, entries[0]);
546
+ }
547
+ console.log(`Restoring from: ${archivePath}`);
548
+ try {
549
+ const { created, files } = extractLocalArchive(archivePath);
550
+ const totalSize = files.reduce((sum, f) => sum + f.size, 0);
551
+ console.log(`Backup date: ${new Date(created).toLocaleString()}`);
552
+ console.log(`Files: ${files.length} (${(totalSize / 1024).toFixed(1)} KB)`);
553
+ let restored = 0;
554
+ for (const file of files) {
555
+ const targetPath = path.join(OPENCLAW_DIR, file.path);
556
+ ensureDir(path.dirname(targetPath));
557
+ fs.writeFileSync(targetPath, Buffer.from(file.content, "base64"));
558
+ restored++;
559
+ process.stdout.write(`\r Restored: ${restored}/${files.length}`);
560
+ }
561
+ console.log("");
562
+ console.log(`
563
+ \u2713 Restore complete!`);
564
+ console.log(` Restored to: ${OPENCLAW_DIR}`);
565
+ console.log(` Files: ${files.length}`);
566
+ const cfg = readConfig();
567
+ trackCliEvent(cfg?.profileId || "anonymous", "local_backup_restored", {
568
+ file_count: files.length,
569
+ source: opts.file ? "file" : "local"
570
+ });
571
+ } catch (e) {
572
+ console.error(`
573
+ \u2717 Restore failed: ${e.message}`);
574
+ process.exit(1);
575
+ }
576
+ });
315
577
  program.command("status").description("Show current status").action(async () => {
316
578
  const cfg = readConfig();
317
579
  console.log("Clawon Status\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawon",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Backup and restore your OpenClaw workspace",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,7 +15,13 @@
15
15
  "prepublishOnly": "npm run build",
16
16
  "start": "tsx src/index.ts"
17
17
  },
18
- "keywords": ["openclaw", "clawon", "backup", "restore", "cli"],
18
+ "keywords": [
19
+ "openclaw",
20
+ "clawon",
21
+ "backup",
22
+ "restore",
23
+ "cli"
24
+ ],
19
25
  "license": "MIT",
20
26
  "dependencies": {
21
27
  "commander": "^12.1.0"