chore: import upstream snapshot with attribution
pi-agent-plugin checks / lint (push) Has been cancelled
pi-agent-plugin checks / test (20) (push) Has been cancelled
pi-agent-plugin checks / test (22) (push) Has been cancelled
pi-agent-plugin checks / build (push) Has been cancelled
TypeScript SDK CI / check_changes (push) Has been cancelled
TypeScript SDK CI / changelog_check (push) Has been cancelled
ci / changelog_check (push) Has been cancelled
ci / check_changes (push) Has been cancelled
ci / build_mem0 (3.10) (push) Has been cancelled
ci / build_mem0 (3.11) (push) Has been cancelled
ci / build_mem0 (3.12) (push) Has been cancelled
CLI Node CI / lint (push) Has been cancelled
CLI Node CI / test (20) (push) Has been cancelled
CLI Node CI / test (22) (push) Has been cancelled
CLI Node CI / build (push) Has been cancelled
CLI Python CI / lint (push) Has been cancelled
CLI Python CI / test (3.10) (push) Has been cancelled
CLI Python CI / test (3.11) (push) Has been cancelled
CLI Python CI / test (3.12) (push) Has been cancelled
CLI Python CI / build (push) Has been cancelled
openclaw checks / lint (push) Has been cancelled
openclaw checks / test (20) (push) Has been cancelled
openclaw checks / test (22) (push) Has been cancelled
openclaw checks / build (push) Has been cancelled
opencode-plugin checks / build (push) Has been cancelled
TypeScript SDK CI / build_ts_sdk (20) (push) Has been cancelled
TypeScript SDK CI / build_ts_sdk (22) (push) Has been cancelled
TypeScript SDK CI / integration_ts_sdk (20) (push) Has been cancelled
TypeScript SDK CI / integration_ts_sdk (22) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:03:45 +08:00
commit 555e282cc4
1802 changed files with 338080 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+248
View File
@@ -0,0 +1,248 @@
/**
* File-based config helpers for the OpenClaw Mem0 plugin.
*
* Plugin auth and settings are stored in ~/.openclaw/openclaw.json under
* plugins.entries.openclaw-mem0.config — the single source of truth.
*
* Uses fs-safe.ts for all filesystem operations to pass the OpenClaw
* code_safety scanner.
*/
import { join } from "node:path";
import { homedir } from "node:os";
import { readText, exists, writeText, mkdirp } from "../fs-safe.ts";
// OpenClaw config — source of truth for plugin settings
export const OPENCLAW_CONFIG_DIR = join(homedir(), ".openclaw");
export const OPENCLAW_CONFIG_FILE = join(OPENCLAW_CONFIG_DIR, "openclaw.json");
export const DEFAULT_BASE_URL = "https://api.mem0.ai";
const PLUGIN_ID = "openclaw-mem0";
// ============================================================================
// Types
// ============================================================================
/** Fields stored in the plugin config section of openclaw.json */
export interface PluginAuthConfig {
apiKey?: string;
baseUrl?: string;
userId?: string;
userEmail?: string;
mode?: string;
autoRecall?: boolean;
autoCapture?: boolean;
topK?: number;
anonymousTelemetryId?: string;
}
// ============================================================================
// OpenClaw config read/write
// ============================================================================
/**
* Read the full ~/.openclaw/openclaw.json.
*
* Returns {} only when the file doesn't exist (first-time setup).
* Throws on parse errors to prevent writes from destroying existing config.
*/
function readFullConfig(): Record<string, unknown> {
if (!exists(OPENCLAW_CONFIG_FILE)) {
return {};
}
const text = readText(OPENCLAW_CONFIG_FILE);
// Handle empty or whitespace-only files as first-time setup
if (!text.trim()) {
return {};
}
try {
const parsed = JSON.parse(text);
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error("Config is not a JSON object");
}
return parsed;
} catch (err) {
// Fail closed: throw so writes don't proceed with empty config
const msg = err instanceof Error ? err.message : String(err);
throw new Error(
`[openclaw-mem0] Failed to parse ${OPENCLAW_CONFIG_FILE}: ${msg}\n` +
`Fix the JSON syntax error manually before running config commands.`,
);
}
}
/**
* Write the full ~/.openclaw/openclaw.json.
*
* Re-reads the file immediately before writing and deep-merges the
* `plugins` section so that fields written by other processes (e.g.
* OpenClaw gateway adding `installs`, `slots`) are not clobbered.
*/
function writeFullConfig(config: Record<string, unknown>): void {
if (!exists(OPENCLAW_CONFIG_DIR)) {
mkdirp(OPENCLAW_CONFIG_DIR, 0o700);
}
if (exists(OPENCLAW_CONFIG_FILE)) {
try {
const diskText = readText(OPENCLAW_CONFIG_FILE);
if (diskText.trim()) {
const disk = JSON.parse(diskText) as Record<string, unknown>;
const diskPlugins = disk.plugins as Record<string, unknown> | undefined;
const ourPlugins = config.plugins as Record<string, unknown> | undefined;
if (diskPlugins && ourPlugins) {
const OPENCLAW_MANAGED = ["installs", "slots"];
for (const key of OPENCLAW_MANAGED) {
if (key in diskPlugins) {
ourPlugins[key] = diskPlugins[key];
}
}
for (const key of Object.keys(diskPlugins)) {
if (!(key in ourPlugins)) {
ourPlugins[key] = diskPlugins[key];
}
}
}
}
} catch {
// disk unreadable — write our version as-is
}
}
writeText(
OPENCLAW_CONFIG_FILE,
JSON.stringify(config, null, 2),
{ mode: 0o600 },
);
}
/** Read plugin auth/identity config from openclaw.json's plugin section */
export function readPluginAuth(): PluginAuthConfig {
const full = readFullConfig() as any;
const cfg = full?.plugins?.entries?.[PLUGIN_ID]?.config;
if (!cfg || typeof cfg !== "object") return {};
return {
apiKey: (cfg.apiKey ?? cfg.api_key) as string | undefined,
baseUrl: (cfg.baseUrl ?? cfg.base_url) as string | undefined,
userId: (cfg.userId ?? cfg.user_id) as string | undefined,
userEmail: (cfg.userEmail ?? cfg.user_email) as string | undefined,
mode: cfg.mode as string | undefined,
autoRecall: cfg.autoRecall as boolean | undefined,
autoCapture: cfg.autoCapture as boolean | undefined,
topK: cfg.topK as number | undefined,
anonymousTelemetryId: cfg.anonymousTelemetryId as string | undefined,
};
}
/** Write auth/identity fields into the plugin section of openclaw.json */
export function writePluginAuth(auth: PluginAuthConfig): void {
const full = readFullConfig() as any;
ensurePluginStructure(full);
const cfg = full.plugins.entries[PLUGIN_ID].config;
// Write all defined fields into the config section
for (const [key, value] of Object.entries(auth)) {
if (value !== undefined) cfg[key] = value;
}
writeFullConfig(full);
}
/** Ensure the nested plugin entry structure exists in the config object. */
function ensurePluginStructure(full: any): void {
if (!full.plugins) full.plugins = {};
if (!full.plugins.entries) full.plugins.entries = {};
if (!full.plugins.entries[PLUGIN_ID]) {
full.plugins.entries[PLUGIN_ID] = { enabled: true, config: {} };
}
if (!full.plugins.entries[PLUGIN_ID].config) {
full.plugins.entries[PLUGIN_ID].config = {};
}
}
export function writePluginConfigField(
path: string[],
value: unknown,
): void {
const full = readFullConfig() as any;
ensurePluginStructure(full);
let target = full.plugins.entries[PLUGIN_ID].config;
for (let i = 0; i < path.length - 1; i++) {
if (!target[path[i]] || typeof target[path[i]] !== "object") {
target[path[i]] = {};
}
target = target[path[i]];
}
target[path[path.length - 1]] = value;
writeFullConfig(full);
}
/**
* Default skills configuration — matches configure.py output.
* Enables triage, recall (with reranking), and dream consolidation.
*/
const DEFAULT_SKILLS_CONFIG = {
triage: { enabled: true },
recall: {
enabled: true,
tokenBudget: 1500,
rerank: true,
keywordSearch: true,
identityAlwaysInclude: true,
},
dream: { enabled: true },
domain: "companion",
};
/**
* Enable skills-mode config after onboarding.
*
* Sets skills config on the plugin entry, tools.profile = "full",
* and disables the built-in session-memory hook to avoid conflicts.
* Preserves any existing skills config if already set.
*/
export function enableSkillsConfig(userId: string): void {
const full = readFullConfig() as any;
ensurePluginStructure(full);
const cfg = full.plugins.entries[PLUGIN_ID].config;
if (!cfg.skills) {
cfg.skills = { ...DEFAULT_SKILLS_CONFIG };
}
if (!full.tools) full.tools = {};
full.tools.profile = "full";
if (!full.hooks) full.hooks = {};
if (!full.hooks.internal) full.hooks.internal = {};
if (!full.hooks.internal.entries) full.hooks.internal.entries = {};
full.hooks.internal.entries["session-memory"] = { enabled: false };
writeFullConfig(full);
}
/** Get the configured base URL from openclaw.json or default */
export function getBaseUrl(): string {
const auth = readPluginAuth();
return auth.baseUrl || DEFAULT_BASE_URL;
}
/** Remove anonymousTelemetryId from config (after PostHog aliasing) */
export function clearAnonymousTelemetryId(): void {
const full = readFullConfig() as any;
const cfg = full?.plugins?.entries?.[PLUGIN_ID]?.config;
if (cfg && "anonymousTelemetryId" in cfg) {
delete cfg.anonymousTelemetryId;
writeFullConfig(full);
}
}
+40
View File
@@ -0,0 +1,40 @@
/**
* JSON output helpers for agent-friendly CLI commands.
*/
function writeStdout(data: Record<string, unknown>): void {
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
}
export function jsonOut(
opts: { json?: boolean },
data: Record<string, unknown>,
): boolean {
if (!opts.json) return false;
writeStdout(data);
return true;
}
export function jsonErr(
opts: { json?: boolean },
error: string,
): boolean {
if (!opts.json) return false;
writeStdout({ ok: false, error });
return true;
}
export function redactSecrets(
obj: Record<string, unknown>,
secretKeys: Set<string>,
): Record<string, unknown> {
const result = { ...obj };
for (const key of secretKeys) {
const val = result[key];
if (typeof val !== "string") continue;
result[key] = val.length <= 8
? val.slice(0, 2) + "***"
: val.slice(0, 4) + "..." + val.slice(-4);
}
return result;
}
+239
View File
@@ -0,0 +1,239 @@
/**
* OSS provider wizard — provider definitions, config builders, and validation.
*
* Used by the init command for both interactive wizard and non-interactive
* --oss-* flag paths.
*/
import { join } from "node:path";
import { homedir } from "node:os";
// ============================================================================
// Provider definitions
// ============================================================================
export interface ProviderDef {
id: string;
label: string;
needsApiKey: boolean;
needsUrl: boolean;
envVar?: string;
defaultModel: string;
defaultUrl?: string;
}
export const LLM_PROVIDERS: ProviderDef[] = [
{ id: "openai", label: "OpenAI (requires API key)", needsApiKey: true, needsUrl: false, envVar: "OPENAI_API_KEY", defaultModel: "gpt-5-mini" },
{ id: "ollama", label: "Ollama (local, no API key)", needsApiKey: false, needsUrl: true, defaultModel: "llama3.1:8b", defaultUrl: "http://localhost:11434" },
{ id: "anthropic", label: "Anthropic (requires API key)", needsApiKey: true, needsUrl: false, envVar: "ANTHROPIC_API_KEY", defaultModel: "claude-sonnet-4-5-20250514" },
];
export interface EmbedderDef extends ProviderDef {
defaultDims: number;
}
export const EMBEDDER_PROVIDERS: EmbedderDef[] = [
{ id: "openai", label: "OpenAI (requires API key)", needsApiKey: true, needsUrl: false, envVar: "OPENAI_API_KEY", defaultModel: "text-embedding-3-small", defaultDims: 1536 },
{ id: "ollama", label: "Ollama (local, no API key)", needsApiKey: false, needsUrl: true, defaultModel: "nomic-embed-text", defaultUrl: "http://localhost:11434", defaultDims: 768 },
];
export interface VectorDef {
id: string;
label: string;
needsConnection: boolean;
defaultUrl?: string;
defaultPort?: number;
setupHint?: string;
}
export const VECTOR_PROVIDERS: VectorDef[] = [
{ id: "qdrant", label: "Qdrant (requires server — Docker or cloud)", needsConnection: true, defaultUrl: "http://localhost:6333", defaultPort: 6333, setupHint: "docker run -d -p 6333:6333 qdrant/qdrant" },
{ id: "pgvector", label: "PGVector (requires PostgreSQL + pgvector extension)", needsConnection: true, defaultPort: 5432, setupHint: "docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=postgres pgvector/pgvector:pg17" },
];
export const KNOWN_EMBEDDER_DIMS: Record<string, number> = {
"text-embedding-3-small": 1536,
"text-embedding-3-large": 3072,
"text-embedding-ada-002": 1536,
"nomic-embed-text": 768,
"mxbai-embed-large": 1024,
"all-minilm": 384,
"snowflake-arctic-embed": 1024,
};
export function collectionNameForDims(dims: number): string {
return `mem0_${dims}d`;
}
// ============================================================================
// Config builders
// ============================================================================
export interface LlmConfigInput {
apiKey?: string;
model?: string;
url?: string;
}
export function buildOssLlmConfig(
providerId: string,
input: LlmConfigInput,
): { provider: string; config: Record<string, unknown> } {
const def = LLM_PROVIDERS.find((p) => p.id === providerId);
if (!def) throw new Error(`Unknown LLM provider: ${providerId}`);
const config: Record<string, unknown> = {
model: input.model || def.defaultModel,
};
if (input.apiKey) config.apiKey = input.apiKey;
if (providerId === "ollama") {
config.url = input.url || def.defaultUrl;
}
return { provider: providerId, config };
}
export interface EmbedderConfigInput {
apiKey?: string;
model?: string;
url?: string;
}
export function buildOssEmbedderConfig(
providerId: string,
input: EmbedderConfigInput,
): { provider: string; config: Record<string, unknown>; dims: number | undefined } {
const def = EMBEDDER_PROVIDERS.find((p) => p.id === providerId);
if (!def) throw new Error(`Unknown embedder provider: ${providerId}`);
const model = input.model || def.defaultModel;
const config: Record<string, unknown> = { model };
if (input.apiKey) config.apiKey = input.apiKey;
if (providerId === "ollama") {
config.url = input.url || def.defaultUrl;
}
const dims = KNOWN_EMBEDDER_DIMS[model] ?? def.defaultDims;
if (dims) config.embeddingDims = dims;
return { provider: providerId, config, dims };
}
export interface VectorConfigInput {
url?: string;
host?: string;
port?: string;
user?: string;
password?: string;
dbname?: string;
apiKey?: string;
dims?: number;
}
export function buildOssVectorConfig(
providerId: string,
input: VectorConfigInput,
): { provider: string; config: Record<string, unknown> } {
const config: Record<string, unknown> = {};
if (providerId === "qdrant") {
config.url = input.url || "http://localhost:6333";
config.onDisk = true;
if (input.apiKey) config.apiKey = input.apiKey;
} else if (providerId === "pgvector") {
config.host = input.host || "localhost";
config.port = parseInt(input.port || "5432", 10);
if (input.user) config.user = input.user;
if (input.password) config.password = input.password;
config.dbname = input.dbname || "postgres";
}
if (input.dims) {
config.dimension = input.dims;
config.embeddingModelDims = input.dims;
config.collectionName = collectionNameForDims(input.dims);
}
return { provider: providerId, config };
}
export async function checkOllamaConnectivity(url: string): Promise<{ ok: boolean; error?: string }> {
try {
const resp = await fetch(`${url.replace(/\/+$/, "")}/api/tags`, { signal: AbortSignal.timeout(3000) });
if (resp.ok) return { ok: true };
return { ok: false, error: `Ollama returned HTTP ${resp.status}` };
} catch {
return { ok: false, error: `Cannot reach Ollama at ${url}. Install: https://ollama.com/download` };
}
}
export async function checkPgConnectivity(host: string, port: number): Promise<{ ok: boolean; error?: string }> {
return new Promise((resolve) => {
import("node:net").then(({ createConnection }) => {
const sock = createConnection({ host, port, timeout: 3000 });
sock.once("connect", () => { sock.destroy(); resolve({ ok: true }); });
sock.once("timeout", () => { sock.destroy(); resolve({ ok: false, error: `PostgreSQL not reachable at ${host}:${port}` }); });
sock.once("error", () => { sock.destroy(); resolve({ ok: false, error: `PostgreSQL not reachable at ${host}:${port}. Ensure PostgreSQL with pgvector extension is running.` }); });
});
});
}
export async function checkQdrantConnectivity(url: string): Promise<{ ok: boolean; error?: string }> {
try {
const resp = await fetch(`${url.replace(/\/+$/, "")}/healthz`, { signal: AbortSignal.timeout(3000) });
if (resp.ok) return { ok: true };
return { ok: false, error: `Qdrant returned HTTP ${resp.status}` };
} catch (err) {
return { ok: false, error: `Cannot reach Qdrant at ${url}. Start it with: docker run -d -p 6333:6333 qdrant/qdrant` };
}
}
// ============================================================================
// Non-interactive flag validation
// ============================================================================
export interface OssFlags {
ossLlm?: string;
ossLlmKey?: string;
ossLlmModel?: string;
ossLlmUrl?: string;
ossEmbedder?: string;
ossEmbedderKey?: string;
ossEmbedderModel?: string;
ossEmbedderUrl?: string;
ossVector?: string;
ossVectorUrl?: string;
ossVectorHost?: string;
ossVectorPort?: string;
ossVectorUser?: string;
ossVectorPassword?: string;
ossVectorDbname?: string;
ossVectorDims?: string;
}
export function validateOssFlags(
flags: OssFlags,
): { error?: string } {
const llmId = flags.ossLlm || "openai";
const llmDef = LLM_PROVIDERS.find((p) => p.id === llmId);
if (!llmDef) return { error: `Unknown LLM provider: ${llmId}. Valid: ${LLM_PROVIDERS.map((p) => p.id).join(", ")}` };
if (llmDef.needsApiKey && !flags.ossLlmKey) {
return { error: `--oss-llm-key required when --oss-llm is ${llmId}` };
}
const embId = flags.ossEmbedder || "openai";
const embDef = EMBEDDER_PROVIDERS.find((p) => p.id === embId);
if (!embDef) return { error: `Unknown embedder provider: ${embId}. Valid: ${EMBEDDER_PROVIDERS.map((p) => p.id).join(", ")}` };
if (embDef.needsApiKey && !flags.ossEmbedderKey && !flags.ossLlmKey) {
return { error: `--oss-embedder-key required when --oss-embedder is ${embId}` };
}
const vecId = flags.ossVector || "qdrant";
const vecDef = VECTOR_PROVIDERS.find((p) => p.id === vecId);
if (!vecDef) return { error: `Unknown vector store provider: ${vecId}. Valid: ${VECTOR_PROVIDERS.map((p) => p.id).join(", ")}` };
if (vecId === "pgvector" && !flags.ossVectorUser) {
return { error: "--oss-vector-user required when --oss-vector is pgvector" };
}
return {};
}