8900 commited on
Commit
6a9b123
·
verified ·
1 Parent(s): e76aaa9

Update hf-sync-manager.mjs

Browse files
Files changed (1) hide show
  1. hf-sync-manager.mjs +146 -232
hf-sync-manager.mjs CHANGED
@@ -1,277 +1,191 @@
1
  // ============================================================
2
- // HF Memory Sync Manager - Production Grade
3
  //
4
- // TRACK 1 - Settings (openclaw.json):
5
- // Boot: repo/settings/openclaw.json -> local disk
6
- // ONLY if repo file passes JSON validation
7
- // If invalid: keep local file, log warning
8
- // Upload: local disk -> repo/settings/openclaw.json
9
- // ONLY if local file passes JSON validation
10
- // Single copy, always overwrite, never accumulate
11
  //
12
- // TRACK 2 - Memory (conversations, history, context, agents, logs):
13
- // Boot: repo/data/* -> local disk (full restore, all sessions)
14
- // Upload: local disk -> repo/data/* every 3h
15
- // Upload only, NEVER download during runtime
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  // ============================================================
17
 
18
  import fs from "node:fs";
19
  import path from "node:path";
20
  import { execSync } from "node:child_process";
21
 
22
- // -- config ---------------------------------------------------
23
-
24
- var HOME = process.env.OPENCLAW_HOME || process.env.HOME || "/home/user";
25
- var OPENCLAW_DIR = path.join(HOME, ".openclaw");
26
- var HF_TOKEN = (process.env.HF_TOKEN || "").trim();
27
- var DATASET_ID = (process.env.HF_DATASET_ID || "").trim();
28
- var REPO_DIR = "/tmp/hf-memory-sync";
29
- var UPLOAD_INTERVAL = 3 * 60 * 60 * 1000;
30
-
31
- var SETTINGS_FILE = "openclaw.json";
32
- var REPO_SETTINGS = "settings";
33
-
34
- var MEMORY_TARGETS = [
35
- "memory",
36
- "history",
37
- "logs",
38
- "conversations",
39
- "context.json",
40
- "agents"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  ];
42
 
 
43
  var NEVER_SYNC = [
44
- "openclaw.json",
45
- "openclaw.json.bak",
46
- ".sync-state.json",
47
- "settings.json",
48
- "config.yaml",
49
- "config.json",
50
- ".env",
51
- "node_modules",
52
- ".git",
53
- "canvas"
54
  ];
55
 
56
- // -- helpers --------------------------------------------------
57
 
58
  function log(msg) {
59
- console.log("[hf-sync] " + msg);
60
  }
61
 
62
  function sleep(ms) {
63
- return new Promise(function(resolve) { setTimeout(resolve, ms); });
64
  }
65
 
66
  function repoUrl() {
67
- return "https://user:" + HF_TOKEN +
68
- "@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 isMemoryTarget(name) {
81
- var i;
82
- for (i = 0; i < NEVER_SYNC.length; i++) {
83
- if (name === NEVER_SYNC[i]) return false;
84
- }
85
- for (i = 0; i < MEMORY_TARGETS.length; i++) {
86
- if (name === MEMORY_TARGETS[i]) return true;
87
- }
88
- return false;
89
- }
90
-
91
- // FIX: validate JSON before any config file operation
92
- // Returns true if file exists and contains valid JSON
93
  function isValidJson(filePath) {
94
- try {
95
- if (!fs.existsSync(filePath)) return false;
96
- var content = fs.readFileSync(filePath, "utf-8").trim();
97
- if (!content) return false;
98
- JSON.parse(content);
99
- return true;
100
- } catch (e) {
101
- return false;
102
- }
103
  }
104
 
105
  function copyItem(src, dst) {
106
- if (fs.statSync(src).isDirectory()) {
107
- execSync("cp -r " + src + " " + dst, { stdio: "pipe" });
108
- } else {
109
- fs.mkdirSync(path.dirname(dst), { recursive: true });
110
- fs.copyFileSync(src, dst);
111
- }
112
  }
113
 
114
- // -- git repo setup ------------------------------------------
115
 
