clawon 0.1.8 → 0.1.10
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/LICENSE +21 -0
- package/README.md +33 -2
- package/dist/index.js +294 -37
- package/package.json +1 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Clawon
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -21,6 +21,8 @@ Local backups are stored in `~/.clawon/backups/` as standard `.tar.gz` archives.
|
|
|
21
21
|
# Create a backup
|
|
22
22
|
npx clawon local backup
|
|
23
23
|
npx clawon local backup --tag "before migration"
|
|
24
|
+
npx clawon local backup --include-memory-db # Include SQLite memory index
|
|
25
|
+
npx clawon local backup --max-snapshots 10 # Keep only 10 most recent
|
|
24
26
|
|
|
25
27
|
# List all local backups
|
|
26
28
|
npx clawon local list
|
|
@@ -31,6 +33,27 @@ npx clawon local restore --pick 2 # Backup #2 from list
|
|
|
31
33
|
npx clawon local restore --file path.tar.gz # External file
|
|
32
34
|
```
|
|
33
35
|
|
|
36
|
+
### Scheduled Backups
|
|
37
|
+
|
|
38
|
+
Set up automatic backups via cron (macOS/Linux only).
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Schedule local backups every 12 hours (default)
|
|
42
|
+
npx clawon local schedule on
|
|
43
|
+
npx clawon local schedule on --every 6h --max-snapshots 10
|
|
44
|
+
npx clawon local schedule on --include-memory-db
|
|
45
|
+
|
|
46
|
+
# Disable local schedule
|
|
47
|
+
npx clawon local schedule off
|
|
48
|
+
|
|
49
|
+
# Schedule cloud backups (requires Hobby or Pro account)
|
|
50
|
+
npx clawon schedule on
|
|
51
|
+
npx clawon schedule off
|
|
52
|
+
|
|
53
|
+
# Check schedule status
|
|
54
|
+
npx clawon schedule status
|
|
55
|
+
```
|
|
56
|
+
|
|
34
57
|
### Cloud Backups (requires account)
|
|
35
58
|
|
|
36
59
|
Cloud backups sync your workspace to Clawon's servers for cross-machine access.
|
|
@@ -43,6 +66,7 @@ npx clawon login --api-key <your-key>
|
|
|
43
66
|
npx clawon backup
|
|
44
67
|
npx clawon backup --tag "stable config"
|
|
45
68
|
npx clawon backup --dry-run # Preview without uploading
|
|
69
|
+
npx clawon backup --include-memory-db # Requires Pro account
|
|
46
70
|
|
|
47
71
|
# List cloud backups
|
|
48
72
|
npx clawon list
|
|
@@ -63,6 +87,8 @@ npx clawon activity # Recent events
|
|
|
63
87
|
|
|
64
88
|
```bash
|
|
65
89
|
npx clawon discover # Show exactly which files would be backed up
|
|
90
|
+
npx clawon discover --include-memory-db # Include SQLite memory index
|
|
91
|
+
npx clawon schedule status # Show active schedules
|
|
66
92
|
npx clawon status # Connection status and file count
|
|
67
93
|
npx clawon logout # Remove local credentials
|
|
68
94
|
```
|
|
@@ -80,6 +106,9 @@ Clawon uses an **allowlist** — only files matching these patterns are included
|
|
|
80
106
|
| `workspace/canvas/**` | Canvas data |
|
|
81
107
|
| `skills/**` | Top-level skills |
|
|
82
108
|
| `agents/*/config.json` | Agent configurations |
|
|
109
|
+
| `agents/*/models.json` | Model preferences |
|
|
110
|
+
| `agents/*/agent/**` | Agent config data |
|
|
111
|
+
| `cron/runs/*.jsonl` | Cron run logs |
|
|
83
112
|
|
|
84
113
|
Run `npx clawon discover` to see the exact file list for your instance.
|
|
85
114
|
|
|
@@ -91,9 +120,11 @@ These are **always excluded**, even if they match an include pattern:
|
|
|
91
120
|
|---------|-----|
|
|
92
121
|
| `credentials/**` | API keys, tokens, auth files |
|
|
93
122
|
| `openclaw.json` | May contain credentials |
|
|
94
|
-
| `agents/*/
|
|
123
|
+
| `agents/*/auth.json` | Authentication data |
|
|
124
|
+
| `agents/*/auth-profiles.json` | Auth profiles |
|
|
125
|
+
| `agents/*/sessions/**` | Chat history (large, use future `--include-sessions`) |
|
|
95
126
|
| `memory/lancedb/**` | Vector database (binary, large) |
|
|
96
|
-
| `memory/*.sqlite` | SQLite databases |
|
|
127
|
+
| `memory/*.sqlite` | SQLite databases (use `--include-memory-db` to include) |
|
|
97
128
|
| `*.lock`, `*.wal`, `*.shm` | Database lock files |
|
|
98
129
|
| `node_modules/**` | Dependencies |
|
|
99
130
|
|
package/dist/index.js
CHANGED
|
@@ -5,6 +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 { execSync } from "child_process";
|
|
8
9
|
import * as tar from "tar";
|
|
9
10
|
var CONFIG_DIR = path.join(os.homedir(), ".clawon");
|
|
10
11
|
var CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
@@ -21,6 +22,15 @@ function writeConfig(cfg) {
|
|
|
21
22
|
ensureDir(CONFIG_DIR);
|
|
22
23
|
fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2));
|
|
23
24
|
}
|
|
25
|
+
function updateConfig(partial) {
|
|
26
|
+
const existing = readConfig() || {};
|
|
27
|
+
const merged = { ...existing, ...partial };
|
|
28
|
+
if (partial.schedule) {
|
|
29
|
+
merged.schedule = { ...existing.schedule, ...partial.schedule };
|
|
30
|
+
}
|
|
31
|
+
ensureDir(CONFIG_DIR);
|
|
32
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2));
|
|
33
|
+
}
|
|
24
34
|
async function api(baseUrl, endpoint, method, apiKey, body) {
|
|
25
35
|
const res = await fetch(`${baseUrl}${endpoint}`, {
|
|
26
36
|
method,
|
|
@@ -41,11 +51,16 @@ var INCLUDE_PATTERNS = [
|
|
|
41
51
|
"workspace/skills/**",
|
|
42
52
|
"workspace/canvas/**",
|
|
43
53
|
"skills/**",
|
|
44
|
-
"agents/*/config.json"
|
|
54
|
+
"agents/*/config.json",
|
|
55
|
+
"agents/*/models.json",
|
|
56
|
+
"agents/*/agent/**",
|
|
57
|
+
"cron/runs/*.jsonl"
|
|
45
58
|
];
|
|
46
59
|
var EXCLUDE_PATTERNS = [
|
|
47
60
|
"credentials/**",
|
|
48
61
|
"openclaw.json",
|
|
62
|
+
"agents/*/auth.json",
|
|
63
|
+
"agents/*/auth-profiles.json",
|
|
49
64
|
"agents/*/sessions/**",
|
|
50
65
|
"memory/lancedb/**",
|
|
51
66
|
"memory/*.sqlite",
|
|
@@ -61,7 +76,10 @@ function matchGlob(filePath, pattern) {
|
|
|
61
76
|
let regexPattern = pattern.replace(/\./g, "\\.").replace(/\*\*\//g, "(.*/)?").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*");
|
|
62
77
|
return new RegExp(`^${regexPattern}$`).test(filePath);
|
|
63
78
|
}
|
|
64
|
-
function shouldInclude(relativePath) {
|
|
79
|
+
function shouldInclude(relativePath, includeMemoryDb = false) {
|
|
80
|
+
if (includeMemoryDb && matchGlob(relativePath, "memory/*.sqlite")) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
65
83
|
for (const pattern of EXCLUDE_PATTERNS) {
|
|
66
84
|
if (matchGlob(relativePath, pattern)) return false;
|
|
67
85
|
}
|
|
@@ -70,7 +88,7 @@ function shouldInclude(relativePath) {
|
|
|
70
88
|
}
|
|
71
89
|
return false;
|
|
72
90
|
}
|
|
73
|
-
function discoverFiles(baseDir) {
|
|
91
|
+
function discoverFiles(baseDir, includeMemoryDb = false) {
|
|
74
92
|
const files = [];
|
|
75
93
|
function walk(dir, relativePath = "") {
|
|
76
94
|
if (!fs.existsSync(dir)) return;
|
|
@@ -81,7 +99,7 @@ function discoverFiles(baseDir) {
|
|
|
81
99
|
if (entry.isDirectory()) {
|
|
82
100
|
walk(fullPath, relPath);
|
|
83
101
|
} else if (entry.isFile()) {
|
|
84
|
-
if (shouldInclude(relPath)) {
|
|
102
|
+
if (shouldInclude(relPath, includeMemoryDb)) {
|
|
85
103
|
const stats = fs.statSync(fullPath);
|
|
86
104
|
files.push({
|
|
87
105
|
path: relPath,
|
|
@@ -94,12 +112,14 @@ function discoverFiles(baseDir) {
|
|
|
94
112
|
walk(baseDir);
|
|
95
113
|
return files;
|
|
96
114
|
}
|
|
97
|
-
async function createLocalArchive(files, openclawDir, outputPath, tag) {
|
|
115
|
+
async function createLocalArchive(files, openclawDir, outputPath, tag, includeMemoryDb, trigger) {
|
|
98
116
|
const meta = {
|
|
99
117
|
version: 2,
|
|
100
118
|
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
101
119
|
...tag ? { tag } : {},
|
|
102
|
-
file_count: files.length
|
|
120
|
+
file_count: files.length,
|
|
121
|
+
...includeMemoryDb ? { include_memory_db: true } : {},
|
|
122
|
+
...trigger ? { trigger } : {}
|
|
103
123
|
};
|
|
104
124
|
const metaPath = path.join(openclawDir, "_clawon_meta.json");
|
|
105
125
|
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
|
|
@@ -153,6 +173,67 @@ function trackCliEvent(distinctId, event, properties = {}) {
|
|
|
153
173
|
}).catch(() => {
|
|
154
174
|
});
|
|
155
175
|
}
|
|
176
|
+
var CRON_MARKER_LOCAL = "# clawon-schedule-local";
|
|
177
|
+
var CRON_MARKER_CLOUD = "# clawon-schedule-cloud";
|
|
178
|
+
var SCHEDULE_LOG = path.join(CONFIG_DIR, "schedule.log");
|
|
179
|
+
var INTERVAL_CRON = {
|
|
180
|
+
"1h": "0 * * * *",
|
|
181
|
+
"6h": "0 */6 * * *",
|
|
182
|
+
"12h": "0 */12 * * *",
|
|
183
|
+
"24h": "0 0 * * *"
|
|
184
|
+
};
|
|
185
|
+
var VALID_INTERVALS = Object.keys(INTERVAL_CRON);
|
|
186
|
+
function resolveCliCommand(args) {
|
|
187
|
+
const nodePath = process.execPath;
|
|
188
|
+
const cliEntry = path.resolve(import.meta.dirname, "index.js");
|
|
189
|
+
return `${nodePath} ${cliEntry} ${args}`;
|
|
190
|
+
}
|
|
191
|
+
function getCurrentCrontab() {
|
|
192
|
+
try {
|
|
193
|
+
return execSync("crontab -l 2>/dev/null", { encoding: "utf8" });
|
|
194
|
+
} catch {
|
|
195
|
+
return "";
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function setCrontab(content) {
|
|
199
|
+
const tmpFile = path.join(CONFIG_DIR, ".crontab-tmp");
|
|
200
|
+
ensureDir(CONFIG_DIR);
|
|
201
|
+
fs.writeFileSync(tmpFile, content);
|
|
202
|
+
try {
|
|
203
|
+
execSync(`crontab ${tmpFile}`, { encoding: "utf8" });
|
|
204
|
+
} finally {
|
|
205
|
+
fs.unlinkSync(tmpFile);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function addCronEntry(marker, cronExpr, command) {
|
|
209
|
+
const current = getCurrentCrontab();
|
|
210
|
+
const filtered = current.split("\n").filter((line) => !line.includes(marker)).join("\n");
|
|
211
|
+
const entry = `${cronExpr} ${command} >> ${SCHEDULE_LOG} 2>&1 ${marker}`;
|
|
212
|
+
const updated = filtered.trim() ? `${filtered.trim()}
|
|
213
|
+
${entry}
|
|
214
|
+
` : `${entry}
|
|
215
|
+
`;
|
|
216
|
+
setCrontab(updated);
|
|
217
|
+
}
|
|
218
|
+
function removeCronEntry(marker) {
|
|
219
|
+
const current = getCurrentCrontab();
|
|
220
|
+
const lines = current.split("\n");
|
|
221
|
+
const filtered = lines.filter((line) => !line.includes(marker));
|
|
222
|
+
if (filtered.length === lines.length) return false;
|
|
223
|
+
setCrontab(filtered.join("\n"));
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
function getCronEntry(marker) {
|
|
227
|
+
const current = getCurrentCrontab();
|
|
228
|
+
const line = current.split("\n").find((l) => l.includes(marker));
|
|
229
|
+
return line || null;
|
|
230
|
+
}
|
|
231
|
+
function assertNotWindows() {
|
|
232
|
+
if (process.platform === "win32") {
|
|
233
|
+
console.error("\u2717 Scheduled backups require cron (macOS/Linux). Windows is not supported yet.");
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
156
237
|
var program = new Command();
|
|
157
238
|
program.name("clawon").description("Backup and restore your OpenClaw workspace").version("0.1.1");
|
|
158
239
|
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) => {
|
|
@@ -176,7 +257,11 @@ program.command("login").description("Connect to Clawon with your API key").requ
|
|
|
176
257
|
process.exit(1);
|
|
177
258
|
}
|
|
178
259
|
});
|
|
179
|
-
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) => {
|
|
260
|
+
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").option("--include-memory-db", "Include SQLite memory index").option("--scheduled", "Internal: triggered by cron (suppresses interactive output)").action(async (opts) => {
|
|
261
|
+
if (opts.includeMemoryDb) {
|
|
262
|
+
console.error("\u2717 Memory DB cloud backup requires a Pro account. Use `clawon local backup --include-memory-db` for local backups.");
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
180
265
|
const cfg = readConfig();
|
|
181
266
|
if (!cfg) {
|
|
182
267
|
console.error("\u2717 Not logged in. Run: clawon login --api-key <key>");
|
|
@@ -246,12 +331,18 @@ program.command("backup").description("Backup your OpenClaw workspace to the clo
|
|
|
246
331
|
console.log(` Snapshot ID: ${snapshotId}`);
|
|
247
332
|
console.log(` Files: ${files.length}`);
|
|
248
333
|
console.log(` Size: ${(totalSize / 1024).toFixed(1)} KB`);
|
|
249
|
-
trackCliEvent(cfg.profileId, "cloud_backup_created", {
|
|
334
|
+
trackCliEvent(cfg.profileId, opts.scheduled ? "scheduled_backup_created" : "cloud_backup_created", {
|
|
250
335
|
file_count: files.length,
|
|
251
|
-
total_bytes: totalSize
|
|
336
|
+
total_bytes: totalSize,
|
|
337
|
+
include_memory_db: !!opts.includeMemoryDb,
|
|
338
|
+
type: "cloud",
|
|
339
|
+
trigger: opts.scheduled ? "scheduled" : "manual"
|
|
252
340
|
});
|
|
253
341
|
} catch (e) {
|
|
254
342
|
const msg = e.message;
|
|
343
|
+
if (opts.scheduled) {
|
|
344
|
+
trackCliEvent(cfg.profileId, "scheduled_backup_failed", { type: "cloud", error: msg });
|
|
345
|
+
}
|
|
255
346
|
if (msg.includes("Snapshot limit")) {
|
|
256
347
|
console.error("\n\u2717 Snapshot limit reached (2).");
|
|
257
348
|
console.error(" Delete one first: clawon delete <id>");
|
|
@@ -455,12 +546,12 @@ program.command("delete [id]").description("Delete a snapshot").option("--oldest
|
|
|
455
546
|
process.exit(1);
|
|
456
547
|
}
|
|
457
548
|
});
|
|
458
|
-
program.command("discover").description("Preview which files would be included in a backup").action(async () => {
|
|
549
|
+
program.command("discover").description("Preview which files would be included in a backup").option("--include-memory-db", "Include SQLite memory index").action(async (opts) => {
|
|
459
550
|
if (!fs.existsSync(OPENCLAW_DIR)) {
|
|
460
551
|
console.error(`\u2717 OpenClaw directory not found: ${OPENCLAW_DIR}`);
|
|
461
552
|
process.exit(1);
|
|
462
553
|
}
|
|
463
|
-
const files = discoverFiles(OPENCLAW_DIR);
|
|
554
|
+
const files = discoverFiles(OPENCLAW_DIR, !!opts.includeMemoryDb);
|
|
464
555
|
if (files.length === 0) {
|
|
465
556
|
console.log("No files matched the include patterns.");
|
|
466
557
|
return;
|
|
@@ -485,7 +576,70 @@ program.command("discover").description("Preview which files would be included i
|
|
|
485
576
|
Total: ${files.length} files (${(totalSize / 1024).toFixed(1)} KB)`);
|
|
486
577
|
console.log(`Source: ${OPENCLAW_DIR}`);
|
|
487
578
|
const cfg = readConfig();
|
|
488
|
-
trackCliEvent(cfg?.profileId || "anonymous", "cli_discover", { file_count: files.length });
|
|
579
|
+
trackCliEvent(cfg?.profileId || "anonymous", "cli_discover", { file_count: files.length, include_memory_db: !!opts.includeMemoryDb });
|
|
580
|
+
});
|
|
581
|
+
var schedule = program.command("schedule").description("Manage scheduled cloud backups");
|
|
582
|
+
schedule.command("on").description("Enable scheduled cloud backups via cron").option("--every <interval>", "Backup interval: 1h, 6h, 12h, 24h", "12h").action(async (opts) => {
|
|
583
|
+
assertNotWindows();
|
|
584
|
+
const cfg = readConfig();
|
|
585
|
+
trackCliEvent(cfg?.profileId || "anonymous", "schedule_enabled_attempted", {
|
|
586
|
+
type: "cloud",
|
|
587
|
+
interval_hours: parseInt(opts.every),
|
|
588
|
+
gated: true
|
|
589
|
+
});
|
|
590
|
+
console.error("\u2717 Scheduled cloud backups require a Hobby or Pro account. Use `clawon local schedule on` for local scheduled backups.");
|
|
591
|
+
process.exit(1);
|
|
592
|
+
});
|
|
593
|
+
schedule.command("off").description("Disable scheduled cloud backups").action(async () => {
|
|
594
|
+
assertNotWindows();
|
|
595
|
+
const removed = removeCronEntry(CRON_MARKER_CLOUD);
|
|
596
|
+
if (!removed) {
|
|
597
|
+
console.log("No cloud schedule was active.");
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
updateConfig({
|
|
601
|
+
schedule: {
|
|
602
|
+
cloud: { enabled: false, intervalHours: 0 }
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
console.log("\u2713 Scheduled cloud backup disabled");
|
|
606
|
+
const cfg = readConfig();
|
|
607
|
+
trackCliEvent(cfg?.profileId || "anonymous", "schedule_disabled", { type: "cloud" });
|
|
608
|
+
});
|
|
609
|
+
schedule.command("status").description("Show schedule status").action(async () => {
|
|
610
|
+
const cfg = readConfig();
|
|
611
|
+
const localEntry = getCronEntry(CRON_MARKER_LOCAL);
|
|
612
|
+
const cloudEntry = getCronEntry(CRON_MARKER_CLOUD);
|
|
613
|
+
console.log("Schedule Status\n");
|
|
614
|
+
if (localEntry) {
|
|
615
|
+
const localCfg = cfg?.schedule?.local;
|
|
616
|
+
console.log("\u2713 Local schedule: active");
|
|
617
|
+
if (localCfg?.intervalHours) console.log(` Interval: every ${localCfg.intervalHours}h`);
|
|
618
|
+
if (localCfg?.maxSnapshots) console.log(` Max snapshots: ${localCfg.maxSnapshots}`);
|
|
619
|
+
if (localCfg?.includeMemoryDb) console.log(` Memory DB: included`);
|
|
620
|
+
console.log(` Cron: ${localEntry.trim()}`);
|
|
621
|
+
} else {
|
|
622
|
+
console.log("\u2717 Local schedule: inactive");
|
|
623
|
+
console.log(" Enable: clawon local schedule on");
|
|
624
|
+
}
|
|
625
|
+
console.log("");
|
|
626
|
+
if (cloudEntry) {
|
|
627
|
+
const cloudCfg = cfg?.schedule?.cloud;
|
|
628
|
+
console.log("\u2713 Cloud schedule: active");
|
|
629
|
+
if (cloudCfg?.intervalHours) console.log(` Interval: every ${cloudCfg.intervalHours}h`);
|
|
630
|
+
console.log(` Cron: ${cloudEntry.trim()}`);
|
|
631
|
+
} else {
|
|
632
|
+
console.log("\u2717 Cloud schedule: inactive");
|
|
633
|
+
console.log(" Enable: clawon schedule on (requires Hobby or Pro)");
|
|
634
|
+
}
|
|
635
|
+
console.log(`
|
|
636
|
+
Log: ${SCHEDULE_LOG}`);
|
|
637
|
+
trackCliEvent(cfg?.profileId || "anonymous", "schedule_status_viewed", {
|
|
638
|
+
local_enabled: !!localEntry,
|
|
639
|
+
cloud_enabled: !!cloudEntry,
|
|
640
|
+
local_interval_hours: cfg?.schedule?.local?.intervalHours || null,
|
|
641
|
+
cloud_interval_hours: cfg?.schedule?.cloud?.intervalHours || null
|
|
642
|
+
});
|
|
489
643
|
});
|
|
490
644
|
program.command("files").description("List files in a backup").option("--snapshot <id>", "Snapshot ID (default: latest)").action(async (opts) => {
|
|
491
645
|
const cfg = readConfig();
|
|
@@ -529,37 +683,129 @@ Total: ${files.length} files`);
|
|
|
529
683
|
}
|
|
530
684
|
});
|
|
531
685
|
var local = program.command("local").description("Local backup and restore (no cloud required)");
|
|
532
|
-
local.command("
|
|
686
|
+
var localSchedule = local.command("schedule").description("Manage scheduled local backups");
|
|
687
|
+
localSchedule.command("on").description("Enable scheduled local backups via cron").option("--every <interval>", "Backup interval: 1h, 6h, 12h, 24h", "12h").option("--max-snapshots <n>", "Keep only the N most recent local backups").option("--include-memory-db", "Include SQLite memory index").action(async (opts) => {
|
|
688
|
+
assertNotWindows();
|
|
689
|
+
const interval = opts.every;
|
|
690
|
+
if (!VALID_INTERVALS.includes(interval)) {
|
|
691
|
+
console.error(`\u2717 Invalid interval: ${interval}. Valid options: ${VALID_INTERVALS.join(", ")}`);
|
|
692
|
+
process.exit(1);
|
|
693
|
+
}
|
|
694
|
+
const cfg = readConfig();
|
|
695
|
+
const wasEnabled = cfg?.schedule?.local?.enabled;
|
|
696
|
+
const cronExpr = INTERVAL_CRON[interval];
|
|
697
|
+
const args = "local backup --scheduled" + (opts.includeMemoryDb ? " --include-memory-db" : "") + (opts.maxSnapshots ? ` --max-snapshots ${opts.maxSnapshots}` : "");
|
|
698
|
+
const command = resolveCliCommand(args);
|
|
699
|
+
addCronEntry(CRON_MARKER_LOCAL, cronExpr, command);
|
|
700
|
+
const maxSnapshots = opts.maxSnapshots ? parseInt(opts.maxSnapshots, 10) : null;
|
|
701
|
+
updateConfig({
|
|
702
|
+
schedule: {
|
|
703
|
+
local: {
|
|
704
|
+
enabled: true,
|
|
705
|
+
intervalHours: parseInt(interval),
|
|
706
|
+
...maxSnapshots ? { maxSnapshots } : {},
|
|
707
|
+
...opts.includeMemoryDb ? { includeMemoryDb: true } : {}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
console.log(`\u2713 Scheduled local backup enabled`);
|
|
712
|
+
console.log(` Interval: every ${interval}`);
|
|
713
|
+
if (maxSnapshots) console.log(` Max snapshots: ${maxSnapshots}`);
|
|
714
|
+
if (opts.includeMemoryDb) console.log(` Memory DB: included`);
|
|
715
|
+
console.log(` Log: ${SCHEDULE_LOG}`);
|
|
716
|
+
const firstRunArgs = "local backup" + (opts.includeMemoryDb ? " --include-memory-db" : "") + (opts.maxSnapshots ? ` --max-snapshots ${opts.maxSnapshots}` : "");
|
|
717
|
+
console.log("\nRunning first backup now...\n");
|
|
718
|
+
try {
|
|
719
|
+
execSync(resolveCliCommand(firstRunArgs), { stdio: "inherit" });
|
|
720
|
+
} catch {
|
|
721
|
+
console.error("\u26A0 First backup failed \u2014 schedule is still active and will retry at next interval.");
|
|
722
|
+
}
|
|
723
|
+
trackCliEvent(cfg?.profileId || "anonymous", wasEnabled ? "schedule_updated" : "schedule_enabled", {
|
|
724
|
+
type: "local",
|
|
725
|
+
interval_hours: parseInt(interval),
|
|
726
|
+
max_snapshots: maxSnapshots,
|
|
727
|
+
include_memory_db: !!opts.includeMemoryDb
|
|
728
|
+
});
|
|
729
|
+
});
|
|
730
|
+
localSchedule.command("off").description("Disable scheduled local backups").action(async () => {
|
|
731
|
+
assertNotWindows();
|
|
732
|
+
const removed = removeCronEntry(CRON_MARKER_LOCAL);
|
|
733
|
+
if (!removed) {
|
|
734
|
+
console.log("No local schedule was active.");
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
updateConfig({
|
|
738
|
+
schedule: {
|
|
739
|
+
local: { enabled: false, intervalHours: 0 }
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
console.log("\u2713 Scheduled local backup disabled");
|
|
743
|
+
const cfg = readConfig();
|
|
744
|
+
trackCliEvent(cfg?.profileId || "anonymous", "schedule_disabled", { type: "local" });
|
|
745
|
+
});
|
|
746
|
+
local.command("backup").description("Save a local backup of your OpenClaw workspace").option("--tag <label>", "Add a label to this backup").option("--include-memory-db", "Include SQLite memory index").option("--scheduled", "Internal: triggered by cron (suppresses interactive output)").option("--max-snapshots <n>", "Keep only the N most recent local backups").action(async (opts) => {
|
|
533
747
|
if (!fs.existsSync(OPENCLAW_DIR)) {
|
|
534
748
|
console.error(`\u2717 OpenClaw directory not found: ${OPENCLAW_DIR}`);
|
|
535
749
|
process.exit(1);
|
|
536
750
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
751
|
+
try {
|
|
752
|
+
if (!opts.scheduled) console.log("Discovering files...");
|
|
753
|
+
const files = discoverFiles(OPENCLAW_DIR, !!opts.includeMemoryDb);
|
|
754
|
+
if (files.length === 0) {
|
|
755
|
+
console.error("\u2717 No files found to backup");
|
|
756
|
+
process.exit(1);
|
|
757
|
+
}
|
|
758
|
+
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
|
|
759
|
+
if (!opts.scheduled) console.log(`Found ${files.length} files (${(totalSize / 1024).toFixed(1)} KB)`);
|
|
760
|
+
ensureDir(BACKUPS_DIR);
|
|
761
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "").replace("T", "T").slice(0, 15);
|
|
762
|
+
const filename = `backup-${timestamp}.tar.gz`;
|
|
763
|
+
const filePath = path.join(BACKUPS_DIR, filename);
|
|
764
|
+
if (!opts.scheduled) console.log("Creating archive...");
|
|
765
|
+
await createLocalArchive(files, OPENCLAW_DIR, filePath, opts.tag, opts.includeMemoryDb, opts.scheduled ? "scheduled" : "manual");
|
|
766
|
+
const archiveSize = fs.statSync(filePath).size;
|
|
767
|
+
if (!opts.scheduled) {
|
|
768
|
+
console.log(`
|
|
769
|
+
\u2713 Local backup saved!`);
|
|
770
|
+
console.log(` File: ${filePath}`);
|
|
771
|
+
console.log(` Files: ${files.length}`);
|
|
772
|
+
console.log(` Size: ${(archiveSize / 1024).toFixed(1)} KB (compressed)`);
|
|
773
|
+
if (opts.tag) console.log(` Tag: ${opts.tag}`);
|
|
774
|
+
}
|
|
775
|
+
const maxSnapshots = opts.maxSnapshots ? parseInt(opts.maxSnapshots, 10) : null;
|
|
776
|
+
let rotatedCount = 0;
|
|
777
|
+
if (maxSnapshots && maxSnapshots > 0) {
|
|
778
|
+
const allBackups = fs.readdirSync(BACKUPS_DIR).filter((f) => f.endsWith(".tar.gz")).sort().reverse();
|
|
779
|
+
if (allBackups.length > maxSnapshots) {
|
|
780
|
+
const toDelete = allBackups.slice(maxSnapshots);
|
|
781
|
+
for (const old of toDelete) {
|
|
782
|
+
fs.unlinkSync(path.join(BACKUPS_DIR, old));
|
|
783
|
+
rotatedCount++;
|
|
784
|
+
}
|
|
785
|
+
if (!opts.scheduled) {
|
|
786
|
+
console.log(` Rotated: deleted ${rotatedCount} old backup(s)`);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
const cfg = readConfig();
|
|
791
|
+
trackCliEvent(cfg?.profileId || "anonymous", opts.scheduled ? "scheduled_backup_created" : "local_backup_created", {
|
|
792
|
+
file_count: files.length,
|
|
793
|
+
total_bytes: totalSize,
|
|
794
|
+
include_memory_db: !!opts.includeMemoryDb,
|
|
795
|
+
type: "local",
|
|
796
|
+
trigger: opts.scheduled ? "scheduled" : "manual",
|
|
797
|
+
rotated_count: rotatedCount
|
|
798
|
+
});
|
|
799
|
+
} catch (e) {
|
|
800
|
+
const msg = e.message;
|
|
801
|
+
if (opts.scheduled) {
|
|
802
|
+
const cfg = readConfig();
|
|
803
|
+
trackCliEvent(cfg?.profileId || "anonymous", "scheduled_backup_failed", { type: "local", error: msg });
|
|
804
|
+
}
|
|
805
|
+
console.error(`
|
|
806
|
+
\u2717 Local backup failed: ${msg}`);
|
|
541
807
|
process.exit(1);
|
|
542
808
|
}
|
|
543
|
-
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
|
|
544
|
-
console.log(`Found ${files.length} files (${(totalSize / 1024).toFixed(1)} KB)`);
|
|
545
|
-
ensureDir(BACKUPS_DIR);
|
|
546
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "").replace("T", "T").slice(0, 15);
|
|
547
|
-
const filename = `backup-${timestamp}.tar.gz`;
|
|
548
|
-
const filePath = path.join(BACKUPS_DIR, filename);
|
|
549
|
-
console.log("Creating archive...");
|
|
550
|
-
await createLocalArchive(files, OPENCLAW_DIR, filePath, opts.tag);
|
|
551
|
-
const archiveSize = fs.statSync(filePath).size;
|
|
552
|
-
console.log(`
|
|
553
|
-
\u2713 Local backup saved!`);
|
|
554
|
-
console.log(` File: ${filePath}`);
|
|
555
|
-
console.log(` Files: ${files.length}`);
|
|
556
|
-
console.log(` Size: ${(archiveSize / 1024).toFixed(1)} KB (compressed)`);
|
|
557
|
-
if (opts.tag) console.log(` Tag: ${opts.tag}`);
|
|
558
|
-
const cfg = readConfig();
|
|
559
|
-
trackCliEvent(cfg?.profileId || "anonymous", "local_backup_created", {
|
|
560
|
-
file_count: files.length,
|
|
561
|
-
total_bytes: totalSize
|
|
562
|
-
});
|
|
563
809
|
});
|
|
564
810
|
local.command("list").description("List local backups").action(async () => {
|
|
565
811
|
if (!fs.existsSync(BACKUPS_DIR)) {
|
|
@@ -671,6 +917,17 @@ program.command("status").description("Show current status").action(async () =>
|
|
|
671
917
|
} else {
|
|
672
918
|
console.log(`\u2717 OpenClaw not found: ${OPENCLAW_DIR}`);
|
|
673
919
|
}
|
|
920
|
+
console.log("");
|
|
921
|
+
const localEntry = getCronEntry(CRON_MARKER_LOCAL);
|
|
922
|
+
const cloudEntry = getCronEntry(CRON_MARKER_CLOUD);
|
|
923
|
+
if (localEntry || cloudEntry) {
|
|
924
|
+
console.log("\u2713 Schedule active");
|
|
925
|
+
if (localEntry) console.log(" Local: " + (cfg?.schedule?.local?.intervalHours || "?") + "h interval");
|
|
926
|
+
if (cloudEntry) console.log(" Cloud: " + (cfg?.schedule?.cloud?.intervalHours || "?") + "h interval");
|
|
927
|
+
} else {
|
|
928
|
+
console.log("\u2717 No schedule configured");
|
|
929
|
+
console.log(" Run: clawon local schedule on");
|
|
930
|
+
}
|
|
674
931
|
trackCliEvent(cfg?.profileId || "anonymous", "cli_status_viewed");
|
|
675
932
|
});
|
|
676
933
|
program.command("logout").description("Remove local credentials").action(() => {
|