@memtensor/memos-cloud-openclaw-plugin 0.1.8-beta.1 → 0.1.8-beta.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.
@@ -0,0 +1,329 @@
1
+ import https from "https";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { spawn, exec } from "child_process";
6
+ import os from "os";
7
+
8
+ /**
9
+ * Kill a spawned child process and its entire process tree.
10
+ */
11
+ function killProcessTree(child) {
12
+ try {
13
+ if (process.platform === "win32") {
14
+ exec(`taskkill /pid ${child.pid} /T /F`, () => {});
15
+ } else {
16
+ // On Unix, kill the process group
17
+ process.kill(-child.pid, "SIGKILL");
18
+ }
19
+ } catch (e) {
20
+ // Fallback: try the basic kill
21
+ try { child.kill("SIGKILL"); } catch (_) {}
22
+ }
23
+ }
24
+
25
+ let isUpdating = false;
26
+
27
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
28
+
29
+ const CHECK_INTERVAL = 2 * 60 * 1000; // 12 hours check interval
30
+ const UPDATE_TIMEOUT = 3 * 60 * 1000; // 3 minutes timeout for the CLI update command to finish
31
+ const PLUGIN_NAME = "@memtensor/memos-cloud-openclaw-plugin";
32
+ const CHECK_FILE = path.join(os.tmpdir(), "memos_openclaw_update_check.json");
33
+
34
+ const ANSI = {
35
+ RESET: "\x1b[0m",
36
+ GREEN: "\x1b[32m",
37
+ YELLOW: "\x1b[33m",
38
+ CYAN: "\x1b[36m",
39
+ RED: "\x1b[31m"
40
+ };
41
+
42
+ function canExecuteCli(cliName, timeoutMs = 8000) {
43
+ return new Promise((resolve) => {
44
+ let done = false;
45
+ const child = spawn(cliName, ["--version"], { shell: true });
46
+ const timer = setTimeout(() => {
47
+ if (done) return;
48
+ done = true;
49
+ killProcessTree(child);
50
+ resolve({ ok: false, reason: `timeout after ${timeoutMs}ms` });
51
+ }, timeoutMs);
52
+
53
+ child.on("error", (err) => {
54
+ if (done) return;
55
+ done = true;
56
+ clearTimeout(timer);
57
+ resolve({ ok: false, reason: err?.message || String(err) });
58
+ });
59
+
60
+ child.on("close", (code) => {
61
+ if (done) return;
62
+ done = true;
63
+ clearTimeout(timer);
64
+ resolve({
65
+ ok: code === 0,
66
+ reason: code === 0 ? "" : `exit code ${code}`,
67
+ });
68
+ });
69
+ });
70
+ }
71
+
72
+
73
+ function getPackageVersion() {
74
+ try {
75
+ const pkgPath = path.join(__dirname, "..", "package.json");
76
+ const pkgData = fs.readFileSync(pkgPath, "utf-8");
77
+ const pkg = JSON.parse(pkgData);
78
+ return pkg.version;
79
+ } catch (err) {
80
+ return null;
81
+ }
82
+ }
83
+
84
+ function getLatestVersion(log) {
85
+ return new Promise((resolve, reject) => {
86
+ const req = https.get(
87
+ `https://registry.npmjs.org/${PLUGIN_NAME}/latest`,
88
+ { timeout: 5000 },
89
+ (res) => {
90
+ if (res.statusCode !== 200) {
91
+ req.destroy();
92
+ return reject(new Error(`Failed to fetch version, status: ${res.statusCode}`));
93
+ }
94
+
95
+ let body = "";
96
+ res.on("data", (chunk) => {
97
+ body += chunk;
98
+ });
99
+
100
+ res.on("end", () => {
101
+ try {
102
+ const data = JSON.parse(body);
103
+ resolve(data.version);
104
+ } catch (err) {
105
+ reject(err);
106
+ }
107
+ });
108
+ }
109
+ );
110
+
111
+ req.on("error", (err) => {
112
+ reject(err);
113
+ });
114
+
115
+ req.on("timeout", () => {
116
+ req.destroy();
117
+ reject(new Error("Timeout getting latest version"));
118
+ });
119
+ });
120
+ }
121
+
122
+ function compareVersions(v1, v2) {
123
+ // Split pre-release tags (e.g. 0.1.8-beta.1 -> "0.1.8" and "beta.1")
124
+ const split1 = v1.split("-");
125
+ const split2 = v2.split("-");
126
+ const parts1 = split1[0].split(".").map(Number);
127
+ const parts2 = split2[0].split(".").map(Number);
128
+
129
+ // Compare major.minor.patch
130
+ for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
131
+ const p1 = parts1[i] || 0;
132
+ const p2 = parts2[i] || 0;
133
+ if (p1 > p2) return 1;
134
+ if (p1 < p2) return -1;
135
+ }
136
+
137
+ // If base versions are equal, compare pre-release tags.
138
+ // A version WITH a pre-release tag is LOWER than a version WITHOUT one.
139
+ // e.g. 0.1.8-beta is less than 0.1.8. 0.1.8 is the final release.
140
+ const hasPre1 = split1.length > 1;
141
+ const hasPre2 = split2.length > 1;
142
+
143
+ if (hasPre1 && !hasPre2) return -1; // v1 is a beta, v2 is a full release
144
+ if (!hasPre1 && hasPre2) return 1; // v1 is a full release, v2 is a beta
145
+ if (!hasPre1 && !hasPre2) return 0; // both are full releases and equal
146
+
147
+ // If both are pre-releases, do a basic string compare on the tag
148
+ // "alpha" < "beta" < "rc"
149
+ if (split1[1] > split2[1]) return 1;
150
+ if (split1[1] < split2[1]) return -1;
151
+
152
+ return 0;
153
+ }
154
+
155
+ export function startUpdateChecker(log) {
156
+ // Only start the interval if we are in the gateway
157
+ const isGateway = process.argv.includes("gateway");
158
+ if (!isGateway) {
159
+ return;
160
+ }
161
+
162
+ const runCheck = async () => {
163
+ if (isUpdating) {
164
+ log.info?.(`${ANSI.YELLOW}[memos-cloud] An update sequence is currently in progress, skipping this check.${ANSI.RESET}`);
165
+ return;
166
+ }
167
+
168
+ // TRULY PREVENT LOOPS: The instant we start a check, record the time BEFORE any network or processing happens.
169
+ // This absolutely guarantees that even if the network hangs, NPM crashes, or openclaw update causes an immediate hot reload,
170
+ // the system has already advanced the 12-hour/1-min clock and will NOT re-enter this function on boot.
171
+ try {
172
+ fs.writeFileSync(CHECK_FILE, JSON.stringify({ time: Date.now() }));
173
+ } catch (e) {
174
+ log.warn?.(`${ANSI.RED}[memos-cloud] Failed to write timestamp file: ${e.message}${ANSI.RESET}`);
175
+ }
176
+
177
+ const currentVersion = getPackageVersion();
178
+ if (!currentVersion) {
179
+ log.warn?.(`${ANSI.RED}[memos-cloud] Could not read current version from package.json${ANSI.RESET}`);
180
+ return;
181
+ }
182
+
183
+ try {
184
+ const latestVersion = await getLatestVersion(log);
185
+
186
+ // Normal version check
187
+ if (compareVersions(latestVersion, currentVersion) <= 0) {
188
+ return;
189
+ }
190
+
191
+ // Check if we have write permission to the plugin directory before attempting update
192
+ const pluginDir = path.join(__dirname, "..");
193
+ try {
194
+ fs.accessSync(pluginDir, fs.constants.W_OK);
195
+ } catch (err) {
196
+ log.warn?.(`${ANSI.YELLOW}[memos-cloud] Update available (${latestVersion}), but skipping auto-update due to missing write permissions in ${pluginDir}. Please run manually with sudo.${ANSI.RESET}`);
197
+ return;
198
+ }
199
+
200
+ log.info?.(`${ANSI.YELLOW}[memos-cloud] Update available: ${currentVersion} -> ${latestVersion}. Updating in background...${ANSI.RESET}`);
201
+
202
+ let dotCount = 0;
203
+ const progressInterval = setInterval(() => {
204
+ dotCount++;
205
+ const dots = ".".repeat(dotCount % 4);
206
+ log.info?.(`${ANSI.YELLOW}[memos-cloud] Update in progress for memos-cloud-openclaw-plugin${dots}${ANSI.RESET}`);
207
+ }, 30000); // Log every 30 seconds to show it's still alive without spamming
208
+
209
+ const cliName = (() => {
210
+ // Check the full path of the entry script (e.g., .../moltbot/bin/index.js) or the executable
211
+ const scriptPath = process.argv[1] ? process.argv[1].toLowerCase() : "";
212
+ const execPath = process.execPath ? process.execPath.toLowerCase() : "";
213
+
214
+ if (scriptPath.includes("moltbot") || execPath.includes("moltbot")) return "moltbot";
215
+ if (scriptPath.includes("clawdbot") || execPath.includes("clawdbot")) return "clawdbot";
216
+ return "openclaw";
217
+ })();
218
+
219
+ const preflight = await canExecuteCli(cliName);
220
+ if (!preflight.ok) {
221
+ log.warn?.(
222
+ `${ANSI.YELLOW}[memos-cloud] Update available (${latestVersion}), but cannot execute CLI '${cliName}' (${preflight.reason}). Please update manually: ${cliName} plugins update memos-cloud-openclaw-plugin${ANSI.RESET}`,
223
+ );
224
+ return;
225
+ }
226
+
227
+ isUpdating = true;
228
+ const spawnOpts = { shell: true };
229
+ // On Unix, detach the process so we can kill the entire process group on timeout
230
+ if (process.platform !== "win32") {
231
+ spawnOpts.detached = true;
232
+ }
233
+ const child = spawn(cliName, ["plugins", "update", "memos-cloud-openclaw-plugin"], spawnOpts);
234
+ let finished = false;
235
+ const finishUpdateSequence = () => {
236
+ if (finished) return;
237
+ finished = true;
238
+ clearTimeout(updateTimeout);
239
+ clearInterval(progressInterval);
240
+ isUpdating = false;
241
+ };
242
+
243
+ // Timeout mechanism: forcefully kill the update process if it hangs for more than the configured timeout
244
+ const updateTimeout = setTimeout(() => {
245
+ log.warn?.(`${ANSI.RED}[memos-cloud] Update process timed out. Please try manually running: ${cliName} plugins update memos-cloud-openclaw-plugin${ANSI.RESET}`);
246
+ killProcessTree(child);
247
+
248
+ // Fallback: if kill failed and the close event never fires, forcefully release the lock after 5 seconds
249
+ setTimeout(() => {
250
+ if (isUpdating) {
251
+ finishUpdateSequence();
252
+ }
253
+ }, 5000);
254
+ }, UPDATE_TIMEOUT);
255
+
256
+ child.stdout.on("data", (data) => {
257
+ const outText = data.toString();
258
+ log.info?.(`${ANSI.CYAN}[${cliName}-cli]${ANSI.RESET}\n${outText.trim()}`);
259
+
260
+ // Auto-reply to any [y/N] prompts from the CLI
261
+ if (outText.toLowerCase().includes("[y/n]")) {
262
+ child.stdin.write("y\n");
263
+ }
264
+ });
265
+
266
+ child.stderr.on("data", (data) => {
267
+ const errText = data.toString();
268
+ log.warn?.(`${ANSI.RED}[${cliName}-cli]${ANSI.RESET}\n${errText.trim()}`);
269
+
270
+ // Some CLIs output interactive prompts to stderr instead of stdout
271
+ if (errText.toLowerCase().includes("[y/n]")) {
272
+ child.stdin.write("y\n");
273
+ }
274
+ });
275
+
276
+ child.on("close", (code) => {
277
+ finishUpdateSequence();
278
+
279
+ // Wait for a brief moment to let file system sync if needed
280
+ setTimeout(() => {
281
+ const postUpdateVersion = getPackageVersion();
282
+ const actuallyUpdated = (postUpdateVersion === latestVersion) && (postUpdateVersion !== currentVersion);
283
+
284
+ if (code !== 0 || !actuallyUpdated) {
285
+ log.warn?.(`${ANSI.RED}[memos-cloud] Auto-update failed or version did not change. Please refer to the CLI logs above, or run manually: ${cliName} plugins update memos-cloud-openclaw-plugin${ANSI.RESET}`);
286
+ } else {
287
+ log.info?.(`${ANSI.GREEN}[memos-cloud] Successfully updated to version ${latestVersion}. Please restart the gateway to apply changes.${ANSI.RESET}`);
288
+ }
289
+ }, 1000); // Small 1-second buffer for file systems
290
+ });
291
+
292
+ child.on("error", (err) => {
293
+ finishUpdateSequence();
294
+ log.warn?.(
295
+ `${ANSI.RED}[memos-cloud] Failed to start auto-update process: ${err?.message || String(err)}. Please run manually: ${cliName} plugins update memos-cloud-openclaw-plugin${ANSI.RESET}`,
296
+ );
297
+ });
298
+
299
+ } catch (error) {
300
+ log.warn?.(`${ANSI.RED}[memos-cloud] Update check failed entirely: ${error.message}${ANSI.RESET}`);
301
+ }
302
+ };
303
+
304
+ // Check when we last ran
305
+ let lastCheckTime = 0;
306
+ try {
307
+ if (fs.existsSync(CHECK_FILE)) {
308
+ const data = JSON.parse(fs.readFileSync(CHECK_FILE, "utf-8"));
309
+ lastCheckTime = data.time || 0;
310
+ }
311
+ } catch (e) {}
312
+
313
+ const now = Date.now();
314
+ const timeSinceLastCheck = now - lastCheckTime;
315
+
316
+ // If the interval has passed, run it IMMEDIATELY without delay.
317
+ // The immediate file-write at the top of runCheck() will prevent loop scenarios.
318
+ if (timeSinceLastCheck >= CHECK_INTERVAL) {
319
+ runCheck();
320
+ setInterval(runCheck, CHECK_INTERVAL);
321
+ } else {
322
+ // If it hasn't been the full interval yet, wait the remaining time, then trigger interval
323
+ const timeUntilNextCheck = CHECK_INTERVAL - timeSinceLastCheck;
324
+ setTimeout(() => {
325
+ runCheck();
326
+ setInterval(runCheck, CHECK_INTERVAL);
327
+ }, timeUntilNextCheck);
328
+ }
329
+ }