116
  function ensureRepo() {
117
- if (fs.existsSync(path.join(REPO_DIR, ".git"))) return;
118
- execSync("rm -rf " + REPO_DIR, { stdio: "pipe" });
119
- try {
120
- execSync(
121
- "git clone --depth 1 " + repoUrl() + " " + REPO_DIR,
122
- {
123
- stdio: "pipe",
124
- timeout: 120000,
125
- env: Object.assign({}, process.env, { GIT_TERMINAL_PROMPT: "0" })
126
- }
127
- );
128
- } catch (e) {
129
- fs.mkdirSync(REPO_DIR, { recursive: true });
130
- execSync("git init " + REPO_DIR, { stdio: "pipe" });
131
- git("remote add origin " + repoUrl());
132
- }
133
- try {
134
- git("config user.email openclaw@hf.space");
135
- git("config user.name OpenClaw-Memory-Sync");
136
- git("config pull.rebase false");
137
- } catch (e) { /* non-fatal */ }
138
  }
139
-
140
- function pullLatest() {
141
- try { git("fetch --quiet origin"); } catch (e) { /* ignore */ }
142
- try {
143
- git("pull --quiet --no-rebase origin main");
144
- } catch (e) {
145
- try { git("pull --quiet --no-rebase origin master"); } catch (e2) { /* no remote yet */ }
146
- }
147
  }
148
-
149
- function commitAndPush(label) {
150
- try {
151
- git("add -A");
152
- var changed = git("diff --cached --name-only");
153
- if (!changed) return 0;
154
- var count = changed.split("\n").filter(Boolean).length;
155
- var ts = new Date().toISOString().replace("T", " ").substring(0, 19);
156
- git("commit -m \"" + label + " " + ts + "\"");
157
- try {
158
- git("push --quiet origin HEAD:main");
159
- } catch (e) {
160
- git("push --quiet origin HEAD:master");
161
- }
162
- return count;
163
- } catch (e) {
164
- log("Commit/push failed: " + e.message);
165
- return 0;
166
- }
167
  }
168
 
169
- // ============================================================
170
- // TRACK 1: Settings
171
- // ============================================================
172
-
173
- function settingsBootRestore() {
174
- var repoFile = path.join(REPO_DIR, REPO_SETTINGS, SETTINGS_FILE);
175
- var localFile = path.join(OPENCLAW_DIR, SETTINGS_FILE);
176
-
177
- if (!fs.existsSync(repoFile)) {
178
- log("[settings] No settings in repo yet, keeping local defaults");
179
- return false;
180
- }
181
-
182
- // FIX: validate repo file before overwriting local config
183
- if (!isValidJson(repoFile)) {
184
- log("[settings] WARN: repo openclaw.json is invalid JSON, skipping restore to protect local config");
185
- log("[settings] Corrupted repo file will be overwritten on next upload");
186
- return false;
187
- }
188
-
189
- try {
190
- fs.mkdirSync(OPENCLAW_DIR, { recursive: true });
191
- // Backup local before overwriting
192
- if (fs.existsSync(localFile)) {
193
- fs.copyFileSync(localFile, localFile + ".pre-restore-bak");
194
- }
195
- fs.copyFileSync(repoFile, localFile);
196
- log("[settings] Boot restore done: openclaw.json <- repo (validated OK)");
197
- return true;
198
- } catch (e) {
199
- log("[settings] Boot restore failed: " + e.message);
200
- return false;
201
- }
202
  }
203
-
204
- function settingsUpload() {
205
- var localFile = path.join(OPENCLAW_DIR, SETTINGS_FILE);
206
-
207
- // FIX: validate local file before uploading to repo
208
- if (!isValidJson(localFile)) {
209
- log("[settings] WARN: local openclaw.json is invalid JSON, skipping upload");
210
- return 0;
211
- }
212
-
213
- var repoDir = path.join(REPO_DIR, REPO_SETTINGS);
214
- var repoFile = path.join(repoDir, SETTINGS_FILE);
215
-
216
- fs.mkdirSync(repoDir, { recursive: true });
217
- // Single copy, direct overwrite - no version accumulation
218
- fs.copyFileSync(localFile, repoFile);
219
- log("[settings] Uploaded openclaw.json -> repo (validated OK)");
220
- return 1;
221
  }
222
 
