Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion clawdbot.plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"id": "memos-cloud-openclaw-plugin",
"name": "MemOS Cloud OpenClaw Plugin",
"description": "MemOS Cloud recall + add memory via lifecycle hooks",
"version": "0.1.17",
"version": "0.1.18",
"kind": "lifecycle",
"main": "./index.js",
"activation": {
Expand Down
63 changes: 39 additions & 24 deletions lib/check-update.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const ANSI = {
};


function getPackageVersion() {
export function getPackageVersion() {
try {
const pkgPath = path.join(__dirname, "..", "package.json");
const pkgData = fs.readFileSync(pkgPath, "utf-8");
Expand All @@ -30,7 +30,7 @@ function getPackageVersion() {
}
}

function getLatestVersion(log) {
export function getLatestVersion() {
return new Promise((resolve, reject) => {
const req = https.get(
`https://registry.npmjs.org/${PLUGIN_NAME}/latest`,
Expand Down Expand Up @@ -68,7 +68,7 @@ function getLatestVersion(log) {
});
}

function compareVersions(v1, v2) {
export function compareVersions(v1, v2) {
// Split pre-release tags (e.g. 0.1.8-beta.1 -> "0.1.8" and "beta.1")
const split1 = v1.split("-");
const split2 = v2.split("-");
Expand Down Expand Up @@ -101,6 +101,37 @@ function compareVersions(v1, v2) {
return 0;
}

function detectCliName() {
// Check the full path of the entry script (e.g., .../moltbot/bin/index.js) or the executable
const scriptPath = process.argv[1] ? process.argv[1].toLowerCase() : "";
const execPath = process.execPath ? process.execPath.toLowerCase() : "";

if (scriptPath.includes("moltbot") || execPath.includes("moltbot")) return "moltbot";
if (scriptPath.includes("clawdbot") || execPath.includes("clawdbot")) return "clawdbot";
return "openclaw";
}

export async function checkForPluginUpdate() {
const currentVersion = getPackageVersion();
if (!currentVersion) {
throw new Error("Could not read current version from package.json");
}

const latestVersion = await getLatestVersion();
const updateAvailable = compareVersions(latestVersion, currentVersion) > 0;
const cliName = detectCliName();

return {
pluginName: PLUGIN_NAME,
currentVersion,
latestVersion,
updateAvailable,
cliName,
updateCommand: `${cliName} plugins update memos-cloud-openclaw-plugin`,
checkedAt: new Date().toISOString(),
};
}

export function startUpdateChecker(log) {
// Only start the interval if we are in the gateway
const isGateway = process.argv.includes("gateway");
Expand All @@ -118,39 +149,23 @@ export function startUpdateChecker(log) {
log.warn?.(`${ANSI.RED}[memos-cloud] Failed to write timestamp file: ${e.message}${ANSI.RESET}`);
}

const currentVersion = getPackageVersion();
if (!currentVersion) {
log.warn?.(`${ANSI.RED}[memos-cloud] Could not read current version from package.json${ANSI.RESET}`);
return;
}

try {
const latestVersion = await getLatestVersion(log);
const updateStatus = await checkForPluginUpdate();

// Normal version check
if (compareVersions(latestVersion, currentVersion) <= 0) {
if (!updateStatus.updateAvailable) {
return;
}

const cliName = (() => {
// Check the full path of the entry script (e.g., .../moltbot/bin/index.js) or the executable
const scriptPath = process.argv[1] ? process.argv[1].toLowerCase() : "";
const execPath = process.execPath ? process.execPath.toLowerCase() : "";

if (scriptPath.includes("moltbot") || execPath.includes("moltbot")) return "moltbot";
if (scriptPath.includes("clawdbot") || execPath.includes("clawdbot")) return "clawdbot";
return "openclaw";
})();

const border = "=".repeat(64);
log.info?.("");
log.info?.(`${ANSI.GREEN}${border}${ANSI.RESET}`);
log.info?.(`${ANSI.YELLOW}🚀 [memos-cloud] NEW VERSION AVAILABLE!${ANSI.RESET}`);
log.info?.(`${ANSI.CYAN}📦 Current version : ${currentVersion}${ANSI.RESET}`);
log.info?.(`${ANSI.GREEN}✨ Latest version : ${latestVersion}${ANSI.RESET}`);
log.info?.(`${ANSI.CYAN}📦 Current version : ${updateStatus.currentVersion}${ANSI.RESET}`);
log.info?.(`${ANSI.GREEN}✨ Latest version : ${updateStatus.latestVersion}${ANSI.RESET}`);
log.info?.(`${ANSI.CYAN}────────────────────────────────────────────────────────────────${ANSI.RESET}`);
log.info?.(`${ANSI.GREEN}Please run the following command to update manually:${ANSI.RESET}`);
log.info?.(`${ANSI.YELLOW}${cliName} plugins update memos-cloud-openclaw-plugin${ANSI.RESET}`);
log.info?.(`${ANSI.YELLOW}${updateStatus.updateCommand}${ANSI.RESET}`);
log.info?.(`${ANSI.GREEN}${border}${ANSI.RESET}`);
log.info?.("");

Expand Down
90 changes: 71 additions & 19 deletions lib/config-ui-server.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@

import { createHash, randomBytes } from "node:crypto";
import { mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
import { createHash, randomBytes } from "node:crypto";
import { mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
import { createServer } from "node:http";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { Script } from "node:vm";
import { getConfigResolution } from "./memos-cloud-api.js";
import { Script } from "node:vm";
import { checkForPluginUpdate, getPackageVersion } from "./check-update.js";
import { getConfigResolution } from "./memos-cloud-api.js";

const __dirname = dirname(fileURLToPath(import.meta.url));

Expand All @@ -21,7 +22,8 @@ const ANSI_CYAN = "\x1b[36m";
const ANSI_GREEN = "\x1b[32m";
const ANSI_YELLOW = "\x1b[33m";
const ANSI_RESET = "\x1b[0m";
const DEFAULT_GATEWAY_READY_PORT = 18789;
const DEFAULT_GATEWAY_READY_PORT = 18789;
const UPDATE_STATUS_CACHE_MS = 30 * 60 * 1000;

// Hook policies that the gateway requires non-bundled plugins to opt into.
// Without these, the gateway blocks `agent_end` (and other conversation-access
Expand Down Expand Up @@ -517,9 +519,10 @@ function buildStatePayload(service) {
const state = readGatewayConfig(service.profile);
const resolution = getConfigResolution(state.config);
return {
runtime: state.profile.id,
runtimeDisplayName: state.profile.displayName,
configPath: state.configPath,
runtime: state.profile.id,
runtimeDisplayName: state.profile.displayName,
pluginVersion: getPackageVersion(),
configPath: state.configPath,
entryExists: state.entryExists,
fileExists: state.fileExists,
enabled: state.enabled,
Expand Down Expand Up @@ -548,7 +551,7 @@ function getCachedStatePayload(service, maxAgeMs = 1200) {
return payload;
}

function getCachedHeartbeatPayload(service, maxAgeMs = 1200) {
function getCachedHeartbeatPayload(service, maxAgeMs = 1200) {
const now = Date.now();
if (service.heartbeatCache && now - service.heartbeatCache.createdAt < maxAgeMs) {
return {
Expand All @@ -570,9 +573,41 @@ function getCachedHeartbeatPayload(service, maxAgeMs = 1200) {
...payload,
timestamp: now,
};
}

function loadAssetTemplate(name) {
}

async function getCachedUpdateStatusPayload(service, maxAgeMs = UPDATE_STATUS_CACHE_MS, options = {}) {
const now = Date.now();
if (!options.force && service.updateStatusCache && now - service.updateStatusCache.createdAt < maxAgeMs) {
return {
...service.updateStatusCache.payload,
cached: true,
};
}

try {
const status = await checkForPluginUpdate();
const payload = {
ok: true,
...status,
cached: false,
};
service.updateStatusCache = {
createdAt: now,
payload,
};
return payload;
} catch (error) {
return {
ok: false,
currentVersion: getPackageVersion(),
error: String(error?.message || error),
checkedAt: new Date().toISOString(),
cached: false,
};
}
}

function loadAssetTemplate(name) {
return readFileSync(join(ASSET_DIR, name), "utf8");
}

Expand Down Expand Up @@ -827,7 +862,17 @@ async function createService(log) {
const token = randomBytes(24).toString("hex");
const bootId = randomBytes(10).toString("hex");

const service = { profile, token, bootId, port: 0, url: "", server: null, stateCache: null, heartbeatCache: null };
const service = {
profile,
token,
bootId,
port: 0,
url: "",
server: null,
stateCache: null,
heartbeatCache: null,
updateStatusCache: null,
};

const server = createServer(async (req, res) => {
try {
Expand Down Expand Up @@ -872,12 +917,19 @@ async function createService(log) {
return;
}

if (requestUrl.pathname === "/api/state" && req.method === "GET") {
sendJson(res, 200, getCachedStatePayload(service));
return;
}

if (requestUrl.pathname === "/api/save" && req.method === "POST") {
if (requestUrl.pathname === "/api/state" && req.method === "GET") {
sendJson(res, 200, getCachedStatePayload(service));
return;
}

if (requestUrl.pathname === "/api/update-status" && req.method === "GET") {
sendJson(res, 200, await getCachedUpdateStatusPayload(service, UPDATE_STATUS_CACHE_MS, {
force: requestUrl.searchParams.get("force") === "1",
}));
return;
}

if (requestUrl.pathname === "/api/save" && req.method === "POST") {
let parsed = {};
try {
parsed = JSON.parse((await readRequestBody(req)) || "{}");
Expand Down
102 changes: 102 additions & 0 deletions lib/config-ui/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,99 @@ h1 {
color: var(--warn);
}

.update-notice {
display: none;
margin-top: 16px;
padding: 14px 16px;
border: 1px solid rgba(181, 71, 8, 0.2);
border-radius: 18px;
background: rgba(181, 71, 8, 0.08);
color: var(--warn);
}

.update-notice.show {
display: block;
}

.update-notice.error {
border-color: rgba(180, 35, 24, 0.18);
background: rgba(180, 35, 24, 0.08);
color: var(--danger);
}

.update-notice.ok,
.update-notice.info {
border-color: rgba(15, 118, 110, 0.18);
background: rgba(15, 118, 110, 0.08);
color: var(--accent2);
}

.update-check-button {
appearance: none;
min-height: 38px;
padding: 8px 14px;
border: 1px solid rgba(15, 23, 42, 0.08);
background: rgba(255, 255, 255, 0.74);
color: var(--strong);
font-weight: 700;
cursor: pointer;
transition: border-color 160ms ease, box-shadow 160ms ease, color 160ms ease, transform 160ms ease;
}

.update-check-button:hover {
border-color: rgba(15, 118, 110, 0.2);
color: var(--accent2);
transform: translateY(-1px);
}

.update-check-button:focus {
outline: none;
border-color: rgba(15, 118, 110, 0.34);
box-shadow: 0 0 0 4px rgba(15, 118, 110, 0.12);
}

.update-check-button:disabled {
cursor: wait;
opacity: 0.7;
transform: none;
}

.update-copy {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 14px;
}

.update-title {
margin-bottom: 4px;
color: var(--strong);
font-weight: 800;
}

.update-body {
font-size: 13px;
line-height: 1.55;
}

.update-command {
display: block;
margin-top: 12px;
padding: 10px 12px;
border-radius: 14px;
background: rgba(255, 255, 255, 0.72);
color: var(--strong);
font-family: "Cascadia Code", "Consolas", monospace;
font-size: 12px;
line-height: 1.45;
overflow-x: auto;
}

.update-copy-button {
flex: 0 0 auto;
white-space: nowrap;
}

.hero-actions {
display: grid;
gap: 12px;
Expand Down Expand Up @@ -609,6 +702,11 @@ h1 {
cursor: pointer;
}

.inline-btn:disabled {
opacity: 0.58;
cursor: not-allowed;
}

.error {
color: var(--danger);
font-size: 12px;
Expand Down Expand Up @@ -868,6 +966,10 @@ body.nav-collapsed .floating-nav {
padding: 18px;
}

.update-copy {
display: grid;
}

.card,
.panel {
padding: 18px;
Expand Down
Loading