8900 commited on
Commit
a2952e7
·
verified ·
1 Parent(s): e9ebcd0

Update hf-sync-manager.mjs

Browse files
Files changed (1) hide show
  1. hf-sync-manager.mjs +387 -158
hf-sync-manager.mjs CHANGED
@@ -1,205 +1,434 @@
1
  // ============================================================
2
- // HF Memory Sync Manager - Definitive Edition
3
  //
4
- // Based on actual OpenClaw disk layout (from official docs):
 
 
5
  //
6
- // ~/.openclaw/openclaw.json <- gateway config
7
- // ~/.openclaw/workspace/ <- agent brain (THIS is memory)
8
- // AGENTS.md <- operating instructions
9
- // SOUL.md <- persona / tone
10
- // USER.md <- user profile
11
- // IDENTITY.md <- name / emoji
12
- // TOOLS.md <- env conventions
13
- // HEARTBEAT.md <- periodic tasks
14
- // MEMORY.md <- long-term memory (iron rules)
15
- // memory/YYYY-MM-DD.md <- daily logs
16
- // memory/archive/ <- logs older than 30 days
17
- // checklists/ <- op checklists
18
- // docs/ <- on-demand reference docs
19
- // ~/.openclaw/agents/<id>/sessions/ <- DO NOT sync (session transcripts)
20
- // ~/.openclaw/agents/<id>/qmd/ <- DO NOT sync (sqlite index, rebuilt on boot)
21
  //
22
- // Repo structure:
23
- // settings/openclaw.json <- single copy, validated, overwrite
24
- // workspace/ <- full workspace mirror
25
- // AGENTS.md / SOUL.md / MEMORY.md / memory/ …
26
- // README.md <- auto-generated index
27
  //
28
- // Rules:
29
- // TRACK 1 - settings:
30
- // Boot: repo -> local (skip if repo JSON invalid)
31
- // Upload: local -> repo (skip if local JSON invalid)
32
- // One file, one copy, always overwrite
33
  //
34
- // TRACK 2 - workspace:
35
- // Boot: repo/workspace/ -> ~/.openclaw/workspace/ (full restore)
36
- // Upload: ~/.openclaw/workspace/ -> repo/workspace/ every 3h
37
- // Never downloads during runtime (prevents memory flooding)
38
- //
39
- // Multi-bot:
40
- // Set BOT1_PROFILE / BOT2_PROFILE / BOT3_PROFILE ... in HF Space Variables.
41
- // The script will automatically use the first defined BOTx_PROFILE as OPENCLAW_PROFILE.
42
- // OpenClaw uses ~/.openclaw/workspace-<profile>/ per profile.
43
- // Repo stores each profile under workspace-<profile>/.
44
  // ============================================================
45
 
46
- import fs from "node:fs";
47
  import path from "node:path";
48
  import { execSync } from "node:child_process";
49
 
50
- // -- config ----------------------------------
51
-
52
- var HOME = process.env.OPENCLAW_HOME || process.env.HOME || "/home/user";
53
- var OPENCLAW_DIR = path.join(HOME, ".openclaw");
54
- var HF_TOKEN = (process.env.HF_TOKEN || "").trim();
55
- var DATASET_ID = (process.env.HF_DATASET_ID || "").trim();
56
- var REPO_DIR = "/tmp/hf-memory-sync";
57
-
58
- // --- Multi-bot support ----------------------
59
- // Automatically detect BOTx_PROFILE from HF Variables
60
- var PROFILE = "";
61
- for (let i = 1; i <= 10; i++) { // 支持最多 10 个 bot,可自由扩展
62
- let envVar = process.env[`BOT${i}_PROFILE`];
63
- if (envVar && envVar.trim()) {
64
- PROFILE = envVar.trim();
65
- break; // 取第一个存在的 BOTx_PROFILE
66
- }
67
- }
68
 
69
- // OPENCLAW_PROFILE fallback (legacy)
70
- if (!PROFILE) {
71
- PROFILE = (process.env.OPENCLAW_PROFILE || "").trim();
72
- }
 
 
73
 
