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.
- package/dist/index.js +264 -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
|
-
|
|
187
|
-
|
|
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.
|
|
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": [
|
|
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"
|