@memtensor/memos-cloud-openclaw-plugin 0.1.14 → 0.1.15-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.
- package/LICENSE +201 -201
- package/README.md +316 -316
- package/README_ZH.md +321 -321
- package/clawdbot.plugin.json +6 -1
- package/index.js +18 -17
- package/lib/arms-reporter.js +116 -116
- package/lib/check-update.js +187 -187
- package/lib/config-resolution-schema.js +50 -50
- package/lib/config-ui/app.css +920 -920
- package/lib/config-ui/app.js +1290 -1290
- package/lib/config-ui/index.html +112 -112
- package/lib/memos-cloud-api.js +848 -848
- package/moltbot.plugin.json +6 -1
- package/openclaw.plugin.json +6 -1
- package/package.json +46 -46
- package/scripts/sync-version.js +45 -45
- package/test/direct-session-user-id.test.mjs +94 -94
- package/test/query-strip.test.mjs +381 -381
package/clawdbot.plugin.json
CHANGED
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
"id": "memos-cloud-openclaw-plugin",
|
|
3
3
|
"name": "MemOS Cloud OpenClaw Plugin",
|
|
4
4
|
"description": "MemOS Cloud recall + add memory via lifecycle hooks",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.15-beta.0",
|
|
6
6
|
"kind": "lifecycle",
|
|
7
7
|
"main": "./index.js",
|
|
8
|
+
"activation": {
|
|
9
|
+
"onCapabilities": [
|
|
10
|
+
"hook"
|
|
11
|
+
]
|
|
12
|
+
},
|
|
8
13
|
"configSchema": {
|
|
9
14
|
"type": "object",
|
|
10
15
|
"properties": {
|
package/index.js
CHANGED
|
@@ -468,28 +468,29 @@ export default {
|
|
|
468
468
|
hostVersion === null ||
|
|
469
469
|
compareVersionStrings(hostVersion, HOOK_POLICY_MIN_VERSION) >= 0;
|
|
470
470
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
//
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
471
|
+
void (async () => {
|
|
472
|
+
const ready = await waitForGatewayReady(api.config, log);
|
|
473
|
+
if (!ready || configUiStartupCancelled) return;
|
|
474
|
+
|
|
475
|
+
// Patch hook policy AFTER gateway is fully ready. Writing the config
|
|
476
|
+
// file at this point triggers the gateway's built-in config-change
|
|
477
|
+
// watcher which will auto-restart, making agent_end effective without
|
|
478
|
+
// requiring the user to manually restart.
|
|
479
|
+
if (needsHookPolicy) {
|
|
480
|
+
try {
|
|
481
|
+
const policyResult = ensurePluginHookPolicy(api.config, log);
|
|
482
|
+
if (policyResult?.error) {
|
|
483
|
+
log.warn?.(
|
|
484
|
+
`[memos-cloud] hook policy check skipped due to error: ${String(policyResult.error?.message ?? policyResult.error)}`,
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
} catch (error) {
|
|
479
488
|
log.warn?.(
|
|
480
|
-
`[memos-cloud]
|
|
489
|
+
`[memos-cloud] failed to ensure plugin hook policy: ${String(error?.message ?? error)}`,
|
|
481
490
|
);
|
|
482
491
|
}
|
|
483
|
-
} catch (error) {
|
|
484
|
-
log.warn?.(
|
|
485
|
-
`[memos-cloud] failed to ensure plugin hook policy: ${String(error?.message ?? error)}`,
|
|
486
|
-
);
|
|
487
492
|
}
|
|
488
|
-
}
|
|
489
493
|
|
|
490
|
-
void (async () => {
|
|
491
|
-
const ready = await waitForGatewayReady(api.config, log);
|
|
492
|
-
if (!ready || configUiStartupCancelled) return;
|
|
493
494
|
await ensureConfigUiService(log);
|
|
494
495
|
})().catch((error) => {
|
|
495
496
|
log.warn?.(`[memos-cloud] config UI failed to start: ${String(error)}`);
|
package/lib/arms-reporter.js
CHANGED
|
@@ -1,116 +1,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_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_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
|
+
}
|
package/lib/check-update.js
CHANGED
|
@@ -1,187 +1,187 @@
|
|
|
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
|
-
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
|
-
function getLatestVersion(log) {
|
|
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
|
-
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
|
-
export function startUpdateChecker(log) {
|
|
105
|
-
// Only start the interval if we are in the gateway
|
|
106
|
-
const isGateway = process.argv.includes("gateway");
|
|
107
|
-
if (!isGateway) {
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const runCheck = async () => {
|
|
112
|
-
// TRULY PREVENT LOOPS: The instant we start a check, record the time BEFORE any network or processing happens.
|
|
113
|
-
// This absolutely guarantees that even if the network hangs, NPM crashes, or openclaw update causes an immediate hot reload,
|
|
114
|
-
// the system has already advanced the 12-hour/1-min clock and will NOT re-enter this function on boot.
|
|
115
|
-
try {
|
|
116
|
-
fs.writeFileSync(CHECK_FILE, JSON.stringify({ time: Date.now() }));
|
|
117
|
-
} catch (e) {
|
|
118
|
-
log.warn?.(`${ANSI.RED}[memos-cloud] Failed to write timestamp file: ${e.message}${ANSI.RESET}`);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const currentVersion = getPackageVersion();
|
|
122
|
-
if (!currentVersion) {
|
|
123
|
-
log.warn?.(`${ANSI.RED}[memos-cloud] Could not read current version from package.json${ANSI.RESET}`);
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
try {
|
|
128
|
-
const latestVersion = await getLatestVersion(log);
|
|
129
|
-
|
|
130
|
-
// Normal version check
|
|
131
|
-
if (compareVersions(latestVersion, currentVersion) <= 0) {
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const cliName = (() => {
|
|
136
|
-
// Check the full path of the entry script (e.g., .../moltbot/bin/index.js) or the executable
|
|
137
|
-
const scriptPath = process.argv[1] ? process.argv[1].toLowerCase() : "";
|
|
138
|
-
const execPath = process.execPath ? process.execPath.toLowerCase() : "";
|
|
139
|
-
|
|
140
|
-
if (scriptPath.includes("moltbot") || execPath.includes("moltbot")) return "moltbot";
|
|
141
|
-
if (scriptPath.includes("clawdbot") || execPath.includes("clawdbot")) return "clawdbot";
|
|
142
|
-
return "openclaw";
|
|
143
|
-
})();
|
|
144
|
-
|
|
145
|
-
const border = "=".repeat(64);
|
|
146
|
-
log.info?.("");
|
|
147
|
-
log.info?.(`${ANSI.GREEN}${border}${ANSI.RESET}`);
|
|
148
|
-
log.info?.(`${ANSI.YELLOW}🚀 [memos-cloud] NEW VERSION AVAILABLE!${ANSI.RESET}`);
|
|
149
|
-
log.info?.(`${ANSI.CYAN}📦 Current version : ${currentVersion}${ANSI.RESET}`);
|
|
150
|
-
log.info?.(`${ANSI.GREEN}✨ Latest version : ${latestVersion}${ANSI.RESET}`);
|
|
151
|
-
log.info?.(`${ANSI.CYAN}────────────────────────────────────────────────────────────────${ANSI.RESET}`);
|
|
152
|
-
log.info?.(`${ANSI.GREEN}Please run the following command to update manually:${ANSI.RESET}`);
|
|
153
|
-
log.info?.(`${ANSI.YELLOW}${cliName} plugins update memos-cloud-openclaw-plugin${ANSI.RESET}`);
|
|
154
|
-
log.info?.(`${ANSI.GREEN}${border}${ANSI.RESET}`);
|
|
155
|
-
log.info?.("");
|
|
156
|
-
|
|
157
|
-
} catch (error) {
|
|
158
|
-
log.warn?.(`${ANSI.RED}[memos-cloud] Update check failed entirely: ${error.message}${ANSI.RESET}`);
|
|
159
|
-
}
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
// Check when we last ran
|
|
163
|
-
let lastCheckTime = 0;
|
|
164
|
-
try {
|
|
165
|
-
if (fs.existsSync(CHECK_FILE)) {
|
|
166
|
-
const data = JSON.parse(fs.readFileSync(CHECK_FILE, "utf-8"));
|
|
167
|
-
lastCheckTime = data.time || 0;
|
|
168
|
-
}
|
|
169
|
-
} catch (e) {}
|
|
170
|
-
|
|
171
|
-
const now = Date.now();
|
|
172
|
-
const timeSinceLastCheck = now - lastCheckTime;
|
|
173
|
-
|
|
174
|
-
// If the interval has passed, run it IMMEDIATELY without delay.
|
|
175
|
-
// The immediate file-write at the top of runCheck() will prevent loop scenarios.
|
|
176
|
-
if (timeSinceLastCheck >= CHECK_INTERVAL) {
|
|
177
|
-
runCheck();
|
|
178
|
-
setInterval(runCheck, CHECK_INTERVAL);
|
|
179
|
-
} else {
|
|
180
|
-
// If it hasn't been the full interval yet, wait the remaining time, then trigger interval
|
|
181
|
-
const timeUntilNextCheck = CHECK_INTERVAL - timeSinceLastCheck;
|
|
182
|
-
setTimeout(() => {
|
|
183
|
-
runCheck();
|
|
184
|
-
setInterval(runCheck, CHECK_INTERVAL);
|
|
185
|
-
}, timeUntilNextCheck);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
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
|
+
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
|
+
function getLatestVersion(log) {
|
|
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
|
+
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
|
+
export function startUpdateChecker(log) {
|
|
105
|
+
// Only start the interval if we are in the gateway
|
|
106
|
+
const isGateway = process.argv.includes("gateway");
|
|
107
|
+
if (!isGateway) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const runCheck = async () => {
|
|
112
|
+
// TRULY PREVENT LOOPS: The instant we start a check, record the time BEFORE any network or processing happens.
|
|
113
|
+
// This absolutely guarantees that even if the network hangs, NPM crashes, or openclaw update causes an immediate hot reload,
|
|
114
|
+
// the system has already advanced the 12-hour/1-min clock and will NOT re-enter this function on boot.
|
|
115
|
+
try {
|
|
116
|
+
fs.writeFileSync(CHECK_FILE, JSON.stringify({ time: Date.now() }));
|
|
117
|
+
} catch (e) {
|
|
118
|
+
log.warn?.(`${ANSI.RED}[memos-cloud] Failed to write timestamp file: ${e.message}${ANSI.RESET}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const currentVersion = getPackageVersion();
|
|
122
|
+
if (!currentVersion) {
|
|
123
|
+
log.warn?.(`${ANSI.RED}[memos-cloud] Could not read current version from package.json${ANSI.RESET}`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const latestVersion = await getLatestVersion(log);
|
|
129
|
+
|
|
130
|
+
// Normal version check
|
|
131
|
+
if (compareVersions(latestVersion, currentVersion) <= 0) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const cliName = (() => {
|
|
136
|
+
// Check the full path of the entry script (e.g., .../moltbot/bin/index.js) or the executable
|
|
137
|
+
const scriptPath = process.argv[1] ? process.argv[1].toLowerCase() : "";
|
|
138
|
+
const execPath = process.execPath ? process.execPath.toLowerCase() : "";
|
|
139
|
+
|
|
140
|
+
if (scriptPath.includes("moltbot") || execPath.includes("moltbot")) return "moltbot";
|
|
141
|
+
if (scriptPath.includes("clawdbot") || execPath.includes("clawdbot")) return "clawdbot";
|
|
142
|
+
return "openclaw";
|
|
143
|
+
})();
|
|
144
|
+
|
|
145
|
+
const border = "=".repeat(64);
|
|
146
|
+
log.info?.("");
|
|
147
|
+
log.info?.(`${ANSI.GREEN}${border}${ANSI.RESET}`);
|
|
148
|
+
log.info?.(`${ANSI.YELLOW}🚀 [memos-cloud] NEW VERSION AVAILABLE!${ANSI.RESET}`);
|
|
149
|
+
log.info?.(`${ANSI.CYAN}📦 Current version : ${currentVersion}${ANSI.RESET}`);
|
|
150
|
+
log.info?.(`${ANSI.GREEN}✨ Latest version : ${latestVersion}${ANSI.RESET}`);
|
|
151
|
+
log.info?.(`${ANSI.CYAN}────────────────────────────────────────────────────────────────${ANSI.RESET}`);
|
|
152
|
+
log.info?.(`${ANSI.GREEN}Please run the following command to update manually:${ANSI.RESET}`);
|
|
153
|
+
log.info?.(`${ANSI.YELLOW}${cliName} plugins update memos-cloud-openclaw-plugin${ANSI.RESET}`);
|
|
154
|
+
log.info?.(`${ANSI.GREEN}${border}${ANSI.RESET}`);
|
|
155
|
+
log.info?.("");
|
|
156
|
+
|
|
157
|
+
} catch (error) {
|
|
158
|
+
log.warn?.(`${ANSI.RED}[memos-cloud] Update check failed entirely: ${error.message}${ANSI.RESET}`);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Check when we last ran
|
|
163
|
+
let lastCheckTime = 0;
|
|
164
|
+
try {
|
|
165
|
+
if (fs.existsSync(CHECK_FILE)) {
|
|
166
|
+
const data = JSON.parse(fs.readFileSync(CHECK_FILE, "utf-8"));
|
|
167
|
+
lastCheckTime = data.time || 0;
|
|
168
|
+
}
|
|
169
|
+
} catch (e) {}
|
|
170
|
+
|
|
171
|
+
const now = Date.now();
|
|
172
|
+
const timeSinceLastCheck = now - lastCheckTime;
|
|
173
|
+
|
|
174
|
+
// If the interval has passed, run it IMMEDIATELY without delay.
|
|
175
|
+
// The immediate file-write at the top of runCheck() will prevent loop scenarios.
|
|
176
|
+
if (timeSinceLastCheck >= CHECK_INTERVAL) {
|
|
177
|
+
runCheck();
|
|
178
|
+
setInterval(runCheck, CHECK_INTERVAL);
|
|
179
|
+
} else {
|
|
180
|
+
// If it hasn't been the full interval yet, wait the remaining time, then trigger interval
|
|
181
|
+
const timeUntilNextCheck = CHECK_INTERVAL - timeSinceLastCheck;
|
|
182
|
+
setTimeout(() => {
|
|
183
|
+
runCheck();
|
|
184
|
+
setInterval(runCheck, CHECK_INTERVAL);
|
|
185
|
+
}, timeUntilNextCheck);
|
|
186
|
+
}
|
|
187
|
+
}
|