74
- // Compute workspace dirs
75
- var WORKSPACE_DIR = PROFILE
76
- ? path.join(OPENCLAW_DIR, "workspace-" + PROFILE)
77
- : path.join(OPENCLAW_DIR, "workspace");
78
-
79
- var REPO_WORKSPACE_FOLDER = PROFILE ? "workspace-" + PROFILE : "workspace";
80
-
81
- var UPLOAD_INTERVAL = 3 * 60 * 60 * 1000; // 3 hours
82
-
83
- // -- workspace files OpenClaw actually uses -----------------
84
- var WORKSPACE_SYNC = [
85
- "AGENTS.md",
86
- "SOUL.md",
87
- "USER.md",
88
- "IDENTITY.md",
89
- "TOOLS.md",
90
- "HEARTBEAT.md",
91
- "BOOT.md",
92
- "MEMORY.md",
93
- "memory", // directory: memory/YYYY-MM-DD.md + memory/archive/
94
- "checklists", // directory: op checklists
95
- "docs" // directory: on-demand reference docs
96
- ];
97
 
98
- // Never touch these (sessions and search index are runtime-only)
99
- var NEVER_SYNC = [
100
- "agents", // ~/.openclaw/agents/<id>/sessions/ and qmd/ - runtime only
101
- "canvas", // canvas UI files - runtime only
102
- ".git",
103
- "node_modules"
 
 
 
 
 
 
 
104
  ];
105
 
106
- // -- helpers ----------------------------------
107
 
108
  function log(msg) {
109
- console.log("[hf-sync] " + msg);
110
  }
111
 
112
  function sleep(ms) {
113
- return new Promise(function(resolve) { setTimeout(resolve, ms); });
114
  }
115
 
116
  function repoUrl() {
117
- return "https://user:" + HF_TOKEN +
118
- "@huggingface.co/datasets/" + DATASET_ID;
119
  }
120
 
121
  function git(args) {
122
- return execSync("git " + args, {
123
- cwd: REPO_DIR,
124
- stdio: "pipe",
125
- timeout: 120000,
126
- env: Object.assign({}, process.env, { GIT_TERMINAL_PROMPT: "0" })
127
- }).toString().trim();
128
  }
129
 
130
  function isValidJson(filePath) {
131
- try {
132
- if (!fs.existsSync(filePath)) return false;
133
- var content = fs.readFileSync(filePath, "utf-8").trim();
134
- if (!content) return false;
135
- JSON.parse(content);
136
- return true;
137
- } catch (e) {
138
- return false;
139
- }
140
  }
141
 
142
- function copyItem(src, dst) {
143
- if (fs.statSync(src).isDirectory()) {
144
- execSync("cp -a " + src + " " + dst, { stdio: "pipe" });
145
- } else {
146
- fs.mkdirSync(path.dirname(dst), { recursive: true });
147
- fs.copyFileSync(src, dst);
148
- }
149
  }
150
 
151
- // -- git repo setup --------------------------
152
 
153
  function ensureRepo() {
154
- if (fs.existsSync(path.join(REPO_DIR, ".git"))) return;
155
- execSync("rm -rf " + REPO_DIR, { stdio: "pipe" });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  try {
157
- execSync(
158
- "git clone --depth 1 " + repoUrl() + " " + REPO_DIR,
159
- {
160
- stdio: "pipe",
161
- timeout: 120000,
162
- env: Object.assign({}, process.env, { GIT_TERMINAL_PROMPT: "0" })
163
- }
164
- );
165
  } catch (e) {
166
- fs.mkdirSync(REPO_DIR, { recursive: true });
167
- execSync("git init " + REPO_DIR, { stdio: "pipe" });
168
- git("remote add origin " + repoUrl());
169
  }
170
- try {
171
- git("config user.email openclaw@hf.space");
172
- git("config user.name OpenClaw-Sync");
173
- git("config pull.rebase false");
174
- } catch (e) { /* non-fatal */ }
175
- }
176
 
