clawon 0.1.9 → 0.1.11

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 +38 -2
  2. package/dist/index.js +308 -42
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -22,6 +22,8 @@ Local backups are stored in `~/.clawon/backups/` as standard `.tar.gz` archives.
22
22
  npx clawon local backup
23
23
  npx clawon local backup --tag "before migration"
24
24
  npx clawon local backup --include-memory-db # Include SQLite memory index
25
+ npx clawon local backup --include-sessions # Include chat history
26
+ npx clawon local backup --max-snapshots 10 # Keep only 10 most recent
25
27
 
26
28
  # List all local backups
27
29
  npx clawon local list
@@ -32,12 +34,38 @@ npx clawon local restore --pick 2 # Backup #2 from list
32
34
  npx clawon local restore --file path.tar.gz # External file
33
35
  ```
34
36
 
37
+ ### Scheduled Backups
38
+
39
+ Set up automatic backups via cron (macOS/Linux only).
40
+
41
+ ```bash
42
+ # Schedule local backups every 12 hours (default)
43
+ npx clawon local schedule on
44
+ npx clawon local schedule on --every 6h --max-snapshots 10
45
+ npx clawon local schedule on --include-memory-db
46
+ npx clawon local schedule on --include-sessions
47
+
48
+ # Disable local schedule
49
+ npx clawon local schedule off
50
+
51
+ # Schedule cloud backups (requires Hobby or Pro account)
52
+ npx clawon schedule on
53
+ npx clawon schedule off
54
+
55
+ # Check schedule status
56
+ npx clawon schedule status
57
+ ```
58
+
35
59
  ### Cloud Backups (requires account)
36
60
 
37
61
  Cloud backups sync your workspace to Clawon's servers for cross-machine access.
38
62
 
39
63
  ```bash
40
- # Authenticate
64
+ # Authenticate (env var recommended to avoid shell history)
65
+ export CLAWON_API_KEY=<your-key>
66
+ npx clawon login
67
+
68
+ # Or inline (key may appear in shell history)
41
69
  npx clawon login --api-key <your-key>
42
70
 
43
71
  # Create a cloud backup
@@ -45,6 +73,7 @@ npx clawon backup
45
73
  npx clawon backup --tag "stable config"
46
74
  npx clawon backup --dry-run # Preview without uploading
47
75
  npx clawon backup --include-memory-db # Requires Pro account
76
+ npx clawon backup --include-sessions # Requires Hobby or Pro
48
77
 
49
78
  # List cloud backups
50
79
  npx clawon list
@@ -66,6 +95,8 @@ npx clawon activity # Recent events
66
95
  ```bash
67
96
  npx clawon discover # Show exactly which files would be backed up
68
97
  npx clawon discover --include-memory-db # Include SQLite memory index
98
+ npx clawon discover --include-sessions # Include chat history
99
+ npx clawon schedule status # Show active schedules
69
100
  npx clawon status # Connection status and file count
70
101
  npx clawon logout # Remove local credentials
71
102
  ```
@@ -83,6 +114,9 @@ Clawon uses an **allowlist** — only files matching these patterns are included
83
114
  | `workspace/canvas/**` | Canvas data |
84
115
  | `skills/**` | Top-level skills |
85
116
  | `agents/*/config.json` | Agent configurations |
117
+ | `agents/*/models.json` | Model preferences |
118
+ | `agents/*/agent/**` | Agent config data |
119
+ | `cron/runs/*.jsonl` | Cron run logs |
86
120
 
87
121
  Run `npx clawon discover` to see the exact file list for your instance.
88
122
 
@@ -94,7 +128,9 @@ These are **always excluded**, even if they match an include pattern:
94
128
  |---------|-----|
95
129
  | `credentials/**` | API keys, tokens, auth files |
96
130
  | `openclaw.json` | May contain credentials |
97
- | `agents/*/sessions/**` | Ephemeral session data |
131
+ | `agents/*/auth.json` | Authentication data |
132
+ | `agents/*/auth-profiles.json` | Auth profiles |
133
+ | `agents/*/sessions/**` | Chat history (large, use `--include-sessions` to include) |
98
134
  | `memory/lancedb/**` | Vector database (binary, large) |
99
135
  | `memory/*.sqlite` | SQLite databases (use `--include-memory-db` to include) |
