clawon 0.1.1
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 +344 -0
- package/package.json +28 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import os from "os";
|
|
8
|
+
var CONFIG_DIR = path.join(os.homedir(), ".clawon");
|
|
9
|
+
var CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
10
|
+
var OPENCLAW_DIR = path.join(os.homedir(), ".openclaw");
|
|
11
|
+
function ensureDir(dir) {
|
|
12
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
function readConfig() {
|
|
15
|
+
if (!fs.existsSync(CONFIG_PATH)) return null;
|
|
16
|
+
return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
|
|
17
|
+
}
|
|
18
|
+
function writeConfig(cfg) {
|
|
19
|
+
ensureDir(CONFIG_DIR);
|
|
20
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2));
|
|
21
|
+
}
|
|
22
|
+
async function api(baseUrl, endpoint, method, apiKey, body) {
|
|
23
|
+
const res = await fetch(`${baseUrl}${endpoint}`, {
|
|
24
|
+
method,
|
|
25
|
+
headers: {
|
|
26
|
+
"content-type": "application/json",
|
|
27
|
+
"x-clawon-api-key": apiKey
|
|
28
|
+
},
|
|
29
|
+
body: body ? JSON.stringify(body) : void 0
|
|
30
|
+
});
|
|
31
|
+
const json = await res.json().catch(() => ({}));
|
|
32
|
+
if (!res.ok) throw new Error(json.error || json.message || `HTTP ${res.status}`);
|
|
33
|
+
return json;
|
|
34
|
+
}
|
|
35
|
+
var INCLUDE_PATTERNS = [
|
|
36
|
+
"workspace/*.md",
|
|
37
|
+
"workspace/memory/*.md",
|
|
38
|
+
"workspace/memory/**/*.md",
|
|
39
|
+
"workspace/skills/**",
|
|
40
|
+
"workspace/canvas/**",
|
|
41
|
+
"skills/**",
|
|
42
|
+
"agents/*/config.json"
|
|
43
|
+
];
|
|
44
|
+
var EXCLUDE_PATTERNS = [
|
|
45
|
+
"credentials/**",
|
|
46
|
+
"agents/*/sessions/**",
|
|
47
|
+
"memory/lancedb/**",
|
|
48
|
+
"memory/*.sqlite",
|
|
49
|
+
"*.lock",
|
|
50
|
+
"*.wal",
|
|
51
|
+
"*.shm",
|
|
52
|
+
"node_modules/**",
|
|
53
|
+
".git/**",
|
|
54
|
+
".DS_Store",
|
|
55
|
+
"Thumbs.db"
|
|
56
|
+
];
|
|
57
|
+
function matchGlob(filePath, pattern) {
|
|
58
|
+
let regexPattern = pattern.replace(/\./g, "\\.").replace(/\*\*\//g, "(.*/)?").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*");
|
|
59
|
+
return new RegExp(`^${regexPattern}$`).test(filePath);
|
|
60
|
+
}
|
|
61
|
+
function shouldInclude(relativePath) {
|
|
62
|
+
for (const pattern of EXCLUDE_PATTERNS) {
|
|
63
|
+
if (matchGlob(relativePath, pattern)) return false;
|
|
64
|
+
}
|
|
65
|
+
for (const pattern of INCLUDE_PATTERNS) {
|
|
66
|
+
if (matchGlob(relativePath, pattern)) return true;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
function discoverFiles(baseDir) {
|
|
71
|
+
const files = [];
|
|
72
|
+
function walk(dir, relativePath = "") {
|
|
73
|
+
if (!fs.existsSync(dir)) return;
|
|
74
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
const fullPath = path.join(dir, entry.name);
|
|
77
|
+
const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
78
|
+
if (entry.isDirectory()) {
|
|
79
|
+
walk(fullPath, relPath);
|
|
80
|
+
} else if (entry.isFile()) {
|
|
81
|
+
if (shouldInclude(relPath)) {
|
|
82
|
+
const stats = fs.statSync(fullPath);
|
|
83
|
+
files.push({
|
|
84
|
+
path: relPath,
|
|
85
|
+
size: stats.size
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
walk(baseDir);
|
|
92
|
+
return files;
|
|
93
|
+
}
|
|
94
|
+
var program = new Command();
|
|
95
|
+
program.name("clawon").description("Backup and restore your OpenClaw workspace").version("0.1.1");
|
|
96
|
+
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) => {
|
|
97
|
+
try {
|
|
98
|
+
const connectJson = await api(opts.apiUrl, "/api/v1/profile/connect", "POST", opts.apiKey, {
|
|
99
|
+
profileName: "default",
|
|
100
|
+
instanceName: os.hostname(),
|
|
101
|
+
syncIntervalMinutes: 60
|
|
102
|
+
});
|
|
103
|
+
writeConfig({
|
|
104
|
+
apiKey: opts.apiKey,
|
|
105
|
+
profileId: connectJson.profileId,
|
|
106
|
+
apiBaseUrl: opts.apiUrl,
|
|
107
|
+
connectedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
108
|
+
});
|
|
109
|
+
console.log("\u2713 Logged in");
|
|
110
|
+
console.log(` Profile ID: ${connectJson.profileId}`);
|
|
111
|
+
} catch (e) {
|
|
112
|
+
console.error(`\u2717 Login failed: ${e.message}`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
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) => {
|
|
117
|
+
const cfg = readConfig();
|
|
118
|
+
if (!cfg) {
|
|
119
|
+
console.error("\u2717 Not logged in. Run: clawon login --api-key <key>");
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
if (!fs.existsSync(OPENCLAW_DIR)) {
|
|
123
|
+
console.error(`\u2717 OpenClaw directory not found: ${OPENCLAW_DIR}`);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
console.log("Discovering files...");
|
|
127
|
+
const files = discoverFiles(OPENCLAW_DIR);
|
|
128
|
+
if (files.length === 0) {
|
|
129
|
+
console.error("\u2717 No files found to backup");
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
|
|
133
|
+
const categories = {
|
|
134
|
+
workspace: files.filter((f) => f.path.startsWith("workspace/")),
|
|
135
|
+
skills: files.filter((f) => f.path.startsWith("skills/")),
|
|
136
|
+
agents: files.filter((f) => f.path.startsWith("agents/"))
|
|
137
|
+
};
|
|
138
|
+
console.log(`Found ${files.length} files (${(totalSize / 1024).toFixed(1)} KB):`);
|
|
139
|
+
if (categories.workspace.length) console.log(` \u2022 workspace: ${categories.workspace.length} files`);
|
|
140
|
+
if (categories.skills.length) console.log(` \u2022 skills: ${categories.skills.length} files`);
|
|
141
|
+
if (categories.agents.length) console.log(` \u2022 agents: ${categories.agents.length} files`);
|
|
142
|
+
if (opts.dryRun) {
|
|
143
|
+
console.log("\n[Dry run] Files that would be backed up:");
|
|
144
|
+
files.forEach((f) => console.log(` ${f.path} (${f.size} bytes)`));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
console.log("\nCreating backup...");
|
|
149
|
+
const { snapshotId, uploadUrls } = await api(
|
|
150
|
+
cfg.apiBaseUrl,
|
|
151
|
+
"/api/v1/backups/prepare",
|
|
152
|
+
"POST",
|
|
153
|
+
cfg.apiKey,
|
|
154
|
+
{
|
|
155
|
+
profileId: cfg.profileId,
|
|
156
|
+
files: files.map((f) => ({ path: f.path, size: f.size }))
|
|
157
|
+
}
|
|
158
|
+
);
|
|
159
|
+
console.log(`Uploading ${files.length} files...`);
|
|
160
|
+
let uploaded = 0;
|
|
161
|
+
for (const file of files) {
|
|
162
|
+
const fullPath = path.join(OPENCLAW_DIR, file.path);
|
|
163
|
+
const content = fs.readFileSync(fullPath);
|
|
164
|
+
const uploadRes = await fetch(uploadUrls[file.path], {
|
|
165
|
+
method: "PUT",
|
|
166
|
+
headers: { "content-type": "application/octet-stream" },
|
|
167
|
+
body: content
|
|
168
|
+
});
|
|
169
|
+
if (!uploadRes.ok) {
|
|
170
|
+
const errText = await uploadRes.text();
|
|
171
|
+
throw new Error(`Failed to upload ${file.path}: ${uploadRes.status} ${errText}`);
|
|
172
|
+
}
|
|
173
|
+
uploaded++;
|
|
174
|
+
process.stdout.write(`\r Uploaded: ${uploaded}/${files.length}`);
|
|
175
|
+
}
|
|
176
|
+
console.log("");
|
|
177
|
+
await api(cfg.apiBaseUrl, "/api/v1/backups/confirm", "POST", cfg.apiKey, {
|
|
178
|
+
snapshotId,
|
|
179
|
+
profileId: cfg.profileId
|
|
180
|
+
});
|
|
181
|
+
console.log("\n\u2713 Backup complete!");
|
|
182
|
+
console.log(` Snapshot ID: ${snapshotId}`);
|
|
183
|
+
console.log(` Files: ${files.length}`);
|
|
184
|
+
console.log(` Size: ${(totalSize / 1024).toFixed(1)} KB`);
|
|
185
|
+
} catch (e) {
|
|
186
|
+
console.error(`
|
|
187
|
+
\u2717 Backup failed: ${e.message}`);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
program.command("restore").description("Restore your OpenClaw workspace from the cloud").option("--snapshot <id>", "Specific snapshot ID to restore (default: latest)").option("--dry-run", "Show what would be restored without extracting").action(async (opts) => {
|
|
192
|
+
const cfg = readConfig();
|
|
193
|
+
if (!cfg) {
|
|
194
|
+
console.error("\u2717 Not logged in. Run: clawon login --api-key <key>");
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
console.log("Fetching backup...");
|
|
199
|
+
const { snapshot, files, downloadUrls } = await api(
|
|
200
|
+
cfg.apiBaseUrl,
|
|
201
|
+
"/api/v1/backups/download",
|
|
202
|
+
"POST",
|
|
203
|
+
cfg.apiKey,
|
|
204
|
+
{
|
|
205
|
+
profileId: cfg.profileId,
|
|
206
|
+
snapshotId: opts.snapshot || null
|
|
207
|
+
}
|
|
208
|
+
);
|
|
209
|
+
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
|
|
210
|
+
console.log(`Found backup from ${new Date(snapshot.created_at).toLocaleString()}`);
|
|
211
|
+
console.log(` Files: ${files.length}`);
|
|
212
|
+
console.log(` Size: ${(totalSize / 1024).toFixed(1)} KB`);
|
|
213
|
+
if (opts.dryRun) {
|
|
214
|
+
console.log("\n[Dry run] Files that would be restored:");
|
|
215
|
+
files.forEach((f) => console.log(` ${f.path}`));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
console.log("\nDownloading files...");
|
|
219
|
+
let downloaded = 0;
|
|
220
|
+
for (const file of files) {
|
|
221
|
+
const res = await fetch(downloadUrls[file.path]);
|
|
222
|
+
if (!res.ok) {
|
|
223
|
+
throw new Error(`Failed to download ${file.path}: ${res.status}`);
|
|
224
|
+
}
|
|
225
|
+
const content = Buffer.from(await res.arrayBuffer());
|
|
226
|
+
const targetPath = path.join(OPENCLAW_DIR, file.path);
|
|
227
|
+
ensureDir(path.dirname(targetPath));
|
|
228
|
+
fs.writeFileSync(targetPath, content);
|
|
229
|
+
downloaded++;
|
|
230
|
+
process.stdout.write(`\r Downloaded: ${downloaded}/${files.length}`);
|
|
231
|
+
}
|
|
232
|
+
console.log("");
|
|
233
|
+
console.log("\n\u2713 Restore complete!");
|
|
234
|
+
console.log(` Restored to: ${OPENCLAW_DIR}`);
|
|
235
|
+
console.log(` Files: ${files.length}`);
|
|
236
|
+
} catch (e) {
|
|
237
|
+
console.error(`
|
|
238
|
+
\u2717 Restore failed: ${e.message}`);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
program.command("list").description("List your backups").option("--limit <n>", "Number of backups to show", "10").action(async (opts) => {
|
|
243
|
+
const cfg = readConfig();
|
|
244
|
+
if (!cfg) {
|
|
245
|
+
console.error("\u2717 Not logged in. Run: clawon login --api-key <key>");
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
const { snapshots } = await api(
|
|
250
|
+
cfg.apiBaseUrl,
|
|
251
|
+
`/api/v1/snapshots/list?profileId=${cfg.profileId}&limit=${opts.limit}`,
|
|
252
|
+
"GET",
|
|
253
|
+
cfg.apiKey
|
|
254
|
+
);
|
|
255
|
+
if (!snapshots?.length) {
|
|
256
|
+
console.log("No backups yet. Run: clawon backup");
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
console.log("Your backups:\n");
|
|
260
|
+
console.log("ID | Date | Files | Size");
|
|
261
|
+
console.log("\u2500".repeat(80));
|
|
262
|
+
for (const s of snapshots) {
|
|
263
|
+
const date = new Date(s.created_at).toLocaleString();
|
|
264
|
+
const size = s.size_bytes ? `${(s.size_bytes / 1024).toFixed(1)} KB` : "N/A";
|
|
265
|
+
const files = s.changed_files_count || "N/A";
|
|
266
|
+
console.log(`${s.id} | ${date.padEnd(20)} | ${String(files).padEnd(5)} | ${size}`);
|
|
267
|
+
}
|
|
268
|
+
console.log(`
|
|
269
|
+
Total: ${snapshots.length} backup(s)`);
|
|
270
|
+
} catch (e) {
|
|
271
|
+
console.error(`\u2717 Failed to list backups: ${e.message}`);
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
program.command("files").description("List files in a backup").option("--snapshot <id>", "Snapshot ID (default: latest)").action(async (opts) => {
|
|
276
|
+
const cfg = readConfig();
|
|
277
|
+
if (!cfg) {
|
|
278
|
+
console.error("\u2717 Not logged in. Run: clawon login --api-key <key>");
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
const { snapshot, files } = await api(
|
|
283
|
+
cfg.apiBaseUrl,
|
|
284
|
+
"/api/v1/backups/files",
|
|
285
|
+
"POST",
|
|
286
|
+
cfg.apiKey,
|
|
287
|
+
{
|
|
288
|
+
profileId: cfg.profileId,
|
|
289
|
+
snapshotId: opts.snapshot || null
|
|
290
|
+
}
|
|
291
|
+
);
|
|
292
|
+
console.log(`Backup: ${snapshot.id}`);
|
|
293
|
+
console.log(`Date: ${new Date(snapshot.created_at).toLocaleString()}
|
|
294
|
+
`);
|
|
295
|
+
const tree = {};
|
|
296
|
+
for (const f of files) {
|
|
297
|
+
const dir = path.dirname(f.path);
|
|
298
|
+
if (!tree[dir]) tree[dir] = [];
|
|
299
|
+
tree[dir].push(f);
|
|
300
|
+
}
|
|
301
|
+
for (const dir of Object.keys(tree).sort()) {
|
|
302
|
+
console.log(`\u{1F4C1} ${dir}/`);
|
|
303
|
+
for (const f of tree[dir]) {
|
|
304
|
+
const name = path.basename(f.path);
|
|
305
|
+
console.log(` \u{1F4C4} ${name} (${f.size} bytes)`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
console.log(`
|
|
309
|
+
Total: ${files.length} files`);
|
|
310
|
+
} catch (e) {
|
|
311
|
+
console.error(`\u2717 Failed to list files: ${e.message}`);
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
program.command("status").description("Show current status").action(async () => {
|
|
316
|
+
const cfg = readConfig();
|
|
317
|
+
console.log("Clawon Status\n");
|
|
318
|
+
if (cfg) {
|
|
319
|
+
console.log(`\u2713 Logged in`);
|
|
320
|
+
console.log(` Profile ID: ${cfg.profileId}`);
|
|
321
|
+
console.log(` API: ${cfg.apiBaseUrl}`);
|
|
322
|
+
} else {
|
|
323
|
+
console.log(`\u2717 Not logged in`);
|
|
324
|
+
console.log(` Run: clawon login --api-key <key>`);
|
|
325
|
+
}
|
|
326
|
+
console.log("");
|
|
327
|
+
if (fs.existsSync(OPENCLAW_DIR)) {
|
|
328
|
+
const files = discoverFiles(OPENCLAW_DIR);
|
|
329
|
+
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
|
|
330
|
+
console.log(`\u2713 OpenClaw found: ${OPENCLAW_DIR}`);
|
|
331
|
+
console.log(` Backupable files: ${files.length} (${(totalSize / 1024).toFixed(1)} KB)`);
|
|
332
|
+
} else {
|
|
333
|
+
console.log(`\u2717 OpenClaw not found: ${OPENCLAW_DIR}`);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
program.command("logout").description("Remove local credentials").action(() => {
|
|
337
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
338
|
+
fs.unlinkSync(CONFIG_PATH);
|
|
339
|
+
console.log("\u2713 Logged out");
|
|
340
|
+
} else {
|
|
341
|
+
console.log("Already logged out");
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
program.parseAsync(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "clawon",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Backup and restore your OpenClaw workspace",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"clawon": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"dev": "tsx src/index.ts",
|
|
14
|
+
"build": "tsup src/index.ts --format esm --clean",
|
|
15
|
+
"prepublishOnly": "npm run build",
|
|
16
|
+
"start": "tsx src/index.ts"
|
|
17
|
+
},
|
|
18
|
+
"keywords": ["openclaw", "clawon", "backup", "restore", "cli"],
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"commander": "^12.1.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"tsup": "^8.2.4",
|
|
25
|
+
"tsx": "^4.19.2",
|
|
26
|
+
"typescript": "^5.6.2"
|
|
27
|
+
}
|
|
28
|
+
}
|