177
- function pullLatest() {
178
- try { git("fetch --quiet origin"); } catch (e) { /* ignore */ }
 
 
179
  try {
180
- git("pull --quiet --no-rebase origin main");
 
 
181
  } catch (e) {
182
- try { git("pull --quiet --no-rebase origin master"); } catch (e2) { /* no remote yet */ }
183
  }
 
 
 
 
 
 
184
  }
185
 
186
- function commitAndPush(label) {
187
- try {
188
- git("add -A");
189
- var changed = git("diff --cached --name-only");
190
- if (!changed) return 0;
191
- var count = changed.split("\n").filter(Boolean).length;
192
- var ts = new Date().toISOString().replace("T", " ").substring(0, 19);
193
- var tag = PROFILE ? "[" + PROFILE + "]" : "";
194
- git("commit -m \"sync" + tag + " " + ts + "\"");
195
- try {
196
- git("push --quiet origin HEAD:main");
197
- } catch (e) {
198
- git("push --quiet origin HEAD:master");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  }
200
- return count;
201
- } catch (e) {
202
- log("Commit/push failed: " + e.message);
203
- return 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  }
205
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  // ============================================================
2
+ // HF Sync Manager - Dataset as Runtime Storage
3
  //
4
+ // Concept: HF Dataset IS the persistent storage layer.
5
+ // OpenClaw writes to local disk. We mirror to Dataset every 30min.
6
+ // On container restart: restore from Dataset before gateway starts.
7
  //
8
+ // What gets synced (everything OpenClaw writes at runtime):
9
+ // ~/.openclaw/workspace/ <- agent brain, memory, persona files
10
+ // ~/.openclaw/openclaw.json <- gateway config (JSON-validated)
 
 
 
 
 
 
 
 
 
 
 
 
11
  //
12
+ // What is intentionally excluded:
13
+ // ~/.openclaw/agents/*/sessions/ <- session transcripts (large, rebuild ok)
14
+ // ~/.openclaw/agents/*/qmd/ <- sqlite vector index (rebuilt on boot)
15
+ // ~/.openclaw/canvas/ <- UI runtime files
 
16
  //
17
+ // Boot sequence (entrypoint.sh calls this before gateway starts):
18
+ // 1. Clone/pull Dataset repo
19
+ // 2. Restore workspace/ from repo
20
+ // 3. Gateway starts with full memory intact
 
21
  //
22
+ // Runtime loop:
23
+ // Every 30 min: workspace + config -> Dataset repo -> git push
 
 
 
 
 
 
 
 
24
  // ============================================================
25
 
26
+ import fs from "node:fs";
27
  import path from "node:path";
28
  import { execSync } from "node:child_process";
29
 
30
+ // -- config ---------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
+ var HOME = process.env.OPENCLAW_HOME || process.env.HOME || "/home/user";
33
+ var OPENCLAW_DIR = path.join(HOME, ".openclaw");
34
+ var WORKSPACE_DIR = path.join(OPENCLAW_DIR, "workspace");
35
+ var HF_TOKEN = (process.env.HF_TOKEN || "").trim();
36
+ var DATASET_ID = (process.env.HF_DATASET_ID || "").trim();
37
+ var REPO_DIR = "/tmp/oc-dataset";
38
 
39
+ // Upload every 30 min during runtime
40
+ var UPLOAD_INTERVAL = 30 * 60 * 1000;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
+ // Workspace files OpenClaw actually uses (official docs)
43
+ var WORKSPACE_FILES = [
44
+ "AGENTS.md",
45
+ "SOUL.md",
46
+ "USER.md",
47
+ "IDENTITY.md",
48
+ "TOOLS.md",
49
+ "HEARTBEAT.md",
50
+ "BOOT.md",
51
+ "MEMORY.md",
52
+ "memory",
53
+ "checklists",
54
+ "docs"
55
  ];
56
 
57
+ // -- helpers --------------------------------------------------
58
 
59
  function log(msg) {
60
+ console.log("[hf-sync] " + msg);
61
  }
62
 
63
  function sleep(ms) {
64
+ return new Promise(function(resolve) { setTimeout(resolve, ms); });
65
  }
66
 
67
  function repoUrl() {
68
+ return "https://user:" + HF_TOKEN + "@huggingface.co/datasets/" + DATASET_ID;
 
69
  }
70
 
71
  function git(args) {
72
+ return execSync("git " + args, {
73
+ cwd: REPO_DIR,
74
+ stdio: "pipe",
75
+ timeout: 120000,
76
+ env: Object.assign({}, process.env, { GIT_TERMINAL_PROMPT: "0" })
77
+ }).toString().trim();
78
  }
79
 
80
  function isValidJson(filePath) {
81
+ try {
82
+ if (!fs.existsSync(filePath)) return false;
83
+ var content = fs.readFileSync(filePath, "utf-8").trim();
84
+ if (!content) return false;
85
+ JSON.parse(content);
86
+ return true;
87
+ } catch (e) {
88
+ return false;
89
+ }
90
  }
91
 
92
+ function copyDir(src, dst) {
93
+ execSync("cp -a " + src + "/. " + dst + "/", { stdio: "pipe" });
94
+ }
95
+
96
+ function copyFile(src, dst) {
97
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
98
+ fs.copyFileSync(src, dst);
99
  }
100
 
101
+ // -- git repo setup ------------------------------------------
102
 
103
  function ensureRepo() {
104
+ if (fs.existsSync(path.join(REPO_DIR, ".git"))) return;
105
+ execSync("rm -rf " + REPO_DIR, { stdio: "pipe" });
106
+ try {
107
+ execSync(
108
+ "git clone --depth 1 " + repoUrl() + " " + REPO_DIR,
109
+ {
110
+ stdio: "pipe",
111
+ timeout: 120000,
112
+ env: Object.assign({}, process.env, { GIT_TERMINAL_PROMPT: "0" })
113
+ }
114
+ );
115
+ log("Repo cloned from Dataset");
116
+ } catch (e) {
117
+ log("Clone failed, initializing empty repo: " + e.message);
118
+ fs.mkdirSync(REPO_DIR, { recursive: true });
119
+ execSync("git init " + REPO_DIR, { stdio: "pipe" });
120
+ git("remote add origin " + repoUrl());
121
+ }
122
+ try {
123
+ git("config user.email openclaw@hf.space");
124
+ git("config user.name OpenClaw-Sync");
125
+ git("config pull.rebase false");
126
+ } catch (e) { /* non-fatal */ }
127
+ }
128
+
129
+ function pullLatest() {
130
+ try { git("fetch --quiet origin"); } catch (e) { /* ignore */ }
131
+ try { git("pull --quiet --no-rebase origin main"); }
132
+ catch (e) {
133
+ try { git("pull --quiet --no-rebase origin master"); }
134
+ catch (e2) { /* repo empty, first push */ }
135
+ }
136
+ }
137
+
138
+ function commitAndPush() {
139
+ try {
140
+ git("add -A");
141
+ var changed = git("diff --cached --name-only");
142
+ if (!changed) return 0;
143
+ var count = changed.split("\n").filter(Boolean).length;
144
+ var ts = new Date().toISOString().replace("T", " ").substring(0, 19);
145
+ git('commit -m "runtime-sync ' + ts + '"');
146
+ try { git("push --quiet origin HEAD:main"); }
147
+ catch (e) { git("push --quiet origin HEAD:master"); }
148
+ return count;
149
+ } catch (e) {
150
+ log("Push failed: " + e.message);
151
+ return 0;
152
+ }
153
+ }
154
+
155
+ // -- README ---------------------------------------------------
156
+
157
+ function writeReadme() {
158
+ var ts = new Date().toISOString().replace("T", " ").substring(0, 19);
159
+ var content = [
160
+ "# OpenClaw Runtime Dataset",
161
+ "",
162
+ "This repository is the persistent storage layer for an OpenClaw gateway",
163
+ "running on Hugging Face Spaces. It is auto-managed - do not edit manually.",
164
+ "",
165
+ "Last sync: " + ts,
166
+ "",
167
+ "## Structure",
168
+ "",
169
+ "`",
170
+ "openclaw.json <- gateway config (JSON-validated, auto-generated)",
171
+ "workspace/ <- agent brain (everything OpenClaw remembers)",
172
+ " AGENTS.md <- operating instructions",
173
+ " SOUL.md <- persona / tone / language",
174
+ " USER.md <- user profile",
175
+ " IDENTITY.md <- agent name and emoji",
176
+ " TOOLS.md <- environment conventions",
177
+ " HEARTBEAT.md <- periodic task instructions",
178
+ " MEMORY.md <- long-term memory (iron rules)",
179
+ " memory/ <- daily logs: YYYY-MM-DD.md",
180
+ " archive/ <- logs older than 30 days",
181
+ " checklists/ <- operation checklists",
182
+ " docs/ <- on-demand reference docs",
183
+ "`",
184
+ "",
185
+ "## What is NOT stored here",
186
+ "",
187
+ "- Session transcripts (`agents/*/sessions/`) - large, not needed for memory",
188
+ "- Vector search index (`agents/*/qmd/`) - rebuilt automatically on boot",
189
+ "- Canvas UI files - runtime only",
190
+ "",
191
+ "## Container restart recovery",
192
+ "",
193
+ "On every container start, the sync manager pulls this repo and restores",
194
+ "the workspace before OpenClaw boots. Memory is never lost across restarts."
195
+ ].join("\n") + "\n";
196
+
197
+ fs.writeFileSync(path.join(REPO_DIR, "README.md"), content, "utf-8");
198
+ }
199
+
200
+ // -- seed default workspace files ----------------------------
201
+
202
+ function seedWorkspaceIfEmpty() {
203
+ fs.mkdirSync(WORKSPACE_DIR, { recursive: true });
204
+ fs.mkdirSync(path.join(WORKSPACE_DIR, "memory"), { recursive: true });
205
+
206
+ var soulPath = path.join(WORKSPACE_DIR, "SOUL.md");
207
+ if (!fs.existsSync(soulPath)) {
208
+ fs.writeFileSync(soulPath, [
209
+ "# Soul",
210
+ "",
211
+ "You are a helpful, warm, and concise AI assistant.",
212
+ "",
213
+ "## Language",
214
+ "",
215
+ "Always respond in Simplified Chinese by default.",
216
+ "Switch to the user's language only if they explicitly write in another language.",
217
+ "",
218
+ "## Tone",
219
+ "",
220
+ "- Friendly and natural, not overly formal",
221
+ "- Get to the point quickly",
222
+ "- Ask clarifying questions when needed"
223
+ ].join("\n") + "\n", "utf-8");
224
+ log("[workspace] Seeded SOUL.md (Chinese language configured)");
225
+ }
226
+
227
+ var agentsPath = path.join(WORKSPACE_DIR, "AGENTS.md");
228
+ if (!fs.existsSync(agentsPath)) {
229
+ fs.writeFileSync(agentsPath, [
230
+ "# Agent Instructions",
231
+ "",
232
+ "## Boot sequence",
233
+ "",
234
+ "1. Read SOUL.md - persona and language rules",
235
+ "2. Read USER.md if present - user profile",
236
+ "3. Read MEMORY.md - long-term memory and iron rules",
237
+ "4. Read today's and yesterday's memory/YYYY-MM-DD.md if present",
238
+ "",
239
+ "## Memory rules",
240
+ "",
241
+ "- Save important facts to MEMORY.md",
242
+ "- Log daily context to memory/YYYY-MM-DD.md",
243
+ "- Do not repeat information the user has already told you"
244
+ ].join("\n") + "\n", "utf-8");
245
+ log("[workspace] Seeded AGENTS.md");
246
+ }
247
+
248
+ var memoryPath = path.join(WORKSPACE_DIR, "MEMORY.md");
249
+ if (!fs.existsSync(memoryPath)) {
250
+ fs.writeFileSync(memoryPath, [
251
+ "# Long-term Memory",
252
+ "",
253
+ "<!-- OpenClaw writes important facts here. -->",
254
+ "<!-- Loaded at the start of every main session. -->"
255
+ ].join("\n") + "\n", "utf-8");
256
+ log("[workspace] Seeded MEMORY.md");
257
+ }
258
+ }
259
+
260
+ // ============================================================
261
+ // BOOT RESTORE
262
+ // Called once before gateway starts. Pulls Dataset -> local disk.
263
+ // ============================================================
264
+
265
+ function bootRestore() {
266
+ if (!HF_TOKEN || !DATASET_ID) {
267
+ log("HF_TOKEN or HF_DATASET_ID not set - running without Dataset backup");
268
+ seedWorkspaceIfEmpty();
269
+ return;
270
+ }
271
+
272
+ log("Boot restore from Dataset: " + DATASET_ID);
273
+
274
+ try {
275
+ ensureRepo();
276
+ pullLatest();
277
+ } catch (e) {
278
+ log("Repo init failed (non-fatal, starting fresh): " + e.message);
279
+ seedWorkspaceIfEmpty();
280
+ return;
281
+ }
282
+
283
+ // Restore workspace
284
+ var repoWorkspace = path.join(REPO_DIR, "workspace");
285
+ if (fs.existsSync(repoWorkspace)) {
286
+ fs.mkdirSync(WORKSPACE_DIR, { recursive: true });
287
  try {
288
+ copyDir(repoWorkspace, WORKSPACE_DIR);
289
+ var count = fs.readdirSync(WORKSPACE_DIR).length;
290
+ log("Workspace restored: " + count + " item(s) -> " + WORKSPACE_DIR);
 
 
 
 
 
291
  } catch (e) {
292
+ log("Workspace restore failed: " + e.message);
 
 
293
  }
294
+ } else {
295
+ log("No workspace in Dataset yet - starting fresh");
296
+ }
 
 
 
297
 
298
+ // Restore openclaw.json ONLY if repo has valid one AND local doesn't exist yet
299
+ var repoConfig = path.join(REPO_DIR, "openclaw.json");
300
+ var localConfig = path.join(OPENCLAW_DIR, "openclaw.json");
301
+ if (!fs.existsSync(localConfig) && isValidJson(repoConfig)) {
302
  try {
303
+ fs.mkdirSync(OPENCLAW_DIR, { recursive: true });
304
+ copyFile(repoConfig, localConfig);
305
+ log("Config restored from Dataset (was missing locally)");
306
  } catch (e) {
307
+ log("Config restore failed: " + e.message);
308
  }
309
+ }
310
+
311
+ // Seed workspace files that don't exist yet
312
+ seedWorkspaceIfEmpty();
313
+
314
+ log("Boot restore complete.");
315
  }
316
 
317
+ // ============================================================
318
+ // UPLOAD CYCLE
319
+ // Runs every 30 min. Pushes local state to Dataset.
320
+ // Never downloads during runtime.
321
+ // ============================================================
322
+
323
+ function runUploadCycle() {
324
+ if (!HF_TOKEN || !DATASET_ID) return;
325
+
326
+ var ts = new Date().toISOString().replace("T", " ").substring(0, 19);
327
+ log("Upload cycle at " + ts + "...");
328
+
329
+ try {
330
+ ensureRepo();
331
+ pullLatest();
332
+
333
+ // Upload workspace
334
+ var repoWorkspace = path.join(REPO_DIR, "workspace");
335
+ if (fs.existsSync(WORKSPACE_DIR)) {
336
+ var existing = WORKSPACE_FILES.filter(function(name) {
337
+ return fs.existsSync(path.join(WORKSPACE_DIR, name));
338
+ });
339
+
340
+ if (existing.length > 0) {
341
+ // Clear and re-copy workspace in repo
342
+ if (fs.existsSync(repoWorkspace)) {
343
+ execSync("rm -rf " + repoWorkspace, { stdio: "pipe" });
344
  }
345
+ fs.mkdirSync(repoWorkspace, { recursive: true });
346
+
347
+ existing.forEach(function(name) {
348
+ var src = path.join(WORKSPACE_DIR, name);
349
+ var dst = path.join(repoWorkspace, name);
350
+ try {
351
+ var stat = fs.statSync(src);
352
+ if (stat.isDirectory()) {
353
+ fs.mkdirSync(dst, { recursive: true });
354
+ copyDir(src, dst);
355
+ } else {
356
+ copyFile(src, dst);
357
+ }
358
+ } catch (e) {
359
+ log("Copy error [" + name + "]: " + e.message);
360
+ }
361
+ });
362
+ log("Workspace uploaded: " + existing.join(", "));
363
+ } else {
364
+ log("No workspace files found yet - skipping workspace upload");
365
+ }
366
  }
367
+
368
+ // Upload openclaw.json (validated)
369
+ var localConfig = path.join(OPENCLAW_DIR, "openclaw.json");
370
+ if (isValidJson(localConfig)) {
371
+ copyFile(localConfig, path.join(REPO_DIR, "openclaw.json"));
372
+ log("Config uploaded (JSON valid)");
373
+ } else {
374
+ log("Config skipped (invalid JSON)");
375
+ }
376
+
377
+ writeReadme();
378
+
379
+ var changed = commitAndPush();
380
+ if (changed > 0) {
381
+ log("Upload done: " + changed + " file(s) pushed to Dataset");
382
+ } else {
383
+ log("Upload done: no changes");
384
+ }
385
+
386
+ } catch (e) {
387
+ log("Upload failed (will retry in 30 min): " + e.message);
388
+ }
389
+ }
390
+
391
+ // ============================================================
392
+ // MAIN
393
+ // ============================================================
394
+
395
+ async function main() {
396
+ log("Starting...");
397
+ log("OPENCLAW_DIR : " + OPENCLAW_DIR);
398
+ log("WORKSPACE_DIR : " + WORKSPACE_DIR);
399
+ log("HF_DATASET_ID : " + (DATASET_ID || "NOT SET"));
400
+ log("Upload every : " + (UPLOAD_INTERVAL / 60000) + " min");
401
+
402
+ fs.mkdirSync(OPENCLAW_DIR, { recursive: true });
403
+ fs.mkdirSync(WORKSPACE_DIR, { recursive: true });
404
+
405
+ try {
406
+ execSync("git config --global user.email openclaw@hf.space", { stdio: "pipe" });
407
+ execSync("git config --global user.name OpenClaw-Sync", { stdio: "pipe" });
408
+ execSync("git config --global http.postBuffer 52428800", { stdio: "pipe" });
409
+ execSync("git config --global pull.rebase false", { stdio: "pipe" });
410
+ } catch (e) { /* non-fatal */ }
411
+
412
+ // Step 1: restore from Dataset (runs before gateway is fully up)
413
+ bootRestore();
414
+
415
+ // Step 2: upload loop (every 30 min, never downloads)
416
+ if (HF_TOKEN && DATASET_ID) {
417
+ log("Upload loop starting (first run in 10 min)...");
418
+ (async function loop() {
419
+ await sleep(10 * 60 * 1000);
420
+ while (true) {
421
+ runUploadCycle();
422
+ await sleep(UPLOAD_INTERVAL);
423
+ }
424
+ })().catch(function(e) { log("Upload loop crashed: " + e.message); });
425
+ } else {
426
+ log("Upload disabled: set HF_TOKEN and HF_DATASET_ID in Space Secrets");
427
+ }
428
+
429
+ log("Ready.");
430
+ }
431
+
432
+ main().catch(function(e) {
433
+ console.error("[hf-sync] Fatal: " + e.message);
434
+ });