223
- // ============================================================
224
- // TRACK 2: Memory
225
- // ============================================================
226
-
227
- function memoryBootRestore() {
228
- var src = path.join(REPO_DIR, "data");
229
- if (!fs.existsSync(src)) return 0;
230
-
231
- var items = fs.readdirSync(src);
232
- var copied = 0;
233
-
234
- items.forEach(function(name) {
235
- var s = path.join(src, name);
236
- var d = path.join(OPENCLAW_DIR, name);
237
- try {
238
- fs.mkdirSync(OPENCLAW_DIR, { recursive: true });
239
- copyItem(s, d);
240
- copied++;
241
- } catch (e) {
242
- log("[memory] Restore error [" + name + "]: " + e.message);
243
- }
244
- });
245
-
246
- return copied;
247
  }
248
-
249
- function memoryUpload() {
250
- var dest = path.join(REPO_DIR, "data");
251
- execSync("rm -rf " + dest, { stdio: "pipe" });
252
- fs.mkdirSync(dest, { recursive: true });
253
-
254
- if (!fs.existsSync(OPENCLAW_DIR)) return 0;
255
-
256
- var items = fs.readdirSync(OPENCLAW_DIR);
257
- var copied = 0;
258
-
259
- items.forEach(function(name) {
260
- if (!isMemoryTarget(name)) return;
261
- var src = path.join(OPENCLAW_DIR, name);
262
- var dst = path.join(dest, name);
263
- try {
264
- copyItem(src, dst);
265
- copied++;
266
- } catch (e) {
267
- log("[memory] Copy error [" + name + "]: " + e.message);
268
- }
269
- });
270
-
271
- return copied;
272
  }
273
-
274
- function scanMemoryDisk() {
275
- if (!fs.existsSync(OPENCLAW_DIR)) return [];
276
- return fs.readdirSync(OPENCLAW_DIR).filter(isMemoryTarget);
277
  }
 
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 OPENCLAW_PROFILE env var in HF Space Variables.
41
+ // OpenClaw uses ~/.openclaw/workspace-<profile>/ per profile.
42
+ // Repo stores each profile under workspace-<profile>/.
43
  // ============================================================
44
 
45
  import fs from "node:fs";
46
  import path from "node:path";
47
  import { execSync } from "node:child_process";
48
 
49
+ // -- config ----------------------------------
50
+
51
+ var HOME = process.env.OPENCLAW_HOME || process.env.HOME || "/home/user";
52
+ var OPENCLAW_DIR = path.join(HOME, ".openclaw");
53
+ var HF_TOKEN = (process.env.HF_TOKEN || "").trim();
54
+ var DATASET_ID = (process.env.HF_DATASET_ID || "").trim();
55
+ var REPO_DIR = "/tmp/hf-memory-sync";
56
+
57
+ // OPENCLAW_PROFILE: if set, workspace is ~/.openclaw/workspace-<profile>/
58
+ // This is OpenClaw's native multi-bot mechanism
59
+ var PROFILE = (process.env.OPENCLAW_PROFILE || "").trim();
60
+ var WORKSPACE_DIR = PROFILE
61
+ ? path.join(OPENCLAW_DIR, "workspace-" + PROFILE)
62
+ : path.join(OPENCLAW_DIR, "workspace");
63
+
64
+ // Repo folder name for this profile's workspace
65
+ var REPO_WORKSPACE_FOLDER = PROFILE ? "workspace-" + PROFILE : "workspace";
66
+
67
+ var UPLOAD_INTERVAL = 3 * 60 * 60 * 1000; // 3 hours
68
+
69
+ // -- workspace files OpenClaw actually uses -----------------
70
+ var WORKSPACE_SYNC = [
71
+ "AGENTS.md",
72
+ "SOUL.md",
73
+ "USER.md",
74
+ "IDENTITY.md",
75
+ "TOOLS.md",
76
+ "HEARTBEAT.md",
77
+ "BOOT.md",
78
+ "MEMORY.md",
79
+ "memory", // directory: memory/YYYY-MM-DD.md + memory/archive/
80
+ "checklists", // directory: op checklists
81
+ "docs" // directory: on-demand reference docs
82
  ];
83
 
