Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
72658b2
Adapt memos-local memory integration
hijzy Apr 8, 2026
ee33f59
Adapt memos-local memory integration (#1436)
hijzy Apr 8, 2026
25ffcd7
release: openclaw-plugin v1.0.9-beta.1
github-actions[bot] Apr 8, 2026
4c14cd1
feat: add memos-local-hermes-plugin with one-click installer
hijzy Apr 9, 2026
94e4684
feat: add memos-local-hermes-plugin with one-click installer (#1441)
hijzy Apr 9, 2026
bcac12c
fix: update install URL to actual GitHub raw path
hijzy Apr 9, 2026
2f52dcc
ci: add Hermes plugin npm publish workflow and update package.json
Apr 9, 2026
55b1abe
ci: add Hermes plugin npm publish workflow (#1442)
syzsunshine219 Apr 9, 2026
f02846a
release: hermes-plugin v1.0.0-beta.1
github-actions[bot] Apr 9, 2026
0e9213a
fix: install.sh auto-resolve latest npm version, support --version flag
hijzy Apr 9, 2026
5c5f561
Merge remote-tracking branch 'upstream/openclaw-local-plugin-20260408…
hijzy Apr 9, 2026
cd7ea9c
fix: install.sh auto-resolve npm version & update download URL (#1444)
hijzy Apr 9, 2026
a344047
docs: add README for @memtensor/memos-local-hermes-plugin
Apr 9, 2026
756f373
docs: add README for @memtensor/memos-local-hermes-plugin (#1445)
syzsunshine219 Apr 9, 2026
cd1c514
release: hermes-plugin v1.0.1-beta.1
github-actions[bot] Apr 9, 2026
8a6ae55
release: hermes-plugin v1.0.0
github-actions[bot] Apr 9, 2026
6a3900b
release: hermes-plugin v1.0.1
github-actions[bot] Apr 9, 2026
7111ffb
fix: rebrand viewer from OpenClaw to MemTensor & fix embedding warning
hijzy Apr 9, 2026
30f25a4
fix: defer ingest until after prefetch search to fix memory_add/searc…
hijzy Apr 9, 2026
b2d71b4
fix: filter trivial content from ingest & rebrand OpenClaw references
hijzy Apr 9, 2026
42e1e38
fix: rebrand viewer, fix embedding warning, fix memory_add/search ord…
hijzy Apr 9, 2026
b63b45e
release: hermes-plugin v1.0.2
github-actions[bot] Apr 9, 2026
b84f2aa
feat: auto-install Node.js when missing in hermes install script
hijzy Apr 9, 2026
abfe2f4
feat: auto-install Node.js when missing in hermes install script (#1449)
hijzy Apr 9, 2026
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
113 changes: 113 additions & 0 deletions .github/workflows/hermes-plugin-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
name: Hermes Plugin — Build Prebuilds & Publish

on:
workflow_dispatch:
inputs:
version:
description: "Version to publish (e.g. 1.0.0 or 1.0.0-beta.1)"
required: true
tag:
description: "npm dist-tag (latest for production, beta/next/alpha for testing)"
required: true
default: "latest"

defaults:
run:
working-directory: apps/memos-local-plugin

permissions:
contents: write

jobs:
build-prebuilds:
strategy:
matrix:
include:
- os: macos-14
platform: darwin-arm64
- os: macos-15
platform: darwin-x64
- os: ubuntu-latest
platform: linux-x64
- os: windows-latest
platform: win32-x64
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22

- name: Install dependencies
run: npm install

- name: Rebuild for x64 under Rosetta (darwin-x64 only)
if: matrix.platform == 'darwin-x64'
run: |
arch -x86_64 npm rebuild better-sqlite3

- name: Collect prebuild
shell: bash
run: |
mkdir -p prebuilds/${{ matrix.platform }}
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node prebuilds/${{ matrix.platform }}/

- name: Upload prebuild artifact
uses: actions/upload-artifact@v4
with:
name: prebuild-hermes-${{ matrix.platform }}
path: apps/memos-local-plugin/prebuilds/${{ matrix.platform }}/better_sqlite3.node

publish:
needs: build-prebuilds
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://registry.npmjs.org

- name: Download all prebuilds
uses: actions/download-artifact@v4
with:
path: apps/memos-local-plugin/prebuilds
pattern: prebuild-hermes-*
merge-multiple: false

- name: Organize prebuilds
run: |
cd prebuilds
for dir in prebuild-hermes-*; do
platform="${dir#prebuild-hermes-}"
mkdir -p "$platform"
mv "$dir/better_sqlite3.node" "$platform/"
rmdir "$dir"
done
echo "Prebuilds collected:"
find . -name "*.node" -exec ls -lh {} \;

- name: Install dependencies (skip native build)
run: npm install --ignore-scripts

- name: Bump version
run: npm version ${{ inputs.version }} --no-git-tag-version --allow-same-version

- name: Publish to npm
run: npm publish --access public --tag ${{ inputs.tag }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Create git tag and push
working-directory: .
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add apps/memos-local-plugin/package.json
if ! git diff --staged --quiet; then
git commit -m "release: hermes-plugin v${{ inputs.version }}"
fi
git tag "hermes-plugin-v${{ inputs.version }}"
git push origin HEAD --tags
47 changes: 45 additions & 2 deletions apps/memos-local-openclaw/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,43 @@ const NEW_SESSION_PROMPT_RE = /A new session was started via \/new or \/reset\./
const INTERNAL_CONTEXT_RE = /OpenClaw runtime context \(internal\):[\s\S]*/i;
const CONTINUE_PROMPT_RE = /^Continue where you left off\.[\s\S]*/i;

const buildMemoryPromptSection = ({ availableTools, citationsMode }: {
availableTools: Set<string>;
citationsMode?: string;
}) => {
const lines: string[] = [];
const hasMemorySearch = availableTools.has("memory_search");
const hasMemoryGet = availableTools.has("memory_get");

if (!hasMemorySearch && !hasMemoryGet) {
return lines;
}

lines.push("## Memory Recall");
lines.push(
"This workspace uses MemOS Local as the active memory slot. Prefer recalled memories and the memory tools before claiming prior context is unavailable.",
);

if (hasMemorySearch && hasMemoryGet) {
lines.push(
"Use `memory_search` to locate relevant memories, then `memory_get` or `memory_timeline` when you need the full source text or surrounding context.",
);
} else if (hasMemorySearch) {
lines.push("Use `memory_search` before answering questions about prior conversations, preferences, plans, or decisions.");
} else {
lines.push("Use `memory_get` or `memory_timeline` to inspect the referenced memory before answering.");
}

if (citationsMode === "off") {
lines.push("Citations are disabled, so avoid mentioning internal memory ids unless the user asks.");
} else {
lines.push("When it helps the user verify a memory-backed claim, mention the relevant memory identifier or tool result.");
}

lines.push("");
return lines;
};

function normalizeAutoRecallQuery(rawPrompt: string): string {
let query = rawPrompt.trim();

Expand Down Expand Up @@ -123,6 +160,10 @@ const memosLocalPlugin = {
configSchema: pluginConfigSchema,

register(api: OpenClawPluginApi) {
api.registerMemoryCapability({
promptBuilder: buildMemoryPromptSection,
});

const moduleDir = path.dirname(fileURLToPath(import.meta.url));
const localRequire = createRequire(import.meta.url);
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
Expand Down Expand Up @@ -2399,8 +2440,10 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,

// Fallback: OpenClaw may load this plugin via deferred reload after
// startPluginServices has already run, so service.start() never fires.
// Self-start the viewer after a grace period if it hasn't been started.
const SELF_START_DELAY_MS = 3000;
// Start on the next tick instead of waiting several seconds; the
// serviceStarted guard still prevents duplicate startup if the host calls
// service.start() immediately after registration.
const SELF_START_DELAY_MS = 0;
setTimeout(() => {
if (!serviceStarted) {
api.logger.info("memos-local: service.start() not called by host, self-starting viewer...");
Expand Down
40 changes: 35 additions & 5 deletions apps/memos-local-openclaw/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,17 @@ ensure_node22() {
exit 1
}

resolve_openclaw_bin() {
if command -v openclaw >/dev/null 2>&1; then
command -v openclaw
return 0
fi

error "Global openclaw CLI not found, 未找到全局 openclaw 命令"
error "Install it first with: npm install -g openclaw@latest"
exit 1
}

print_banner() {
echo -e "${BLUE}${BOLD}🧠 Memos Local OpenClaw Installer${NC}"
echo -e "${BLUE}${DEFAULT_TAGLINE}${NC}"
Expand Down Expand Up @@ -208,6 +219,9 @@ if ! command -v node >/dev/null 2>&1; then
exit 1
fi

OPENCLAW_BIN="$(resolve_openclaw_bin)"
success "Using global OpenClaw CLI, 使用全局 OpenClaw CLI: ${OPENCLAW_BIN}"

PACKAGE_SPEC="${PLUGIN_PACKAGE}@${PLUGIN_VERSION}"
EXTENSION_DIR="${OPENCLAW_HOME}/extensions/${PLUGIN_ID}"
OPENCLAW_CONFIG_PATH="${OPENCLAW_HOME}/openclaw.json"
Expand Down Expand Up @@ -302,7 +316,7 @@ NODE
}

info "Stop OpenClaw Gateway, 停止 OpenClaw Gateway..."
npx openclaw gateway stop >/dev/null 2>&1 || true
"${OPENCLAW_BIN}" gateway stop >/dev/null 2>&1 || true

if command -v lsof >/dev/null 2>&1; then
PIDS="$(lsof -i :"${PORT}" -t 2>/dev/null || true)"
Expand Down Expand Up @@ -388,21 +402,37 @@ fi
update_openclaw_config

info "Install OpenClaw Gateway service, 安装 OpenClaw Gateway 服务..."
npx openclaw gateway install --port "${PORT}" --force 2>&1 || true
"${OPENCLAW_BIN}" gateway install --port "${PORT}" --force 2>&1 || true

success "Start OpenClaw Gateway service, 启动 OpenClaw Gateway 服务..."
npx openclaw gateway start 2>&1
"${OPENCLAW_BIN}" gateway start 2>&1

info "Starting Memory Viewer, 正在启动记忆面板..."
for i in 1 2 3 4 5; do
if command -v lsof >/dev/null 2>&1 && lsof -i :18799 -t >/dev/null 2>&1; then
VIEWER_URL="http://127.0.0.1:18799"
VIEWER_WAIT_SECONDS=30
viewer_ready=0
for ((i=1; i<=VIEWER_WAIT_SECONDS; i++)); do
if command -v curl >/dev/null 2>&1; then
if curl -fsS --max-time 2 "${VIEWER_URL}" >/dev/null 2>&1; then
viewer_ready=1
break
fi
elif command -v lsof >/dev/null 2>&1 && lsof -i :18799 -t >/dev/null 2>&1; then
viewer_ready=1
break
fi
printf "."
sleep 1
done
echo ""

if [[ "${viewer_ready}" -eq 1 ]]; then
success "Memory Viewer is ready, 记忆面板已就绪: ${VIEWER_URL}"
else
warn "Memory Viewer not ready after ${VIEWER_WAIT_SECONDS}s, 记忆面板在 ${VIEWER_WAIT_SECONDS} 秒后仍未就绪"
warn "Check gateway logs if http://127.0.0.1:18799 is still unavailable."
fi

echo ""
success "=========================================="
success " Installation complete! 安装完成!"
Expand Down
2 changes: 1 addition & 1 deletion apps/memos-local-openclaw/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@memtensor/memos-local-openclaw-plugin",
"version": "1.0.8",
"version": "1.0.9-beta.1",
"description": "MemOS Local memory plugin for OpenClaw — full-write, hybrid-recall, progressive retrieval",
"type": "module",
"main": "index.ts",
Expand Down
14 changes: 13 additions & 1 deletion apps/memos-local-openclaw/tests/incremental-sharing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,21 @@ function makeApi(stateDir: string, pluginConfig: Record<string, unknown> = {}) {
return input === "~/.openclaw" ? stateDir : input;
},
logger: noopLog,
registerTool(def: any) {
registerTool(def: any, meta?: { name?: string }) {
if (typeof def === "function") {
const key = meta?.name ?? def({ agentId: "main", sessionKey: "default" }).name;
tools.set(key, {
name: key,
execute: (...args: any[]) => {
const runtimeCtx = args[2] ?? { agentId: "main", sessionKey: "default" };
return def(runtimeCtx).execute(...args);
},
});
return;
}
tools.set(def.name, def);
},
registerMemoryCapability() {},
registerService(def: any) {
service = def;
},
Expand Down
20 changes: 16 additions & 4 deletions apps/memos-local-openclaw/tests/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,21 @@ function makePluginApi(stateDir: string, pluginConfig: Record<string, unknown> =
return input === "~/.openclaw" ? stateDir : input;
},
logger: noopLog,
registerTool(def: any) {
registerTool(def: any, meta?: { name?: string }) {
if (typeof def === "function") {
const key = meta?.name ?? def({ agentId: "main", sessionKey: "default" }).name;
tools.set(key, {
name: key,
execute: (...args: any[]) => {
const runtimeCtx = args[2] ?? { agentId: "main", sessionKey: "default" };
return def(runtimeCtx).execute(...args);
},
});
return;
}
tools.set(def.name, def);
},
registerMemoryCapability() {},
registerService(def: any) {
service = def;
},
Expand Down Expand Up @@ -837,9 +849,9 @@ describe("Integration: root plugin memory_search network scope", () => {
expect(searchTool).toBeDefined();

const result = await searchTool.execute("call-root-search", { query: "rollout checklist", scope: "all", maxResults: 5 }, { agentId: "main" });
expect(result.details.local.hits.length).toBeGreaterThan(0);
expect(result.details.hub.hits.length).toBeGreaterThan(0);
expect(result.details.hub.hits[0].remoteHitId).toBeTruthy();
expect((result.details.filtered ?? []).some((hit: any) => hit.origin !== "hub-remote")).toBe(true);
expect((result.details.filtered ?? []).some((hit: any) => hit.origin === "hub-remote")).toBe(true);
expect((result.details.hubCandidates ?? []).length).toBeGreaterThan(0);
} finally {
await teardownRootMemorySearchHarness(harness);
}
Expand Down
26 changes: 19 additions & 7 deletions apps/memos-local-openclaw/tests/plugin-impl-access.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,21 @@ function makeApi(stateDir: string, pluginConfig: Record<string, unknown> = {}) {
info: () => {},
warn: () => {},
},
registerTool(def: any) {
registerTool(def: any, meta?: { name?: string }) {
if (typeof def === "function") {
const key = meta?.name ?? def({ agentId: "main", sessionKey: "default" }).name;
tools.set(key, {
name: key,
execute: (...args: any[]) => {
const runtimeCtx = args[2] ?? { agentId: "main", sessionKey: "default" };
return def(runtimeCtx).execute(...args);
},
});
return;
}
tools.set(def.name, def);
},
registerMemoryCapability() {},
registerService(def: any) {
service = def;
},
Expand Down Expand Up @@ -219,7 +231,7 @@ describe("plugin-impl owner isolation", () => {
const search = tools.get("memory_search");
await waitFor(async () => {
const result = await search.execute("call-search", { query: "alpha private marker", maxResults: 5, minScore: 0.1 }, { agentId: "alpha" });
return (result?.details?.hits?.length ?? 0) > 0;
return (result?.details?.filtered?.length ?? 0) > 0;
});
});

Expand All @@ -243,20 +255,20 @@ describe("plugin-impl owner isolation", () => {
const beta = await search.execute("call-search", { query: "alpha private marker", maxResults: 5, minScore: 0.1 }, { agentId: "beta" });
const publicHit = await search.execute("call-search", { query: "shared public marker", maxResults: 5, minScore: 0.1 }, { agentId: "beta" });

expect(alpha.details.hits.length).toBeGreaterThan(0);
const betaAlphaHits = (beta.details?.hits ?? []).filter((h: any) =>
expect(alpha.details.filtered.length).toBeGreaterThan(0);
const betaAlphaHits = (beta.details?.filtered ?? []).filter((h: any) =>
h.original_excerpt?.includes("alpha") || h.summary?.includes("alpha"),
);
expect(betaAlphaHits).toHaveLength(0);
expect(publicHit.details.hits.length).toBeGreaterThan(0);
expect(publicHit.details.filtered.length).toBeGreaterThan(0);
});

it("memory_timeline should not leak another agent's private neighbors", async () => {
const search = tools.get("memory_search");
const timeline = tools.get("memory_timeline");

const alpha = await search.execute("call-search", { query: "alpha private marker", maxResults: 5, minScore: 0.1 }, { agentId: "alpha" });
const chunkId = alpha.details.hits[0].chunkId;
const chunkId = alpha.details.filtered[0].chunkId;
const betaTimeline = await timeline.execute("call-timeline", { chunkId }, { agentId: "beta" });

expect(betaTimeline.details.entries).toEqual([]);
Expand Down Expand Up @@ -416,7 +428,7 @@ describe("plugin-impl owner isolation", () => {
const getTool = tools.get("memory_get");

const alpha = await search.execute("call-search", { query: "alpha private marker", maxResults: 5, minScore: 0.1 }, { agentId: "alpha" });
const chunkId = alpha.details.hits[0].chunkId;
const chunkId = alpha.details.filtered[0].chunkId;
const betaGet = await getTool.execute("call-get", { chunkId }, { agentId: "beta" });

expect(betaGet.details.error).toBe("not_found");
Expand Down
Loading
Loading