100
136
  | `*.lock`, `*.wal`, `*.shm` | Database lock files |
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,10 +76,13 @@ 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, includeMemoryDb = false) {
79
+ function shouldInclude(relativePath, includeMemoryDb = false, includeSessions = false) {
65
80
  if (includeMemoryDb && matchGlob(relativePath, "memory/*.sqlite")) {
66
81
  return true;
67
82
  }
83
+ if (includeSessions && matchGlob(relativePath, "agents/*/sessions/**")) {
84
+ return true;
85
+ }
68
86
  for (const pattern of EXCLUDE_PATTERNS) {
69
87
  if (matchGlob(relativePath, pattern)) return false;
70
88
  }
@@ -73,7 +91,7 @@ function shouldInclude(relativePath, includeMemoryDb = false) {
73
91
  }
74
92
  return false;
75
93
  }
76
- function discoverFiles(baseDir, includeMemoryDb = false) {
94
+ function discoverFiles(baseDir, includeMemoryDb = false, includeSessions = false) {
77
95
  const files = [];
78
96
  function walk(dir, relativePath = "") {
79
97
  if (!fs.existsSync(dir)) return;
@@ -84,7 +102,7 @@ function discoverFiles(baseDir, includeMemoryDb = false) {
84
102
  if (entry.isDirectory()) {
85
103
  walk(fullPath, relPath);
86
104
  } else if (entry.isFile()) {
87
- if (shouldInclude(relPath, includeMemoryDb)) {
105
+ if (shouldInclude(relPath, includeMemoryDb, includeSessions)) {
88
106
  const stats = fs.statSync(fullPath);
89
107
  files.push({
90
108
  path: relPath,
@@ -97,13 +115,15 @@ function discoverFiles(baseDir, includeMemoryDb = false) {
97
115
  walk(baseDir);
98
116
  return files;
99
117
  }
100
- async function createLocalArchive(files, openclawDir, outputPath, tag, includeMemoryDb) {
118
+ async function createLocalArchive(files, openclawDir, outputPath, tag, includeMemoryDb, includeSessions, trigger) {
101
119
  const meta = {
102
120
  version: 2,
103
121
  created: (/* @__PURE__ */ new Date()).toISOString(),
104
122
  ...tag ? { tag } : {},
105
123
  file_count: files.length,
106
- ...includeMemoryDb ? { include_memory_db: true } : {}
124
+ ...includeMemoryDb ? { include_memory_db: true } : {},
125
+ ...includeSessions ? { include_sessions: true } : {},
126
+ ...trigger ? { trigger } : {}
107
127
  };
108
128
  const metaPath = path.join(openclawDir, "_clawon_meta.json");
109
129
  fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
@@ -157,17 +177,83 @@ function trackCliEvent(distinctId, event, properties = {}) {
157
177
  }).catch(() => {
158
178
  });
159
179
  }
180
+ var CRON_MARKER_LOCAL = "# clawon-schedule-local";
181
+ var CRON_MARKER_CLOUD = "# clawon-schedule-cloud";
182
+ var SCHEDULE_LOG = path.join(CONFIG_DIR, "schedule.log");
183
+ var INTERVAL_CRON = {
184
+ "1h": "0 * * * *",
185
+ "6h": "0 */6 * * *",
186
+ "12h": "0 */12 * * *",
187
+ "24h": "0 0 * * *"
188
+ };
189
+ var VALID_INTERVALS = Object.keys(INTERVAL_CRON);
190
+ function resolveCliCommand(args) {
191
+ const nodePath = process.execPath;
192
+ const cliEntry = path.resolve(import.meta.dirname, "index.js");
193
+ return `${nodePath} ${cliEntry} ${args}`;
194
+ }
195
+ function getCurrentCrontab() {
196
+ try {
197
+ return execSync("crontab -l 2>/dev/null", { encoding: "utf8" });
198
+ } catch {
199
+ return "";
200
+ }
201
+ }
202
+ function setCrontab(content) {
203
+ const tmpFile = path.join(CONFIG_DIR, ".crontab-tmp");
204
+ ensureDir(CONFIG_DIR);
205
+ fs.writeFileSync(tmpFile, content);
206
+ try {
207
+ execSync(`crontab ${tmpFile}`, { encoding: "utf8" });
208
+ } finally {
209
+ fs.unlinkSync(tmpFile);
210
+ }
211
+ }
212
+ function addCronEntry(marker, cronExpr, command) {
213
+ const current = getCurrentCrontab();
214
+ const filtered = current.split("\n").filter((line) => !line.includes(marker)).join("\n");
215
+ const entry = `${cronExpr} ${command} >> ${SCHEDULE_LOG} 2>&1 ${marker}`;
216
+ const updated = filtered.trim() ? `${filtered.trim()}
217
+ ${entry}
218
+ ` : `${entry}
219
+ `;
220
+ setCrontab(updated);
221
+ }
222
+ function removeCronEntry(marker) {
223
+ const current = getCurrentCrontab();
224
+ const lines = current.split("\n");
225
+ const filtered = lines.filter((line) => !line.includes(marker));
226
+ if (filtered.length === lines.length) return false;
227
+ setCrontab(filtered.join("\n"));
228
+ return true;
229
+ }
230
+ function getCronEntry(marker) {
231
+ const current = getCurrentCrontab();
232
+ const line = current.split("\n").find((l) => l.includes(marker));
233
+ return line || null;
234
+ }
235
+ function assertNotWindows() {
236
+ if (process.platform === "win32") {
237
+ console.error("\u2717 Scheduled backups require cron (macOS/Linux). Windows is not supported yet.");
238
+ process.exit(1);
239
+ }
240
+ }
160
241
  var program = new Command();
161
242
  program.name("clawon").description("Backup and restore your OpenClaw workspace").version("0.1.1");
162
- 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) => {
243
+ program.command("login").description("Connect to Clawon with your API key").option("--api-key <key>", "Your Clawon API key (or set CLAWON_API_KEY env var)").option("--api-url <url>", "API base URL", "https://clawon.io").action(async (opts) => {
244
+ const apiKey = opts.apiKey || process.env.CLAWON_API_KEY;
245
+ if (!apiKey) {
246
+ console.error("\u2717 API key required. Use --api-key <key> or set CLAWON_API_KEY environment variable.");
247
+ process.exit(1);
248
+ }
163
249
  try {
164
- const connectJson = await api(opts.apiUrl, "/api/v1/profile/connect", "POST", opts.apiKey, {
250
+ const connectJson = await api(opts.apiUrl, "/api/v1/profile/connect", "POST", apiKey, {
165
251
  profileName: "default",
166
252
  instanceName: os.hostname(),
167
253
  syncIntervalMinutes: 60
168
254
  });
169
255
  writeConfig({
170
- apiKey: opts.apiKey,
256
+ apiKey,
171
257
  profileId: connectJson.profileId,
172
258
  apiBaseUrl: opts.apiUrl,
173
259
  connectedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -180,9 +266,13 @@ program.command("login").description("Connect to Clawon with your API key").requ
180
266
  process.exit(1);
181
267
  }
182
268
  });
183
- 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").action(async (opts) => {
269
+ 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("--include-sessions", "Include chat history (sessions)").option("--scheduled", "Internal: triggered by cron (suppresses interactive output)").action(async (opts) => {
184
270
  if (opts.includeMemoryDb) {
185
- console.error("\u2717 Memory DB backup requires a Pro account. Use `clawon local backup --include-memory-db` for local backups.");
271
+ console.error("\u2717 Memory DB cloud backup requires a Pro account. Use `clawon local backup --include-memory-db` for local backups.");
272
+ process.exit(1);
273
+ }
274
+ if (opts.includeSessions) {
275
+ console.error("\u2717 Session backup requires a Hobby or Pro account. Use `clawon local backup --include-sessions` for local backups.");
186
276
  process.exit(1);
187
277
  }
188
278
  const cfg = readConfig();
@@ -254,13 +344,19 @@ program.command("backup").description("Backup your OpenClaw workspace to the clo
254
344
  console.log(` Snapshot ID: ${snapshotId}`);
255
345
  console.log(` Files: ${files.length}`);
256
346
  console.log(` Size: ${(totalSize / 1024).toFixed(1)} KB`);
257
- trackCliEvent(cfg.profileId, "cloud_backup_created", {
347
+ trackCliEvent(cfg.profileId, opts.scheduled ? "scheduled_backup_created" : "cloud_backup_created", {
258
348
  file_count: files.length,
259
349
  total_bytes: totalSize,
260
- include_memory_db: !!opts.includeMemoryDb
350
+ include_memory_db: !!opts.includeMemoryDb,
351
+ include_sessions: !!opts.includeSessions,
352
+ type: "cloud",
353
+ trigger: opts.scheduled ? "scheduled" : "manual"
261
354
  });
262
355
  } catch (e) {
263
356
  const msg = e.message;
357
+ if (opts.scheduled) {
358
+ trackCliEvent(cfg.profileId, "scheduled_backup_failed", { type: "cloud", error: msg });
359
+ }
264
360
  if (msg.includes("Snapshot limit")) {
265
361
  console.error("\n\u2717 Snapshot limit reached (2).");
266
362
  console.error(" Delete one first: clawon delete <id>");
@@ -464,12 +560,12 @@ program.command("delete [id]").description("Delete a snapshot").option("--oldest
464
560
  process.exit(1);
465
561
  }
466
562
  });
467
- program.command("discover").description("Preview which files would be included in a backup").option("--include-memory-db", "Include SQLite memory index").action(async (opts) => {
563
+ program.command("discover").description("Preview which files would be included in a backup").option("--include-memory-db", "Include SQLite memory index").option("--include-sessions", "Include chat history (sessions)").action(async (opts) => {
468
564
  if (!fs.existsSync(OPENCLAW_DIR)) {
469
565
  console.error(`\u2717 OpenClaw directory not found: ${OPENCLAW_DIR}`);
470
566
  process.exit(1);
471
567
  }
472
- const files = discoverFiles(OPENCLAW_DIR, !!opts.includeMemoryDb);
568
+ const files = discoverFiles(OPENCLAW_DIR, !!opts.includeMemoryDb, !!opts.includeSessions);
473
569
  if (files.length === 0) {
474
570
  console.log("No files matched the include patterns.");
475
571
  return;
@@ -494,7 +590,71 @@ program.command("discover").description("Preview which files would be included i
494
590
  Total: ${files.length} files (${(totalSize / 1024).toFixed(1)} KB)`);
495
591
  console.log(`Source: ${OPENCLAW_DIR}`);
496
592
  const cfg = readConfig();
497
- trackCliEvent(cfg?.profileId || "anonymous", "cli_discover", { file_count: files.length, include_memory_db: !!opts.includeMemoryDb });
593
+ trackCliEvent(cfg?.profileId || "anonymous", "cli_discover", { file_count: files.length, include_memory_db: !!opts.includeMemoryDb, include_sessions: !!opts.includeSessions });
594
+ });
595
+ var schedule = program.command("schedule").description("Manage scheduled cloud backups");
596
+ schedule.command("on").description("Enable scheduled cloud backups via cron").option("--every <interval>", "Backup interval: 1h, 6h, 12h, 24h", "12h").action(async (opts) => {
597
+ assertNotWindows();
598
+ const cfg = readConfig();
599
+ trackCliEvent(cfg?.profileId || "anonymous", "schedule_enabled_attempted", {
600
+ type: "cloud",
601
+ interval_hours: parseInt(opts.every),
602
+ gated: true
603
+ });
604
+ console.error("\u2717 Scheduled cloud backups require a Hobby or Pro account. Use `clawon local schedule on` for local scheduled backups.");
605
+ process.exit(1);
606
+ });
607
+ schedule.command("off").description("Disable scheduled cloud backups").action(async () => {
608
+ assertNotWindows();
609
+ const removed = removeCronEntry(CRON_MARKER_CLOUD);
610
+ if (!removed) {
611
+ console.log("No cloud schedule was active.");
612
+ return;
613
+ }
614
+ updateConfig({
615
+ schedule: {
616
+ cloud: { enabled: false, intervalHours: 0 }
617
+ }
618
+ });
619
+ console.log("\u2713 Scheduled cloud backup disabled");
620
+ const cfg = readConfig();
621
+ trackCliEvent(cfg?.profileId || "anonymous", "schedule_disabled", { type: "cloud" });
622
+ });
623
+ schedule.command("status").description("Show schedule status").action(async () => {
624
+ const cfg = readConfig();
625
+ const localEntry = getCronEntry(CRON_MARKER_LOCAL);
626
+ const cloudEntry = getCronEntry(CRON_MARKER_CLOUD);
627
+ console.log("Schedule Status\n");
628
+ if (localEntry) {
629
+ const localCfg = cfg?.schedule?.local;
630
+ console.log("\u2713 Local schedule: active");
631
+ if (localCfg?.intervalHours) console.log(` Interval: every ${localCfg.intervalHours}h`);
632
+ if (localCfg?.maxSnapshots) console.log(` Max snapshots: ${localCfg.maxSnapshots}`);
633
+ if (localCfg?.includeMemoryDb) console.log(` Memory DB: included`);
634
+ if (localCfg?.includeSessions) console.log(` Sessions: included`);
635
+ console.log(` Cron: ${localEntry.trim()}`);
636
+ } else {
637
+ console.log("\u2717 Local schedule: inactive");
638
+ console.log(" Enable: clawon local schedule on");
639
+ }
640
+ console.log("");
641
+ if (cloudEntry) {
642
+ const cloudCfg = cfg?.schedule?.cloud;
643
+ console.log("\u2713 Cloud schedule: active");
644
+ if (cloudCfg?.intervalHours) console.log(` Interval: every ${cloudCfg.intervalHours}h`);
645
+ console.log(` Cron: ${cloudEntry.trim()}`);
646
+ } else {
647
+ console.log("\u2717 Cloud schedule: inactive");
648
+ console.log(" Enable: clawon schedule on (requires Hobby or Pro)");
649
+ }
650
+ console.log(`
651
+ Log: ${SCHEDULE_LOG}`);
652
+ trackCliEvent(cfg?.profileId || "anonymous", "schedule_status_viewed", {
653
+ local_enabled: !!localEntry,
654
+ cloud_enabled: !!cloudEntry,
655
+ local_interval_hours: cfg?.schedule?.local?.intervalHours || null,
656
+ cloud_interval_hours: cfg?.schedule?.cloud?.intervalHours || null
657
+ });
498
658
  });
499
659
  program.command("files").description("List files in a backup").option("--snapshot <id>", "Snapshot ID (default: latest)").action(async (opts) => {
500
660
  const cfg = readConfig();
@@ -538,38 +698,133 @@ Total: ${files.length} files`);
538
698
  }
539
699
  });
540
700
  var local = program.command("local").description("Local backup and restore (no cloud required)");
541
- 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").action(async (opts) => {
701
+ var localSchedule = local.command("schedule").description("Manage scheduled local backups");
702
+ 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").option("--include-sessions", "Include chat history (sessions)").action(async (opts) => {
703
+ assertNotWindows();
704
+ const interval = opts.every;
705
+ if (!VALID_INTERVALS.includes(interval)) {
706
+ console.error(`\u2717 Invalid interval: ${interval}. Valid options: ${VALID_INTERVALS.join(", ")}`);
707
+ process.exit(1);
708
+ }
709
+ const cfg = readConfig();
710
+ const wasEnabled = cfg?.schedule?.local?.enabled;
711
+ const cronExpr = INTERVAL_CRON[interval];
712
+ const args = "local backup --scheduled" + (opts.includeMemoryDb ? " --include-memory-db" : "") + (opts.includeSessions ? " --include-sessions" : "") + (opts.maxSnapshots ? ` --max-snapshots ${opts.maxSnapshots}` : "");
713
+ const command = resolveCliCommand(args);
714
+ addCronEntry(CRON_MARKER_LOCAL, cronExpr, command);
715
+ const maxSnapshots = opts.maxSnapshots ? parseInt(opts.maxSnapshots, 10) : null;
716
+ updateConfig({
717
+ schedule: {
718
+ local: {
719
+ enabled: true,
720
+ intervalHours: parseInt(interval),
721
+ ...maxSnapshots ? { maxSnapshots } : {},
722
+ ...opts.includeMemoryDb ? { includeMemoryDb: true } : {},
723
+ ...opts.includeSessions ? { includeSessions: true } : {}
724
+ }
725
+ }
726
+ });
727
+ console.log(`\u2713 Scheduled local backup enabled`);
728
+ console.log(` Interval: every ${interval}`);
729
+ if (maxSnapshots) console.log(` Max snapshots: ${maxSnapshots}`);
730
+ if (opts.includeMemoryDb) console.log(` Memory DB: included`);
731
+ if (opts.includeSessions) console.log(` Sessions: included`);
732
+ console.log(` Log: ${SCHEDULE_LOG}`);
733
+ const firstRunArgs = "local backup" + (opts.includeMemoryDb ? " --include-memory-db" : "") + (opts.includeSessions ? " --include-sessions" : "") + (opts.maxSnapshots ? ` --max-snapshots ${opts.maxSnapshots}` : "");
734
+ console.log("\nRunning first backup now...\n");
735
+ try {
736
+ execSync(resolveCliCommand(firstRunArgs), { stdio: "inherit" });
737
+ } catch {
738
+ console.error("\u26A0 First backup failed \u2014 schedule is still active and will retry at next interval.");
739
+ }
740
+ trackCliEvent(cfg?.profileId || "anonymous", wasEnabled ? "schedule_updated" : "schedule_enabled", {
741
+ type: "local",
742
+ interval_hours: parseInt(interval),
743
+ max_snapshots: maxSnapshots,
744
+ include_memory_db: !!opts.includeMemoryDb,
745
+ include_sessions: !!opts.includeSessions
746
+ });
747
+ });
748
+ localSchedule.command("off").description("Disable scheduled local backups").action(async () => {
749
+ assertNotWindows();
750
+ const removed = removeCronEntry(CRON_MARKER_LOCAL);
751
+ if (!removed) {
752
+ console.log("No local schedule was active.");
753
+ return;
754
+ }
755
+ updateConfig({
756
+ schedule: {
757
+ local: { enabled: false, intervalHours: 0 }
758
+ }
759
+ });
760
+ console.log("\u2713 Scheduled local backup disabled");
761
+ const cfg = readConfig();
762
+ trackCliEvent(cfg?.profileId || "anonymous", "schedule_disabled", { type: "local" });
763
+ });
764
+ 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("--include-sessions", "Include chat history (sessions)").option("--scheduled", "Internal: triggered by cron (suppresses interactive output)").option("--max-snapshots <n>", "Keep only the N most recent local backups").action(async (opts) => {
542
765
  if (!fs.existsSync(OPENCLAW_DIR)) {
543
766
  console.error(`\u2717 OpenClaw directory not found: ${OPENCLAW_DIR}`);
544
767
  process.exit(1);
545
768
  }
546
- console.log("Discovering files...");
547
- const files = discoverFiles(OPENCLAW_DIR, !!opts.includeMemoryDb);
548
- if (files.length === 0) {
549
- console.error("\u2717 No files found to backup");
769
+ try {
770
+ if (!opts.scheduled) console.log("Discovering files...");
771
+ const files = discoverFiles(OPENCLAW_DIR, !!opts.includeMemoryDb, !!opts.includeSessions);
772
+ if (files.length === 0) {
773
+ console.error("\u2717 No files found to backup");
774
+ process.exit(1);
775
+ }
776
+ const totalSize = files.reduce((sum, f) => sum + f.size, 0);
777
+ if (!opts.scheduled) console.log(`Found ${files.length} files (${(totalSize / 1024).toFixed(1)} KB)`);
778
+ ensureDir(BACKUPS_DIR);
779
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "").replace("T", "T").slice(0, 15);
780
+ const filename = `backup-${timestamp}.tar.gz`;
781
+ const filePath = path.join(BACKUPS_DIR, filename);
782
+ if (!opts.scheduled) console.log("Creating archive...");
783
+ await createLocalArchive(files, OPENCLAW_DIR, filePath, opts.tag, opts.includeMemoryDb, opts.includeSessions, opts.scheduled ? "scheduled" : "manual");
784
+ const archiveSize = fs.statSync(filePath).size;
785
+ if (!opts.scheduled) {
786
+ console.log(`
787
+ \u2713 Local backup saved!`);
788
+ console.log(` File: ${filePath}`);
789
+ console.log(` Files: ${files.length}`);
790
+ console.log(` Size: ${(archiveSize / 1024).toFixed(1)} KB (compressed)`);
791
+ if (opts.tag) console.log(` Tag: ${opts.tag}`);
792
+ }
793
+ const maxSnapshots = opts.maxSnapshots ? parseInt(opts.maxSnapshots, 10) : null;
794
+ let rotatedCount = 0;
795
+ if (maxSnapshots && maxSnapshots > 0) {
796
+ const allBackups = fs.readdirSync(BACKUPS_DIR).filter((f) => f.endsWith(".tar.gz")).sort().reverse();
797
+ if (allBackups.length > maxSnapshots) {
798
+ const toDelete = allBackups.slice(maxSnapshots);
799
+ for (const old of toDelete) {
800
+ fs.unlinkSync(path.join(BACKUPS_DIR, old));
801
+ rotatedCount++;
802
+ }
803
+ if (!opts.scheduled) {
804
+ console.log(` Rotated: deleted ${rotatedCount} old backup(s)`);
805
+ }
806
+ }
807
+ }
808
+ const cfg = readConfig();
809
+ trackCliEvent(cfg?.profileId || "anonymous", opts.scheduled ? "scheduled_backup_created" : "local_backup_created", {
810
+ file_count: files.length,
811
+ total_bytes: totalSize,
812
+ include_memory_db: !!opts.includeMemoryDb,
813
+ include_sessions: !!opts.includeSessions,
814
+ type: "local",
815
+ trigger: opts.scheduled ? "scheduled" : "manual",
816
+ rotated_count: rotatedCount
817
+ });
818
+ } catch (e) {
819
+ const msg = e.message;
820
+ if (opts.scheduled) {
821
+ const cfg = readConfig();
822
+ trackCliEvent(cfg?.profileId || "anonymous", "scheduled_backup_failed", { type: "local", error: msg });
823
+ }
824
+ console.error(`
825
+ \u2717 Local backup failed: ${msg}`);
550
826
  process.exit(1);
551
827
  }
552
- const totalSize = files.reduce((sum, f) => sum + f.size, 0);
553
- console.log(`Found ${files.length} files (${(totalSize / 1024).toFixed(1)} KB)`);
554
- ensureDir(BACKUPS_DIR);
555
- const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "").replace("T", "T").slice(0, 15);
556
- const filename = `backup-${timestamp}.tar.gz`;
557
- const filePath = path.join(BACKUPS_DIR, filename);
558
- console.log("Creating archive...");
559
- await createLocalArchive(files, OPENCLAW_DIR, filePath, opts.tag, opts.includeMemoryDb);
560
- const archiveSize = fs.statSync(filePath).size;
561
- console.log(`
562
- \u2713 Local backup saved!`);
563
- console.log(` File: ${filePath}`);
564
- console.log(` Files: ${files.length}`);
565
- console.log(` Size: ${(archiveSize / 1024).toFixed(1)} KB (compressed)`);
566
- if (opts.tag) console.log(` Tag: ${opts.tag}`);
567
- const cfg = readConfig();
568
- trackCliEvent(cfg?.profileId || "anonymous", "local_backup_created", {
569
- file_count: files.length,
570
- total_bytes: totalSize,
571
- include_memory_db: !!opts.includeMemoryDb
572
- });
573
828
  });
574
829
  local.command("list").description("List local backups").action(async () => {
575
830
  if (!fs.existsSync(BACKUPS_DIR)) {
@@ -681,6 +936,17 @@ program.command("status").description("Show current status").action(async () =>
681
936
  } else {
682
937
  console.log(`\u2717 OpenClaw not found: ${OPENCLAW_DIR}`);
683
938
  }
939
+ console.log("");
940
+ const localEntry = getCronEntry(CRON_MARKER_LOCAL);
941
+ const cloudEntry = getCronEntry(CRON_MARKER_CLOUD);
942
+ if (localEntry || cloudEntry) {
943
+ console.log("\u2713 Schedule active");
944
+ if (localEntry) console.log(" Local: " + (cfg?.schedule?.local?.intervalHours || "?") + "h interval");
945
+ if (cloudEntry) console.log(" Cloud: " + (cfg?.schedule?.cloud?.intervalHours || "?") + "h interval");
946
+ } else {
947
+ console.log("\u2717 No schedule configured");
948
+ console.log(" Run: clawon local schedule on");
949
+ }
684
950
  trackCliEvent(cfg?.profileId || "anonymous", "cli_status_viewed");
685
951
  });
686
952
  program.command("logout").description("Remove local credentials").action(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawon",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Backup and restore your OpenClaw workspace",
5
5
  "type": "module",
6
6
  "bin": {