Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
05c3054
feat(memos-local-openclaw): model validation on save, CN providers, s…
tangbotony Mar 9, 2026
1163052
Merge branch 'MemTensor:main' into native_memos
tangbotony Mar 11, 2026
1647b16
feat(memos-local-openclaw): improve summarize quality, topic judge, m…
tangbotony Mar 11, 2026
0091760
fix(memos-local-openclaw): task chunk expand/collapse, model health s…
Mar 12, 2026
d48e5c1
fix(memos-local-openclaw): show plugin version badge (v*) next to log…
Mar 12, 2026
cbfe54b
feat(memos-local-openclaw): v1.0.2-beta.5 — multi-agent isolation, vi…
tangbotony Mar 12, 2026
6b3e959
fix(memos-local-openclaw): channel-aware update check, one-click upda…
Mar 12, 2026
7fbf933
fix auto search bug
tangbotony Mar 12, 2026
c27f065
fix: resolve capture telemetry crash and add multi-provider LLM support
Mar 15, 2026
427a5ac
fix(memos-local-openclaw): Viewer 一键更新后执行 postinstall 且不再吞掉 better-sq…
Mar 16, 2026
4dc76f1
chore(memos-local-openclaw): bump version to 1.0.2-beta.7
Mar 16, 2026
f766453
fix(memos-local-openclaw): resolve 38 TypeScript build errors
Mar 16, 2026
27c9432
chore(memos-local-openclaw): bump version to 1.0.2-beta.8
Mar 16, 2026
6ed9d33
fix(openclaw): sanitize FTS date queries with hyphen
hijzy Mar 16, 2026
2246900
fix(memos-local-openclaw): improve better-sqlite3 bindings error for …
Mar 16, 2026
56bb0c3
chore(memos-local-openclaw): bump version to 1.0.3 (latest)
Mar 16, 2026
616b9c2
Merge branch 'native_memos' into jiang
tangbotony Mar 17, 2026
0eddf48
Merge pull request #7 from tangbotony/jiang
tangbotony Mar 17, 2026
e7584b9
fix(memos-local-openclaw): sanitize hyphen in FTS date query
hijzy Mar 17, 2026
576740b
fix(openclaw): detect Anthropic provider and use correct API format
zerone0x Mar 17, 2026
807248d
fix(memos-local-openclaw): respect OPENCLAW_STATE_DIR in viewer migrate
Mar 17, 2026
1b1641a
fix(memos-local-openclaw): detect Anthropic provider in OpenClaw fall…
Mar 17, 2026
2dcc0ec
fix(memos-local-openclaw): respect OPENCLAW_STATE_DIR in fallback mod…
Mar 17, 2026
cecfe59
fix(memos-local-openclaw): normalize Windows paths when resolving bet…
Mar 17, 2026
2d09a92
fix(openclaw): strip <think/> tags from assistant messages
invalid-email-address Mar 18, 2026
faa011c
feat(memos-local-openclaw): replace PostHog telemetry with Aliyun ARM…
Mar 18, 2026
6d8ef94
fix(openclaw): strip <think/> tags from assistant messages (#1287)
syzsunshine219 Mar 18, 2026
2d4236f
fix(memos-local-openclaw): normalize Windows paths when resolving bet…
syzsunshine219 Mar 18, 2026
af1a8fb
fix(memos-local-openclaw): respect OPENCLAW_STATE_DIR in viewer migra…
syzsunshine219 Mar 18, 2026
e5c791d
fix(memos-local-openclaw): respect OPENCLAW_STATE_DIR in fallback mod…
syzsunshine219 Mar 19, 2026
59a5e20
fix(OpenClaw Local Plugin): resolve search and SQLite installation is…
syzsunshine219 Mar 19, 2026
3ee8444
feat(www): unify one-line install command, improve copy button UX, an…
hijzy Mar 19, 2026
3a0caaf
Merge remote-tracking branch 'upstream/openclaw-local-plugin-20260317…
hijzy Mar 19, 2026
7c8b63f
feat(www): update installer URL and polish install command UI on land…
hijzy Mar 19, 2026
68de10e
Merge branch 'openclaw-local-plugin-20260317' into fix/anthropic-endp…
syzsunshine219 Mar 19, 2026
4276223
fix(openclaw): detect Anthropic provider and use correct API format (…
syzsunshine219 Mar 19, 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,9 @@ cython_debug/
# DS_Store
.DS_Store

# OpenWork integration assets (managed separately)
apps/openwork-memos-integration/apps/desktop/public/assets/usecases/

# Outputs and Evaluation Results
outputs

Expand Down
2 changes: 0 additions & 2 deletions apps/memos-local-openclaw/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ Thumbs.db
# Generated / non-essential
package-lock.json
.installed-version
www/
docs/
ppt/

# Database files
Expand Down
2 changes: 1 addition & 1 deletion apps/memos-local-openclaw/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,7 @@ TELEMETRY_ENABLED=false

### Technical details

- Uses [PostHog](https://posthog.com) for event collection
- Uses Aliyun ARMS RUM for event collection
- Each installation gets a random anonymous UUID (stored at `~/.openclaw/memos-local/.anonymous-id`)
- Events are batched and sent in the background; failures are silently ignored
- The anonymous ID is never linked to any personal information
Expand Down
15 changes: 13 additions & 2 deletions apps/memos-local-openclaw/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { Type } from "@sinclair/typebox";
import * as fs from "fs";
import * as path from "path";
import { fileURLToPath } from "url";
import { buildContext } from "./src/config";
import { SqliteStore } from "./src/storage/sqlite";
import { Embedder } from "./src/embedding";
Expand Down Expand Up @@ -76,13 +77,20 @@ const memosLocalPlugin = {

register(api: OpenClawPluginApi) {
// ─── Ensure better-sqlite3 native module is available ───
const pluginDir = path.dirname(new URL(import.meta.url).pathname);
const pluginDir = path.dirname(fileURLToPath(import.meta.url));

function normalizeFsPath(p: string): string {
return path.resolve(p).replace(/\\/g, "/").toLowerCase();
}

let sqliteReady = false;

function trySqliteLoad(): boolean {
try {
const resolved = require.resolve("better-sqlite3", { paths: [pluginDir] });
if (!resolved.startsWith(pluginDir)) {
const resolvedNorm = normalizeFsPath(resolved);
const pluginNorm = normalizeFsPath(pluginDir);
if (!resolvedNorm.startsWith(pluginNorm + "/") && resolvedNorm !== pluginNorm) {
api.logger.warn(`memos-local: better-sqlite3 resolved outside plugin dir: ${resolved}`);
return false;
}
Expand Down Expand Up @@ -182,6 +190,7 @@ const memosLocalPlugin = {
const workspaceDir = api.resolvePath("~/.openclaw/workspace");
const skillCtx = { ...ctx, workspaceDir };
const skillEvolver = new SkillEvolver(store, engine, skillCtx);
skillEvolver.onSkillEvolved = (name, type) => telemetry.trackSkillEvolved(name, type);
const skillInstaller = new SkillInstaller(store, skillCtx);

let pluginVersion = "0.0.0";
Expand Down Expand Up @@ -257,6 +266,7 @@ const memosLocalPlugin = {
return result;
} catch (e) {
ok = false;
telemetry.trackError(toolName, (e as Error)?.name ?? "unknown");
throw e;
} finally {
const dur = performance.now() - t0;
Expand Down Expand Up @@ -720,6 +730,7 @@ const memosLocalPlugin = {
parameters: Type.Object({}),
execute: trackTool("memory_viewer", async () => {
ctx.log.debug(`memory_viewer called`);
telemetry.trackViewerOpened();
const url = `http://127.0.0.1:${viewerPort}`;
return {
content: [
Expand Down
1 change: 0 additions & 1 deletion apps/memos-local-openclaw/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@
"@huggingface/transformers": "^3.8.0",
"@sinclair/typebox": "^0.34.48",
"better-sqlite3": "^12.6.2",
"posthog-node": "^5.28.0",
"puppeteer": "^24.38.0",
"semver": "^7.7.4",
"uuid": "^10.0.0"
Expand Down
2 changes: 1 addition & 1 deletion apps/memos-local-openclaw/scripts/postinstall.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ try {
function ensureDependencies() {
phase(0, "检测核心依赖 / Check core dependencies");

const coreDeps = ["@sinclair/typebox", "uuid", "posthog-node", "@huggingface/transformers"];
const coreDeps = ["@sinclair/typebox", "uuid", "@huggingface/transformers"];
const missing = [];
for (const dep of coreDeps) {
try {
Expand Down
62 changes: 46 additions & 16 deletions apps/memos-local-openclaw/scripts/refresh-summaries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,36 +62,66 @@ async function main() {
process.exit(1);
}

const isAnthropic = cfg.provider === "anthropic"
|| cfg.endpoint?.toLowerCase().includes("anthropic");

console.log(`Summarizer: ${cfg.provider} / ${cfg.model}`);

let endpoint = cfg.endpoint.replace(/\/+$/, "");
if (!endpoint.endsWith("/chat/completions")) endpoint += "/chat/completions";
if (isAnthropic) {
if (!endpoint.endsWith("/v1/messages") && !endpoint.endsWith("/messages")) {
endpoint += "/v1/messages";
}
} else {
if (!endpoint.endsWith("/chat/completions")) endpoint += "/chat/completions";
}

async function callLLM(text: string): Promise<string> {
const headers: Record<string, string> = isAnthropic
? {
"Content-Type": "application/json",
"x-api-key": cfg.apiKey,
"anthropic-version": "2023-06-01",
}
: {
"Content-Type": "application/json",
Authorization: `Bearer ${cfg.apiKey}`,
};

const body = isAnthropic
? JSON.stringify({
model: cfg.model,
temperature: 0.1,
max_tokens: 4096,
system: TASK_SUMMARY_PROMPT,
messages: [{ role: "user", content: text }],
})
: JSON.stringify({
model: cfg.model,
temperature: 0.1,
max_tokens: 4096,
messages: [
{ role: "system", content: TASK_SUMMARY_PROMPT },
{ role: "user", content: text },
],
});

const resp = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${cfg.apiKey}`,
},
body: JSON.stringify({
model: cfg.model,
temperature: 0.1,
max_tokens: 4096,
messages: [
{ role: "system", content: TASK_SUMMARY_PROMPT },
{ role: "user", content: text },
],
}),
headers,
body,
signal: AbortSignal.timeout(60_000),
});

if (!resp.ok) {
const body = await resp.text();
throw new Error(`API ${resp.status}: ${body.slice(0, 200)}`);
const respBody = await resp.text();
throw new Error(`API ${resp.status}: ${respBody.slice(0, 200)}`);
}

const json = (await resp.json()) as any;
if (isAnthropic) {
return json.content?.find((c: any) => c.type === "text")?.text?.trim() ?? "";
}
return json.choices[0]?.message?.content?.trim() ?? "";
}

Expand Down
8 changes: 8 additions & 0 deletions apps/memos-local-openclaw/src/capture/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export function captureMessages(
if (role === "user") {
content = stripInboundMetadata(content);
} else {
content = stripThinkingTags(content);
content = stripEvidenceWrappers(content, evidenceTag);
}
if (!content.trim()) continue;
Expand Down Expand Up @@ -149,6 +150,13 @@ export function stripInboundMetadata(text: string): string {
return stripEnvelopePrefix(result.join("\n")).trim();
}

/** Strip <think…>…</think⟩ blocks emitted by DeepSeek-style reasoning models. */
const THINKING_TAG_RE = /<think[\s>][\s\S]*?<\/think>\s*/gi;

function stripThinkingTags(text: string): string {
return text.replace(THINKING_TAG_RE, "");
}

function stripEnvelopePrefix(text: string): string {
return text.replace(ENVELOPE_PREFIX_RE, "");
}
Expand Down
2 changes: 0 additions & 2 deletions apps/memos-local-openclaw/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,6 @@ export function resolveConfig(raw: Partial<MemosLocalConfig> | undefined, stateD
},
telemetry: {
enabled: telemetryEnabled,
posthogApiKey: cfg.telemetry?.posthogApiKey ?? process.env.POSTHOG_API_KEY ?? "",
posthogHost: cfg.telemetry?.posthogHost ?? process.env.POSTHOG_HOST ?? "",
},
};
}
Expand Down
50 changes: 42 additions & 8 deletions apps/memos-local-openclaw/src/ingest/providers/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,56 @@
import * as fs from "fs";
import * as path from "path";
import type { SummarizerConfig, Logger } from "../../types";
import { summarizeOpenAI, summarizeTaskOpenAI, generateTaskTitleOpenAI, judgeNewTopicOpenAI, filterRelevantOpenAI, judgeDedupOpenAI } from "./openai";
import type { SummarizerConfig, SummaryProvider, Logger } from "../../types";
import { summarizeOpenAI, summarizeTaskOpenAI, judgeNewTopicOpenAI, filterRelevantOpenAI, judgeDedupOpenAI } from "./openai";
import type { FilterResult, DedupResult } from "./openai";
export type { FilterResult, DedupResult } from "./openai";
import { summarizeAnthropic, summarizeTaskAnthropic, generateTaskTitleAnthropic, judgeNewTopicAnthropic, filterRelevantAnthropic, judgeDedupAnthropic } from "./anthropic";
import { summarizeGemini, summarizeTaskGemini, generateTaskTitleGemini, judgeNewTopicGemini, filterRelevantGemini, judgeDedupGemini } from "./gemini";
import { summarizeBedrock, summarizeTaskBedrock, generateTaskTitleBedrock, judgeNewTopicBedrock, filterRelevantBedrock, judgeDedupBedrock } from "./bedrock";

/**
* Detect provider type from provider key name or base URL.
*/
function detectProvider(
providerKey: string | undefined,
baseUrl: string,
): SummaryProvider {
const key = providerKey?.toLowerCase() ?? "";
const url = baseUrl.toLowerCase();
if (key.includes("anthropic") || url.includes("anthropic")) return "anthropic";
if (key.includes("gemini") || url.includes("generativelanguage.googleapis.com")) {
return "gemini";
}
if (key.includes("bedrock") || url.includes("bedrock")) return "bedrock";
return "openai_compatible";
}

/**
* Return the correct endpoint for a given provider and base URL.
*/
function normalizeEndpointForProvider(
provider: SummaryProvider,
baseUrl: string,
): string {
const stripped = baseUrl.replace(/\/+$/, "");
if (provider === "anthropic") {
if (stripped.endsWith("/v1/messages")) return stripped;
return `${stripped}/v1/messages`;
}
if (stripped.endsWith("/chat/completions")) return stripped;
if (stripped.endsWith("/completions")) return stripped;
return `${stripped}/chat/completions`;
}

/**
* Build a SummarizerConfig from OpenClaw's native model configuration (openclaw.json).
* This serves as the final fallback when both strongCfg and plugin summarizer fail or are absent.
*/
function loadOpenClawFallbackConfig(log: Logger): SummarizerConfig | undefined {
try {
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
const cfgPath = path.join(home, ".openclaw", "openclaw.json");
const ocHome = process.env.OPENCLAW_STATE_DIR || path.join(home, ".openclaw");
const cfgPath = path.join(ocHome, "openclaw.json");
if (!fs.existsSync(cfgPath)) return undefined;

const raw = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
Expand All @@ -36,13 +71,12 @@ function loadOpenClawFallbackConfig(log: Logger): SummarizerConfig | undefined {
const apiKey: string | undefined = providerCfg.apiKey;
if (!baseUrl || !apiKey) return undefined;

const endpoint = baseUrl.endsWith("/chat/completions")
? baseUrl
: baseUrl.replace(/\/+$/, "") + "/chat/completions";
const provider = detectProvider(providerKey, baseUrl);
const endpoint = normalizeEndpointForProvider(provider, baseUrl);

log.debug(`OpenClaw fallback model: ${modelId} via ${baseUrl}`);
log.debug(`OpenClaw fallback model: ${modelId} via ${baseUrl} (${provider})`);
return {
provider: "openai_compatible",
provider,
endpoint,
apiKey,
model: modelId,
Expand Down
Loading
Loading