@memtensor/memos-cloud-openclaw-plugin 0.1.19-beta.0 → 0.1.20-beta.0

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.
@@ -1,116 +1,143 @@
1
- import { randomBytes, randomUUID } from "node:crypto";
2
- import { readFileSync, writeFileSync } from "node:fs";
3
- import { homedir } from "node:os";
4
- import { join } from "node:path";
5
-
6
- const ARMS_ENDPOINT = "https://proj-xtrace-e218d9316b328f196a3c640cc7ca84-cn-hangzhou.cn-hangzhou.log.aliyuncs.com/rum/web/v2?workspace=default-cms-1026429231103299-cn-hangzhou&service_id=a3u72ukxmr@bed68dd882dd823439015"
7
- const ARMS_PID = "a3u72ukxmr@c42a249fb14f4d9";
8
- const ARMS_ENV = "prod";
9
- const ARMS_UID_FILE = new URL("../.memos_arms_uid", import.meta.url);
10
-
11
- let armsUidCache = "";
12
-
13
- function readUidFromFile() {
14
- try {
15
- return readFileSync(ARMS_UID_FILE, "utf-8").trim();
16
- } catch {
17
- return "";
18
- }
19
- }
20
-
21
- function writeUidToFile(value) {
22
- try {
23
- writeFileSync(ARMS_UID_FILE, `${value}\n`, { mode: 0o600 });
24
- } catch {}
25
- }
26
-
27
- function createEventId() {
28
- const traceId = randomBytes(16).toString("hex");
29
- const spanId = randomBytes(8).toString("hex");
30
- return `00-${traceId}-${spanId}`;
31
- }
32
-
33
- function readOpenClawDeviceId(log) {
34
- try {
35
- const deviceFile = join(homedir(), ".openclaw", "identity", "device.json");
36
- const content = readFileSync(deviceFile, "utf-8");
37
- const data = JSON.parse(content);
38
- if (data && typeof data.deviceId === "string" && data.deviceId.trim()) {
39
- return `uid_${data.deviceId.trim()}`;
40
- }
41
- } catch (err) {
42
- log?.warn?.(`[memos-cloud] Failed to read OpenClaw deviceId: ${String(err)}`);
43
- }
44
- return "";
45
- }
46
-
47
- function loadArmsUid(log) {
48
- if (armsUidCache) return armsUidCache;
49
-
50
- const openclawDevice = readOpenClawDeviceId(log);
51
- if (openclawDevice) {
52
- armsUidCache = openclawDevice;
53
- writeUidToFile(armsUidCache);
54
- return armsUidCache;
55
- }
56
-
57
- const fromUidFile = readUidFromFile();
58
- if (fromUidFile) {
59
- armsUidCache = fromUidFile;
60
- return armsUidCache;
61
- }
62
-
63
- armsUidCache = `uid_${randomUUID()}`;
64
- writeUidToFile(armsUidCache);
65
- return armsUidCache;
66
- }
67
-
68
- function buildPayload(ctx, eventName, payload, log) {
69
- return {
70
- app: {
71
- id: ARMS_PID,
72
- env: ARMS_ENV,
73
- type: "node",
74
- },
75
- user: { id: loadArmsUid(log) },
76
- session: { id: ctx.sessionId },
77
- net: {},
78
- view: { id: "plugin", name: "memos-cloud-openclaw" },
79
- events: [
80
- {
81
- event_id: createEventId(),
82
- event_type: 'custom',
83
- type: "memos_plugin",
84
- group: "memos_cloud",
85
- name: eventName,
86
- timestamp: +new Date(),
87
- properties: { ...payload }
88
- }
89
- ]
90
- };
91
- }
92
-
93
- export async function reportRumEvent(eventName, payload, cfg, ctx, log) {
94
- if (!cfg.rumEnabled) return;
95
- const controller = new AbortController();
96
- const timeoutId = setTimeout(
97
- () => controller.abort(),
98
- Number.isFinite(cfg.rumTimeoutMs) ? Math.max(1000, cfg.rumTimeoutMs) : 3000,
99
- );
100
- try {
101
- const body = buildPayload(ctx, eventName, payload, log);
102
- const res = await fetch(ARMS_ENDPOINT, {
103
- method: "POST",
104
- headers: { "Content-Type": "text/plain" },
105
- body: JSON.stringify(body),
106
- signal: controller.signal,
107
- });
108
- if (!res.ok) {
109
- throw new Error(`HTTP ${res.status}`);
110
- }
111
- } catch (err) {
112
- log.warn?.(`[memos-cloud] RUM report failed: ${String(err)}`);
113
- } finally {
114
- clearTimeout(timeoutId);
115
- }
116
- }
1
+ import { randomBytes, randomUUID } from "node:crypto";
2
+ import { readFileSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ const ARMS_UID_FILE = new URL("../.memos_arms_uid", import.meta.url);
7
+ const TELEMETRY_CREDENTIALS_FILE = new URL("../telemetry.credentials.json", import.meta.url);
8
+
9
+ let armsUidCache = "";
10
+ let telemetryCredentialsCache;
11
+
12
+ function loadTelemetryCredentials(log) {
13
+ if (telemetryCredentialsCache) return telemetryCredentialsCache;
14
+ if (process.env.MEMOS_ARMS_ENDPOINT) {
15
+ telemetryCredentialsCache = {
16
+ endpoint: String(process.env.MEMOS_ARMS_ENDPOINT || "").trim(),
17
+ pid: String(process.env.MEMOS_ARMS_PID || "").trim(),
18
+ env: String(process.env.MEMOS_ARMS_ENV || "prod").trim() || "prod",
19
+ };
20
+ } else {
21
+ try {
22
+ const parsed = JSON.parse(readFileSync(TELEMETRY_CREDENTIALS_FILE, "utf-8"));
23
+ telemetryCredentialsCache = {
24
+ endpoint: String(parsed.endpoint || "").trim(),
25
+ pid: String(parsed.pid || "").trim(),
26
+ env: String(parsed.env || "prod").trim() || "prod",
27
+ };
28
+ } catch {
29
+ telemetryCredentialsCache = { endpoint: "", pid: "", env: "prod" };
30
+ }
31
+ }
32
+ if (!telemetryCredentialsCache.endpoint || !telemetryCredentialsCache.pid) {
33
+ log?.debug?.("[memos-cloud] RUM disabled: telemetry credentials are incomplete.");
34
+ }
35
+ return telemetryCredentialsCache;
36
+ }
37
+
38
+ function readUidFromFile() {
39
+ try {
40
+ return readFileSync(ARMS_UID_FILE, "utf-8").trim();
41
+ } catch {
42
+ return "";
43
+ }
44
+ }
45
+
46
+ function writeUidToFile(value) {
47
+ try {
48
+ writeFileSync(ARMS_UID_FILE, `${value}\n`, { mode: 0o600 });
49
+ } catch {}
50
+ }
51
+
52
+ function createEventId() {
53
+ const traceId = randomBytes(16).toString("hex");
54
+ const spanId = randomBytes(8).toString("hex");
55
+ return `00-${traceId}-${spanId}`;
56
+ }
57
+
58
+ function readOpenClawDeviceId(log) {
59
+ try {
60
+ const deviceFile = join(homedir(), ".openclaw", "identity", "device.json");
61
+ const content = readFileSync(deviceFile, "utf-8");
62
+ const data = JSON.parse(content);
63
+ if (data && typeof data.deviceId === "string" && data.deviceId.trim()) {
64
+ return `uid_${data.deviceId.trim()}`;
65
+ }
66
+ } catch (err) {
67
+ log?.warn?.(`[memos-cloud] Failed to read OpenClaw deviceId: ${String(err)}`);
68
+ }
69
+ return "";
70
+ }
71
+
72
+ function loadArmsUid(log) {
73
+ if (armsUidCache) return armsUidCache;
74
+
75
+ const openclawDevice = readOpenClawDeviceId(log);
76
+ if (openclawDevice) {
77
+ armsUidCache = openclawDevice;
78
+ writeUidToFile(armsUidCache);
79
+ return armsUidCache;
80
+ }
81
+
82
+ const fromUidFile = readUidFromFile();
83
+ if (fromUidFile) {
84
+ armsUidCache = fromUidFile;
85
+ return armsUidCache;
86
+ }
87
+
88
+ armsUidCache = `uid_${randomUUID()}`;
89
+ writeUidToFile(armsUidCache);
90
+ return armsUidCache;
91
+ }
92
+
93
+ function buildPayload(ctx, eventName, payload, log, credentials) {
94
+ return {
95
+ app: {
96
+ id: credentials.pid,
97
+ env: credentials.env,
98
+ type: "node",
99
+ },
100
+ user: { id: loadArmsUid(log) },
101
+ session: { id: ctx.sessionId },
102
+ net: {},
103
+ view: { id: "plugin", name: "memos-cloud-openclaw" },
104
+ events: [
105
+ {
106
+ event_id: createEventId(),
107
+ event_type: 'custom',
108
+ type: "memos_plugin",
109
+ group: "memos_cloud",
110
+ name: eventName,
111
+ timestamp: +new Date(),
112
+ properties: { ...payload }
113
+ }
114
+ ]
115
+ };
116
+ }
117
+
118
+ export async function reportRumEvent(eventName, payload, cfg, ctx, log) {
119
+ if (!cfg.rumEnabled) return;
120
+ const credentials = loadTelemetryCredentials(log);
121
+ if (!credentials.endpoint || !credentials.pid) return;
122
+ const controller = new AbortController();
123
+ const timeoutId = setTimeout(
124
+ () => controller.abort(),
125
+ Number.isFinite(cfg.rumTimeoutMs) ? Math.max(1000, cfg.rumTimeoutMs) : 3000,
126
+ );
127
+ try {
128
+ const body = buildPayload(ctx, eventName, payload, log, credentials);
129
+ const res = await fetch(credentials.endpoint, {
130
+ method: "POST",
131
+ headers: { "Content-Type": "text/plain" },
132
+ body: JSON.stringify(body),
133
+ signal: controller.signal,
134
+ });
135
+ if (!res.ok) {
136
+ throw new Error(`HTTP ${res.status}`);
137
+ }
138
+ } catch (err) {
139
+ log.warn?.(`[memos-cloud] RUM report failed: ${String(err)}`);
140
+ } finally {
141
+ clearTimeout(timeoutId);
142
+ }
143
+ }
@@ -1,202 +1,174 @@
1
- import https from "https";
2
- import fs from "fs";
3
- import path from "path";
4
- import { fileURLToPath } from "url";
5
- import os from "os";
6
-
7
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
-
9
- const CHECK_INTERVAL = 12 * 60 * 60 * 1000; // 12 hours check interval
10
- const PLUGIN_NAME = "@memtensor/memos-cloud-openclaw-plugin";
11
- const CHECK_FILE = path.join(os.tmpdir(), "memos_openclaw_update_check.json");
12
-
13
- const ANSI = {
14
- RESET: "\x1b[0m",
15
- GREEN: "\x1b[32m",
16
- YELLOW: "\x1b[33m",
17
- CYAN: "\x1b[36m",
18
- RED: "\x1b[31m"
19
- };
20
-
21
-
22
- export function getPackageVersion() {
23
- try {
24
- const pkgPath = path.join(__dirname, "..", "package.json");
25
- const pkgData = fs.readFileSync(pkgPath, "utf-8");
26
- const pkg = JSON.parse(pkgData);
27
- return pkg.version;
28
- } catch (err) {
29
- return null;
30
- }
31
- }
32
-
33
- export function getLatestVersion() {
34
- return new Promise((resolve, reject) => {
35
- const req = https.get(
36
- `https://registry.npmjs.org/${PLUGIN_NAME}/latest`,
37
- { timeout: 5000 },
38
- (res) => {
39
- if (res.statusCode !== 200) {
40
- req.destroy();
41
- return reject(new Error(`Failed to fetch version, status: ${res.statusCode}`));
42
- }
43
-
44
- let body = "";
45
- res.on("data", (chunk) => {
46
- body += chunk;
47
- });
48
-
49
- res.on("end", () => {
50
- try {
51
- const data = JSON.parse(body);
52
- resolve(data.version);
53
- } catch (err) {
54
- reject(err);
55
- }
56
- });
57
- }
58
- );
59
-
60
- req.on("error", (err) => {
61
- reject(err);
62
- });
63
-
64
- req.on("timeout", () => {
65
- req.destroy();
66
- reject(new Error("Timeout getting latest version"));
67
- });
68
- });
69
- }
70
-
71
- export function compareVersions(v1, v2) {
72
- // Split pre-release tags (e.g. 0.1.8-beta.1 -> "0.1.8" and "beta.1")
73
- const split1 = v1.split("-");
74
- const split2 = v2.split("-");
75
- const parts1 = split1[0].split(".").map(Number);
76
- const parts2 = split2[0].split(".").map(Number);
77
-
78
- // Compare major.minor.patch
79
- for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
80
- const p1 = parts1[i] || 0;
81
- const p2 = parts2[i] || 0;
82
- if (p1 > p2) return 1;
83
- if (p1 < p2) return -1;
84
- }
85
-
86
- // If base versions are equal, compare pre-release tags.
87
- // A version WITH a pre-release tag is LOWER than a version WITHOUT one.
88
- // e.g. 0.1.8-beta is less than 0.1.8. 0.1.8 is the final release.
89
- const hasPre1 = split1.length > 1;
90
- const hasPre2 = split2.length > 1;
91
-
92
- if (hasPre1 && !hasPre2) return -1; // v1 is a beta, v2 is a full release
93
- if (!hasPre1 && hasPre2) return 1; // v1 is a full release, v2 is a beta
94
- if (!hasPre1 && !hasPre2) return 0; // both are full releases and equal
95
-
96
- // If both are pre-releases, do a basic string compare on the tag
97
- // "alpha" < "beta" < "rc"
98
- if (split1[1] > split2[1]) return 1;
99
- if (split1[1] < split2[1]) return -1;
100
-
101
- return 0;
102
- }
103
-
104
- function detectCliName() {
105
- // Check the full path of the entry script (e.g., .../moltbot/bin/index.js) or the executable
106
- const scriptPath = process.argv[1] ? process.argv[1].toLowerCase() : "";
107
- const execPath = process.execPath ? process.execPath.toLowerCase() : "";
108
-
109
- if (scriptPath.includes("moltbot") || execPath.includes("moltbot")) return "moltbot";
110
- if (scriptPath.includes("clawdbot") || execPath.includes("clawdbot")) return "clawdbot";
111
- return "openclaw";
112
- }
113
-
114
- export async function checkForPluginUpdate() {
115
- const currentVersion = getPackageVersion();
116
- if (!currentVersion) {
117
- throw new Error("Could not read current version from package.json");
118
- }
119
-
120
- const latestVersion = await getLatestVersion();
121
- const updateAvailable = compareVersions(latestVersion, currentVersion) > 0;
122
- const cliName = detectCliName();
123
-
124
- return {
125
- pluginName: PLUGIN_NAME,
126
- currentVersion,
127
- latestVersion,
128
- updateAvailable,
129
- cliName,
130
- updateCommand: `${cliName} plugins update memos-cloud-openclaw-plugin`,
131
- checkedAt: new Date().toISOString(),
132
- };
133
- }
134
-
135
- export function startUpdateChecker(log) {
136
- // Only start the interval if we are in the gateway
137
- const isGateway = process.argv.includes("gateway");
138
- if (!isGateway) {
139
- return;
140
- }
141
-
142
- const runCheck = async () => {
143
- // TRULY PREVENT LOOPS: The instant we start a check, record the time BEFORE any network or processing happens.
144
- // This absolutely guarantees that even if the network hangs, NPM crashes, or openclaw update causes an immediate hot reload,
145
- // the system has already advanced the 12-hour/1-min clock and will NOT re-enter this function on boot.
146
- try {
147
- fs.writeFileSync(CHECK_FILE, JSON.stringify({ time: Date.now() }));
148
- } catch (e) {
149
- log.warn?.(`${ANSI.RED}[memos-cloud] Failed to write timestamp file: ${e.message}${ANSI.RESET}`);
150
- }
151
-
152
- try {
153
- const updateStatus = await checkForPluginUpdate();
154
-
155
- // Normal version check
156
- if (!updateStatus.updateAvailable) {
157
- return;
158
- }
159
-
160
- const border = "=".repeat(64);
161
- log.info?.("");
162
- log.info?.(`${ANSI.GREEN}${border}${ANSI.RESET}`);
163
- log.info?.(`${ANSI.YELLOW}🚀 [memos-cloud] NEW VERSION AVAILABLE!${ANSI.RESET}`);
164
- log.info?.(`${ANSI.CYAN}📦 Current version : ${updateStatus.currentVersion}${ANSI.RESET}`);
165
- log.info?.(`${ANSI.GREEN}✨ Latest version : ${updateStatus.latestVersion}${ANSI.RESET}`);
166
- log.info?.(`${ANSI.CYAN}────────────────────────────────────────────────────────────────${ANSI.RESET}`);
167
- log.info?.(`${ANSI.GREEN}Please run the following command to update manually:${ANSI.RESET}`);
168
- log.info?.(`${ANSI.YELLOW}${updateStatus.updateCommand}${ANSI.RESET}`);
169
- log.info?.(`${ANSI.GREEN}${border}${ANSI.RESET}`);
170
- log.info?.("");
171
-
172
- } catch (error) {
173
- log.warn?.(`${ANSI.RED}[memos-cloud] Update check failed entirely: ${error.message}${ANSI.RESET}`);
174
- }
175
- };
176
-
177
- // Check when we last ran
178
- let lastCheckTime = 0;
179
- try {
180
- if (fs.existsSync(CHECK_FILE)) {
181
- const data = JSON.parse(fs.readFileSync(CHECK_FILE, "utf-8"));
182
- lastCheckTime = data.time || 0;
183
- }
184
- } catch (e) {}
185
-
186
- const now = Date.now();
187
- const timeSinceLastCheck = now - lastCheckTime;
188
-
189
- // If the interval has passed, run it IMMEDIATELY without delay.
190
- // The immediate file-write at the top of runCheck() will prevent loop scenarios.
191
- if (timeSinceLastCheck >= CHECK_INTERVAL) {
192
- runCheck();
193
- setInterval(runCheck, CHECK_INTERVAL);
194
- } else {
195
- // If it hasn't been the full interval yet, wait the remaining time, then trigger interval
196
- const timeUntilNextCheck = CHECK_INTERVAL - timeSinceLastCheck;
197
- setTimeout(() => {
198
- runCheck();
199
- setInterval(runCheck, CHECK_INTERVAL);
200
- }, timeUntilNextCheck);
201
- }
202
- }
1
+ import https from "https";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+ import os from "os";
6
+ import { compareSemver } from "./semver.js";
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+
10
+ const CHECK_INTERVAL = 12 * 60 * 60 * 1000; // 12 hours check interval
11
+ const PLUGIN_NAME = "@memtensor/memos-cloud-openclaw-plugin";
12
+ const CHECK_FILE = path.join(os.tmpdir(), "memos_openclaw_update_check.json");
13
+
14
+ const ANSI = {
15
+ RESET: "\x1b[0m",
16
+ GREEN: "\x1b[32m",
17
+ YELLOW: "\x1b[33m",
18
+ CYAN: "\x1b[36m",
19
+ RED: "\x1b[31m"
20
+ };
21
+
22
+
23
+ export function getPackageVersion() {
24
+ try {
25
+ const pkgPath = path.join(__dirname, "..", "package.json");
26
+ const pkgData = fs.readFileSync(pkgPath, "utf-8");
27
+ const pkg = JSON.parse(pkgData);
28
+ return pkg.version;
29
+ } catch (err) {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ export function getLatestVersion() {
35
+ return new Promise((resolve, reject) => {
36
+ const req = https.get(
37
+ `https://registry.npmjs.org/${PLUGIN_NAME}/latest`,
38
+ { timeout: 5000 },
39
+ (res) => {
40
+ if (res.statusCode !== 200) {
41
+ req.destroy();
42
+ return reject(new Error(`Failed to fetch version, status: ${res.statusCode}`));
43
+ }
44
+
45
+ let body = "";
46
+ res.on("data", (chunk) => {
47
+ body += chunk;
48
+ });
49
+
50
+ res.on("end", () => {
51
+ try {
52
+ const data = JSON.parse(body);
53
+ resolve(data.version);
54
+ } catch (err) {
55
+ reject(err);
56
+ }
57
+ });
58
+ }
59
+ );
60
+
61
+ req.on("error", (err) => {
62
+ reject(err);
63
+ });
64
+
65
+ req.on("timeout", () => {
66
+ req.destroy();
67
+ reject(new Error("Timeout getting latest version"));
68
+ });
69
+ });
70
+ }
71
+
72
+ export function compareVersions(v1, v2) {
73
+ return compareSemver(v1, v2);
74
+ }
75
+
76
+ function detectCliName() {
77
+ // Check the full path of the entry script (e.g., .../moltbot/bin/index.js) or the executable
78
+ const scriptPath = process.argv[1] ? process.argv[1].toLowerCase() : "";
79
+ const execPath = process.execPath ? process.execPath.toLowerCase() : "";
80
+
81
+ if (scriptPath.includes("moltbot") || execPath.includes("moltbot")) return "moltbot";
82
+ if (scriptPath.includes("clawdbot") || execPath.includes("clawdbot")) return "clawdbot";
83
+ return "openclaw";
84
+ }
85
+
86
+ export async function checkForPluginUpdate() {
87
+ const currentVersion = getPackageVersion();
88
+ if (!currentVersion) {
89
+ throw new Error("Could not read current version from package.json");
90
+ }
91
+
92
+ const latestVersion = await getLatestVersion();
93
+ const updateAvailable = compareVersions(latestVersion, currentVersion) > 0;
94
+ const cliName = detectCliName();
95
+
96
+ return {
97
+ pluginName: PLUGIN_NAME,
98
+ currentVersion,
99
+ latestVersion,
100
+ updateAvailable,
101
+ cliName,
102
+ updateCommand: `${cliName} plugins update memos-cloud-openclaw-plugin`,
103
+ checkedAt: new Date().toISOString(),
104
+ };
105
+ }
106
+
107
+ export function startUpdateChecker(log) {
108
+ // Only start the interval if we are in the gateway
109
+ const isGateway = process.argv.includes("gateway");
110
+ if (!isGateway) {
111
+ return;
112
+ }
113
+
114
+ const runCheck = async () => {
115
+ // TRULY PREVENT LOOPS: The instant we start a check, record the time BEFORE any network or processing happens.
116
+ // This absolutely guarantees that even if the network hangs, NPM crashes, or openclaw update causes an immediate hot reload,
117
+ // the system has already advanced the 12-hour/1-min clock and will NOT re-enter this function on boot.
118
+ try {
119
+ fs.writeFileSync(CHECK_FILE, JSON.stringify({ time: Date.now() }));
120
+ } catch (e) {
121
+ log.warn?.(`${ANSI.RED}[memos-cloud] Failed to write timestamp file: ${e.message}${ANSI.RESET}`);
122
+ }
123
+
124
+ try {
125
+ const updateStatus = await checkForPluginUpdate();
126
+
127
+ // Normal version check
128
+ if (!updateStatus.updateAvailable) {
129
+ return;
130
+ }
131
+
132
+ const border = "=".repeat(64);
133
+ log.info?.("");
134
+ log.info?.(`${ANSI.GREEN}${border}${ANSI.RESET}`);
135
+ log.info?.(`${ANSI.YELLOW}🚀 [memos-cloud] NEW VERSION AVAILABLE!${ANSI.RESET}`);
136
+ log.info?.(`${ANSI.CYAN}📦 Current version : ${updateStatus.currentVersion}${ANSI.RESET}`);
137
+ log.info?.(`${ANSI.GREEN}✨ Latest version : ${updateStatus.latestVersion}${ANSI.RESET}`);
138
+ log.info?.(`${ANSI.CYAN}────────────────────────────────────────────────────────────────${ANSI.RESET}`);
139
+ log.info?.(`${ANSI.GREEN}Please run the following command to update manually:${ANSI.RESET}`);
140
+ log.info?.(`${ANSI.YELLOW}${updateStatus.updateCommand}${ANSI.RESET}`);
141
+ log.info?.(`${ANSI.GREEN}${border}${ANSI.RESET}`);
142
+ log.info?.("");
143
+
144
+ } catch (error) {
145
+ log.warn?.(`${ANSI.RED}[memos-cloud] Update check failed entirely: ${error.message}${ANSI.RESET}`);
146
+ }
147
+ };
148
+
149
+ // Check when we last ran
150
+ let lastCheckTime = 0;
151
+ try {
152
+ if (fs.existsSync(CHECK_FILE)) {
153
+ const data = JSON.parse(fs.readFileSync(CHECK_FILE, "utf-8"));
154
+ lastCheckTime = data.time || 0;
155
+ }
156
+ } catch (e) {}
157
+
158
+ const now = Date.now();
159
+ const timeSinceLastCheck = now - lastCheckTime;
160
+
161
+ // If the interval has passed, run it IMMEDIATELY without delay.
162
+ // The immediate file-write at the top of runCheck() will prevent loop scenarios.
163
+ if (timeSinceLastCheck >= CHECK_INTERVAL) {
164
+ runCheck();
165
+ setInterval(runCheck, CHECK_INTERVAL);
166
+ } else {
167
+ // If it hasn't been the full interval yet, wait the remaining time, then trigger interval
168
+ const timeUntilNextCheck = CHECK_INTERVAL - timeSinceLastCheck;
169
+ setTimeout(() => {
170
+ runCheck();
171
+ setInterval(runCheck, CHECK_INTERVAL);
172
+ }, timeUntilNextCheck);
173
+ }
174
+ }