84
+ // Never touch these (sessions and search index are runtime-only)
85
  var NEVER_SYNC = [
86
+ "agents", // ~/.openclaw/agents/<id>/sessions/ and qmd/ - runtime only
87
+ "canvas", // canvas UI files - runtime only
88
+ ".git",
89
+ "node_modules"
 
 
 
 
 
 
90
  ];
91
 
92
+ // -- helpers ----------------------------------
93
 
94
  function log(msg) {
95
+ console.log("[hf-sync] " + msg);
96
  }
97
 
98
  function sleep(ms) {
99
+ return new Promise(function(resolve) { setTimeout(resolve, ms); });
100
  }
101
 
102
  function repoUrl() {
103
+ return "https://user:" + HF_TOKEN +
104
+ "@huggingface.co/datasets/" + DATASET_ID;
105
  }
106
 
107
  function git(args) {
108
+ return execSync("git " + args, {
109
+ cwd: REPO_DIR,
110
+ stdio: "pipe",
111
+ timeout: 120000,
112
+ env: Object.assign({}, process.env, { GIT_TERMINAL_PROMPT: "0" })
113
+ }).toString().trim();
114
  }
115
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  function isValidJson(filePath) {
117
+ try {
118
+ if (!fs.existsSync(filePath)) return false;
119
+ var content = fs.readFileSync(filePath, "utf-8").trim();
120
+ if (!content) return false;
121
+ JSON.parse(content);
122
+ return true;
123
+ } catch (e) {
124
+ return false;
125
+ }
126
  }
127
 
128
  function copyItem(src, dst) {
129
+ if (fs.statSync(src).isDirectory()) {
130
+ execSync("cp -a " + src + " " + dst, { stdio: "pipe" });
131
+ } else {
132
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
133
+ fs.copyFileSync(src, dst);
134
+ }
135
  }
136
 
137
+ // -- git repo setup --------------------------
138
 
139
  function ensureRepo() {
140
+ if (fs.existsSync(path.join(REPO_DIR, ".git"))) return;
141
+ execSync("rm -rf " + REPO_DIR, { stdio: "pipe" });
142
+ try {
143
+ execSync(
144
+ "git clone --depth 1 " + repoUrl() + " " + REPO_DIR,
145
+ {
146
+ stdio: "pipe",
147
+ timeout: 120000,
148
+ env: Object.assign({}, process.env, { GIT_TERMINAL_PROMPT: "0" })
 
 
 
 
 
 
 
 
 
 
 
 
149
  }
150
+ );
151
+ } catch (e) {
152
+ fs.mkdirSync(REPO_DIR, { recursive: true });
153
+ execSync("git init " + REPO_DIR, { stdio: "pipe" });
154
+ git("remote add origin " + repoUrl());
 
 
 
155
  }
156
+ try {
157
+ git("config user.email openclaw@hf.space");
158
+ git("config user.name OpenClaw-Sync");
159
+ git("config pull.rebase false");
160
+ } catch (e) { /* non-fatal */ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  }
162
 
163
+ function pullLatest() {
164
+ try { git("fetch --quiet origin"); } catch (e) { /* ignore */ }
165
+ try {
166
+ git("pull --quiet --no-rebase origin main");
167
+ } catch (e) {
168
+ try { git("pull --quiet --no-rebase origin master"); } catch (e2) { /* no remote yet */ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  }
171
 
172
+ function commitAndPush(label) {
173
+ try {
174
+ git("add -A");
175
+ var changed = git("diff --cached --name-only");
176
+ if (!changed) return 0;
177
+ var count = changed.split("\n").filter(Boolean).length;
178
+ var ts = new Date().toISOString().replace("T", " ").substring(0, 19);
179
+ var tag = PROFILE ? "[" + PROFILE + "]" : "";
180
+ git("commit -m \"sync" + tag + " " + ts + "\"");
181
+ try {
182
+ git("push --quiet origin HEAD:main");
183
+ } catch (e) {
184
+ git("push --quiet origin HEAD:master");
 
 
 
 
 
 
 
 
 
 
 
185
  }
186
+ return count;
187
+ } catch (e) {
188
+ log("Commit/push failed: " + e.message);
189
+ return 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  }
 
 
 
 
191
  }