Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 606ede7c0a | |||
| edd1b3e2f2 | |||
| 74d043731b | |||
| 843ab82905 | |||
| 79793b0d2e | |||
| 5f7ace2aef | |||
| 219b1a6f3d | |||
| 57c8468ce6 | |||
| ddee5f8671 | |||
| fbce5fab14 | |||
| 6a1597c6fb | |||
| c9e8482a35 | |||
| e602923751 | |||
| 70bc9e51d5 | |||
| 0107fd53b8 | |||
| 54a03cc721 | |||
| e95de4ca50 | |||
| a623cfaf76 | |||
| 92491c00c2 | |||
| 9043fbf61e | |||
| c90cbc75a2 | |||
| 58304fc939 | |||
| 397f3414ee | |||
| a734e057cf | |||
| 0fdaa29b4a | |||
| 6d3486ca56 | |||
| ebb9bb2b15 | |||
| 594b4e65d6 | |||
| 1b95c99db4 | |||
| b66cf0f272 | |||
| 72dca1cdf5 | |||
| ece7ff6b84 |
@@ -12,7 +12,7 @@
|
||||
"name": "mem0",
|
||||
"source": "./mem0-plugin",
|
||||
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Claude workflows.",
|
||||
"version": "0.1.1"
|
||||
"version": "0.1.3"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ on:
|
||||
- 'tests/**'
|
||||
- 'embedchain/**'
|
||||
- 'pyproject.toml'
|
||||
- 'cli/**'
|
||||
- 'docs/**'
|
||||
- '.github/workflows/**'
|
||||
|
||||
jobs:
|
||||
changelog_check:
|
||||
|
||||
@@ -27,7 +27,7 @@ This is a **polyglot monorepo** containing Python and TypeScript packages, CLIs,
|
||||
| `server/` | FastAPI REST server for self-hosted Mem0 (Docker: FastAPI + PostgreSQL/pgvector + Neo4j) |
|
||||
| `openmemory/` | Self-hosted memory platform — `api/` (FastAPI + Alembic + MCP server) and `ui/` (Next.js 15 + React 19) |
|
||||
| `mem0-plugin/` | AI editor plugins (Claude Code, Cursor, Codex) — MCP server connection, lifecycle hooks, skills |
|
||||
| `skills/` | Claude Code skill definitions — `mem0/`, `mem0-cli/`, `mem0-vercel-ai-sdk/` |
|
||||
| `skills/` | Claude Code skill definitions. Reference skills (SDK knowledge, always-on): `mem0/`, `mem0-cli/`, `mem0-vercel-ai-sdk/`. Pipeline skills (run on demand): `mem0-integrate/`, `mem0-test-integration/` |
|
||||
| `docs/` | Documentation site (Mintlify) |
|
||||
| `tests/` | Python SDK tests (pytest) |
|
||||
| `evaluation/` | Benchmarking framework — LOCOMO evals, experiment runner, score generation |
|
||||
@@ -387,7 +387,9 @@ Model Context Protocol support in multiple places:
|
||||
### Plugin & Skills System
|
||||
|
||||
- `mem0-plugin/` provides integrations for Claude Code, Cursor, and Codex via MCP server connections and lifecycle hooks for automatic memory capture.
|
||||
- `skills/` contains structured skill definitions for AI agents, covering SDK usage, CLI workflows, and Vercel AI SDK patterns.
|
||||
- `skills/` contains structured skill definitions for AI agents, split into two categories:
|
||||
- **Reference skills** (always-on SDK knowledge): `mem0` (Python + TS SDKs, framework integrations), `mem0-cli` (terminal workflows), `mem0-vercel-ai-sdk` (Vercel AI provider).
|
||||
- **Pipeline skills** (run on demand): `mem0-integrate` wires Mem0 into an existing repo via a TDD pipeline; `mem0-test-integration` verifies what the integrator produced on the same branch. The two are loosely coupled via `.mem0-integration/` artifacts.
|
||||
|
||||
### Adding a New Provider
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
| Benchmark | Old | New | Tokens | Latency p50 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| **LoCoMo** | 71.4 | **91.6** | 7.0K | 0.88s |
|
||||
| **LongMemEval** | 67.8 | **93.4** | 6.8K | 1.09s |
|
||||
| **LongMemEval** | 67.8 | **94.8** | 6.8K | 1.09s |
|
||||
| **BEAM (1M)** | — | **64.1** | 6.7K | 1.00s |
|
||||
| **BEAM (10M)** | — | **48.6** | 6.9K | 1.05s |
|
||||
|
||||
@@ -58,12 +58,13 @@ All benchmarks run on the same production-representative model stack. Single-pas
|
||||
- **Agent-generated facts are first-class** -- when an agent confirms an action, that information is now stored with equal weight.
|
||||
- **Entity linking** -- entities are extracted, embedded, and linked across memories for retrieval boosting.
|
||||
- **Multi-signal retrieval** -- semantic, BM25 keyword, and entity matching scored in parallel and fused.
|
||||
- **Temporal Reasoning** -- time-aware retrieval that ranks the right dated instance for queries about current state, past events, and upcoming plans.
|
||||
|
||||
See the [migration guide](https://docs.mem0.ai/migration/oss-v2-to-v3) for upgrade instructions. The [evaluation framework](https://github.com/mem0ai/memory-benchmarks) is open-sourced so anyone can reproduce the numbers.
|
||||
|
||||
## Research Highlights
|
||||
- **91.6 on LoCoMo** -- +20 points over the previous algorithm
|
||||
- **93.4 on LongMemEval** -- +26 points, with +53.6 on assistant memory recall
|
||||
- **94.8 on LongMemEval** -- +27 points, with +53.6 on assistant memory recall
|
||||
- **64.1 on BEAM (1M)** -- production-scale memory evaluation at 1M tokens
|
||||
- [Read the full paper](https://mem0.ai/research)
|
||||
|
||||
@@ -85,6 +86,26 @@ See the [migration guide](https://docs.mem0.ai/migration/oss-v2-to-v3) for upgra
|
||||
|
||||
## 🚀 Quickstart Guide <a name="quickstart"></a>
|
||||
|
||||
### Sign up as an agent
|
||||
|
||||
AI agents can mint a working Mem0 API key in under five seconds — no email, no dashboard, no OTP. Four commands end-to-end:
|
||||
|
||||
```bash
|
||||
# 1. Install
|
||||
npm install -g @mem0/cli # or: pip install mem0-cli
|
||||
|
||||
# 2. Sign up as an agent (replace `claude-code` with your name)
|
||||
mem0 init --agent --agent-caller claude-code
|
||||
|
||||
# 3. Add a memory
|
||||
mem0 add "I am using mem0"
|
||||
|
||||
# 4. Search
|
||||
mem0 search "am I using mem0"
|
||||
```
|
||||
|
||||
The human owner can claim the account later with `mem0 init --email <their-email>` — same key, memories preserved. Full guide: [Sign up as an agent](https://docs.mem0.ai/platform/agent-signup).
|
||||
|
||||
| | Library | Self-Hosted Server | Cloud Platform |
|
||||
|---|---------|-------------------|----------------|
|
||||
| **Best for** | Testing, prototyping | Teams running on their own infrastructure | Zero-ops production use |
|
||||
@@ -132,6 +153,7 @@ See the [self-hosted docs](https://docs.mem0.ai/open-source/overview) for config
|
||||
|
||||
1. Sign up on [Mem0 Platform](https://app.mem0.ai?utm_source=oss&utm_medium=readme)
|
||||
2. Embed the memory layer via SDK or API keys
|
||||
3. Using hosted Qdrant vectors? See the [Platform migration guide](https://docs.mem0.ai/migration/oss-to-platform) to import them into Mem0 Platform.
|
||||
|
||||
### CLI
|
||||
|
||||
@@ -147,6 +169,27 @@ mem0 search "What does Alice prefer?" --user-id alice
|
||||
|
||||
See the [CLI documentation](https://docs.mem0.ai/platform/cli) for the full command reference.
|
||||
|
||||
### Agent Skills
|
||||
|
||||
Teach your AI coding assistant (Claude Code, Codex, Cursor, Windsurf, OpenCode, OpenClaw, and any tool that supports the skills standard) how to build with Mem0. Two categories:
|
||||
|
||||
**Reference skills — always on** (SDK knowledge loaded into the assistant's context):
|
||||
|
||||
```bash
|
||||
npx skills add https://github.com/mem0ai/mem0 --skill mem0
|
||||
npx skills add https://github.com/mem0ai/mem0 --skill mem0-cli
|
||||
npx skills add https://github.com/mem0ai/mem0 --skill mem0-vercel-ai-sdk
|
||||
```
|
||||
|
||||
**Pipeline skills — run on demand** (execute an end-to-end workflow in an existing repo):
|
||||
|
||||
```bash
|
||||
npx skills add https://github.com/mem0ai/mem0 --skill mem0-integrate
|
||||
npx skills add https://github.com/mem0ai/mem0 --skill mem0-test-integration
|
||||
```
|
||||
|
||||
Use `/mem0-integrate` to wire Mem0 into an existing repo via a test-first pipeline, then `/mem0-test-integration` to verify. See the [skills catalog](./skills/) or [Vibecoding with Mem0](https://docs.mem0.ai/vibecoding) for the full picture.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
Mem0 requires an LLM to function, with `gpt-5-mini` from OpenAI as the default. However, it supports a variety of LLMs; for details, refer to our [Supported LLMs documentation](https://docs.mem0.ai/components/llms/overview).
|
||||
|
||||
+4
-2
@@ -503,7 +503,7 @@
|
||||
},
|
||||
{
|
||||
"name": "init",
|
||||
"description": "Setup wizard for mem0 CLI. Supports email login (--email) or manual API key (--api-key).",
|
||||
"description": "Setup wizard for mem0 CLI. Supports Agent Mode bootstrap (--agent), email login (--email), or manual API key (--api-key).",
|
||||
"usage": "mem0 init [OPTIONS]",
|
||||
"needsBackend": false,
|
||||
"needsConfig": false,
|
||||
@@ -516,7 +516,9 @@
|
||||
{ "name": "user-id", "flags": ["-u", "--user-id"], "type": "string", "default": null, "help": "Default user ID (skip prompt)." },
|
||||
{ "name": "email", "flags": ["--email"], "type": "string", "default": null, "help": "Login via email verification code." },
|
||||
{ "name": "code", "flags": ["--code"], "type": "string", "default": null, "help": "Verification code (use with --email for non-interactive login)." },
|
||||
{ "name": "force", "flags": ["--force"], "type": "boolean", "default": false, "help": "Overwrite existing config without confirmation." }
|
||||
{ "name": "force", "flags": ["--force"], "type": "boolean", "default": false, "help": "Overwrite existing config without confirmation." },
|
||||
{ "name": "agent", "flags": ["--agent"], "type": "boolean", "default": false, "help": "Bootstrap an unattended Agent Mode account (no email required)." },
|
||||
{ "name": "source", "flags": ["--source"], "type": "string", "default": null, "help": "Channel attribution for signup (e.g. github, hn, ph)." }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to `@mem0/cli` are documented here.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.2.7] — 2026-05-20
|
||||
|
||||
### Added
|
||||
|
||||
- `mem0 whoami` — print the active agent's `default_user_id` (the AGENTRUSH
|
||||
leaderboard identifier). Reads from local config, no network call.
|
||||
- `mem0 agent-rush <add | search>` — subcommand group that wraps the new
|
||||
`/v1/agent-rush/` platform endpoints for the 7-day AGENTRUSH game. Project
|
||||
routing is implicit (resolved server-side); no flags exposed. Pretty-prints
|
||||
platform error codes into actionable hints (e.g. `agentrush_search_first`
|
||||
→ "Run 3 'mem0 agent-rush search' commands before adding.").
|
||||
- PII safety prompt on first `mem0 agent-rush add`. Interactive runs require
|
||||
explicit `y` to acknowledge that AGENTRUSH memories are public; the
|
||||
acknowledgement is persisted in `~/.mem0/config.json` under
|
||||
`agent_rush.acknowledged_at` so the prompt only appears once per machine.
|
||||
Non-interactive (agent) invocations surface the warning to stderr without
|
||||
blocking.
|
||||
- New config schema field: `agent_rush.acknowledged_at` (ISO timestamp,
|
||||
empty until first interactive acknowledgement).
|
||||
|
||||
### Changed
|
||||
|
||||
- HTTP requests from the new agent-rush commands send `X-Mem0-Mode: agent-rush`
|
||||
in addition to the existing source headers, so platform telemetry can split
|
||||
game traffic from regular CLI usage.
|
||||
|
||||
## [0.2.6] and earlier
|
||||
|
||||
Unlogged historical releases. See git history under `cli/node/`.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mem0/cli",
|
||||
"version": "0.2.4",
|
||||
"version": "0.2.7",
|
||||
"description": "The official CLI for mem0 — the memory layer for AI agents",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Detect whether the CLI is being invoked from inside an AI-agent context.
|
||||
*
|
||||
* Used by `mem0 init` to auto-enter Agent Mode (Rule 3 bootstrap) when an
|
||||
* agent runtime env var is present. The return value is a context **trigger
|
||||
* only** — the canonical agent identity is self-declared by the agent via
|
||||
* `--agent-caller <name>` (Proof Editor-style) and never sniffed from env
|
||||
* vars to fill the `agent_caller` field on the APIKey row.
|
||||
*
|
||||
* Returns a short name or null. Honest reporting depends on `--agent-caller`;
|
||||
* this list is just enough to enable the zero-friction auto-bootstrap UX.
|
||||
*/
|
||||
|
||||
const AGENT_CALLER_ENV: ReadonlyArray<readonly [string, readonly string[]]> = [
|
||||
["claude-code", ["CLAUDECODE", "CLAUDE_CODE"]],
|
||||
["cursor", ["CURSOR_AGENT", "CURSOR_SESSION_ID"]],
|
||||
["codex", ["CODEX_CLI", "OPENAI_CODEX"]],
|
||||
["cline", ["CLINE_AGENT", "CLINE"]],
|
||||
["continue", ["CONTINUE_AGENT", "CONTINUE_SESSION"]],
|
||||
["aider", ["AIDER_SESSION"]],
|
||||
["goose", ["GOOSE_AGENT"]],
|
||||
["windsurf", ["WINDSURF_AGENT"]],
|
||||
] as const;
|
||||
|
||||
export function detectAgentCaller(): string | null {
|
||||
for (const [name, envVars] of AGENT_CALLER_ENV) {
|
||||
if (envVars.some((v) => process.env[v])) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import type { PlatformConfig } from "../config.js";
|
||||
import { isAgentMode } from "../state.js";
|
||||
import { captureNotice, isAgentMode } from "../state.js";
|
||||
import { CLI_VERSION } from "../version.js";
|
||||
import {
|
||||
APIError,
|
||||
@@ -90,7 +90,39 @@ export class PlatformBackend implements Backend {
|
||||
if (resp.status === 204) {
|
||||
return {};
|
||||
}
|
||||
return resp.json();
|
||||
|
||||
const data = await resp.json();
|
||||
|
||||
// Pull the unclaimed-Agent-Mode notice out of the body (or the header
|
||||
// fallback for endpoints returning non-dict / non-dict-leading payloads)
|
||||
// and stash for end-of-command surfacing.
|
||||
let notice: string | null = null;
|
||||
if (
|
||||
data &&
|
||||
typeof data === "object" &&
|
||||
!Array.isArray(data) &&
|
||||
"mem0_notice" in data
|
||||
) {
|
||||
notice = (data as Record<string, unknown>).mem0_notice as string;
|
||||
// biome-ignore lint/performance/noDelete: intentional strip so downstream consumers don't see duplicate notice
|
||||
delete (data as Record<string, unknown>).mem0_notice;
|
||||
} else if (
|
||||
Array.isArray(data) &&
|
||||
data.length > 0 &&
|
||||
typeof data[0] === "object" &&
|
||||
data[0] !== null &&
|
||||
"mem0_notice" in data[0]
|
||||
) {
|
||||
notice = (data[0] as Record<string, unknown>).mem0_notice as string;
|
||||
// biome-ignore lint/performance/noDelete: see above.
|
||||
delete (data[0] as Record<string, unknown>).mem0_notice;
|
||||
}
|
||||
if (!notice) {
|
||||
notice = resp.headers.get("X-Mem0-Notice-Message") ?? null;
|
||||
}
|
||||
captureNotice(notice);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async add(
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* Agent Mode commands — bootstrap (unattended signup) and OTP-based claim.
|
||||
*/
|
||||
|
||||
import readline from "node:readline";
|
||||
import { colors, printError, printInfo, printSuccess } from "../branding.js";
|
||||
import { type Mem0Config, saveConfig } from "../config.js";
|
||||
|
||||
const { brand, dim } = colors;
|
||||
|
||||
const SOURCE_HEADERS = {
|
||||
"X-Mem0-Source": "cli",
|
||||
"X-Mem0-Client-Language": "node",
|
||||
} as const;
|
||||
|
||||
export interface BootstrapEnvelope {
|
||||
api_key: string;
|
||||
default_user_id: string;
|
||||
org_id: string;
|
||||
project_id: string;
|
||||
mcp_url?: string;
|
||||
smoke_test_url?: string;
|
||||
claim_command?: string;
|
||||
mem0_notice?: string;
|
||||
}
|
||||
|
||||
function isValidEnvelope(v: unknown): v is BootstrapEnvelope {
|
||||
return (
|
||||
!!v &&
|
||||
typeof v === "object" &&
|
||||
typeof (v as BootstrapEnvelope).api_key === "string" &&
|
||||
(v as BootstrapEnvelope).api_key.length > 0 &&
|
||||
typeof (v as BootstrapEnvelope).default_user_id === "string" &&
|
||||
(v as BootstrapEnvelope).default_user_id.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/auth/agent_mode/ and mutate config in place.
|
||||
*
|
||||
* @param config - Mem0Config mutated in place with the new platform values.
|
||||
* @param source - `--source` flag passthrough (analytics tag, free-form).
|
||||
* @param agentCaller - Self-declared agent identity passed via `--agent-caller`
|
||||
* (e.g. `claude-code`, `cursor`). May be null when the caller omitted the
|
||||
* flag; the agent can backfill later via `mem0 identify <name>`. Sent to the
|
||||
* backend in the request body and saved into `platform.agentCaller` for
|
||||
* local introspection.
|
||||
*/
|
||||
export async function bootstrapViaBackend(
|
||||
config: Mem0Config,
|
||||
{
|
||||
source,
|
||||
agentCaller,
|
||||
}: { source?: string | null; agentCaller?: string | null } = {},
|
||||
): Promise<void> {
|
||||
const baseUrl = (config.platform.baseUrl || "https://api.mem0.ai").replace(
|
||||
/\/+$/,
|
||||
"",
|
||||
);
|
||||
const body: Record<string, unknown> = {};
|
||||
if (source) body.source = source;
|
||||
if (agentCaller) body.agent_caller = agentCaller;
|
||||
|
||||
let resp: Response;
|
||||
try {
|
||||
resp = await fetch(`${baseUrl}/api/v1/auth/agent_mode/`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
...SOURCE_HEADERS,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
} catch (err) {
|
||||
printError(
|
||||
`Network error contacting Mem0: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (resp.status === 429) {
|
||||
printError("Rate-limited. Try again in a few minutes.");
|
||||
process.exit(1);
|
||||
}
|
||||
if (resp.status === 503) {
|
||||
printError("Agent Mode is temporarily disabled. Try again later.");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!resp.ok) {
|
||||
let detail: string = resp.statusText;
|
||||
try {
|
||||
const errBody = (await resp.json()) as {
|
||||
error?: string;
|
||||
detail?: string;
|
||||
};
|
||||
detail = errBody.error ?? errBody.detail ?? resp.statusText;
|
||||
} catch {
|
||||
/* leave detail as statusText */
|
||||
}
|
||||
// Backend's @ratelimit decorator raises PermissionDenied, which DRF
|
||||
// translates to a generic 403 "You do not have permission to perform
|
||||
// this action." That's opaque — surface it as the rate-limit message
|
||||
// it actually is.
|
||||
if (resp.status === 403 && /permission/i.test(detail)) {
|
||||
printError(
|
||||
"Daily Agent Mode signup limit reached for this network (5/day). Try again from a different IP or after midnight UTC.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
printError(`Bootstrap failed: ${detail}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const envelope = (await resp.json()) as BootstrapEnvelope;
|
||||
if (!isValidEnvelope(envelope)) {
|
||||
// Defend against partial/malformed backend responses (e.g. {api_key: null}).
|
||||
// Without this guard, the typed `string` field is silently set to
|
||||
// undefined/null and persisted, producing confusing downstream errors.
|
||||
printError(
|
||||
"Bootstrap response missing required fields — please update the CLI.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
config.platform.apiKey = envelope.api_key;
|
||||
config.platform.baseUrl = baseUrl;
|
||||
config.platform.agentMode = true;
|
||||
config.platform.createdVia = "agent_mode";
|
||||
config.platform.agentCaller = agentCaller ?? "";
|
||||
config.platform.claimedAt = "";
|
||||
config.platform.defaultUserId = envelope.default_user_id;
|
||||
// Adopt the slug-derived user_id as the default scope for memory ops.
|
||||
config.defaults.userId = envelope.default_user_id;
|
||||
saveConfig(config);
|
||||
|
||||
printSuccess(
|
||||
`Agent Mode active. Default user_id: ${envelope.default_user_id}`,
|
||||
);
|
||||
if (envelope.mem0_notice) {
|
||||
console.log(`\n\x1b[33m🔔 ${envelope.mem0_notice}\x1b[0m\n`);
|
||||
} else {
|
||||
// Fallback for older backends without the unified notice field.
|
||||
const claimCmd = envelope.claim_command ?? "mem0 init --email <your-email>";
|
||||
console.log(` ${dim(`To claim this account later: ${claimCmd}`)}`);
|
||||
}
|
||||
|
||||
if (!agentCaller) {
|
||||
console.log(
|
||||
` ${dim("If you are an AI agent, identify yourself: `mem0 identify <your-name>` (e.g. claude-code, cursor).")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Claim an existing Agent Mode account via OTP — no browser, no polling.
|
||||
*
|
||||
* Hits /api/v1/auth/email_code/ to send a verification code, prompts for it
|
||||
* interactively (or accepts via `code`), then sends it to /verify/ alongside
|
||||
* `agent_mode_api_key`. Backend's verify_email_code runs upgrade-in-place
|
||||
* inline and returns the claim result.
|
||||
*/
|
||||
export async function claimViaOtp(
|
||||
config: Mem0Config,
|
||||
{ email, code }: { email: string; code?: string },
|
||||
): Promise<void> {
|
||||
const baseUrl = (config.platform.baseUrl || "https://api.mem0.ai").replace(
|
||||
/\/+$/,
|
||||
"",
|
||||
);
|
||||
if (!config.platform.apiKey || !config.platform.agentMode) {
|
||||
printError(
|
||||
"This command requires an active Agent Mode config. Run `mem0 init` first.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const rawKey = config.platform.apiKey;
|
||||
|
||||
// Step 1: request OTP (unless --code was supplied)
|
||||
if (!code) {
|
||||
const sendResp = await fetch(`${baseUrl}/api/v1/auth/email_code/`, {
|
||||
method: "POST",
|
||||
headers: { ...SOURCE_HEADERS, "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email }),
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
if (sendResp.status === 429) {
|
||||
printError("Too many attempts. Try again in a few minutes.");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!sendResp.ok) {
|
||||
let detail: string = sendResp.statusText;
|
||||
try {
|
||||
const errBody = (await sendResp.json()) as { error?: string };
|
||||
if (errBody.error) detail = errBody.error;
|
||||
} catch {
|
||||
/* leave as statusText */
|
||||
}
|
||||
printError(`Failed to send code: ${detail}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
printSuccess(`Verification code sent to ${email}. Check your inbox.`);
|
||||
|
||||
if (!process.stdin.isTTY) {
|
||||
printError(
|
||||
"No --code provided and terminal is non-interactive.",
|
||||
`Re-run: mem0 init --email ${email} --code <code>`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log();
|
||||
code = await promptLine(` ${brand("Verification Code")}`);
|
||||
if (!code) {
|
||||
printError("Code is required.");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: verify + claim atomically
|
||||
const verifyResp = await fetch(`${baseUrl}/api/v1/auth/email_code/verify/`, {
|
||||
method: "POST",
|
||||
headers: { ...SOURCE_HEADERS, "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
code: code.trim(),
|
||||
agent_mode_api_key: rawKey,
|
||||
}),
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
|
||||
if (!verifyResp.ok) {
|
||||
let detail: string = verifyResp.statusText;
|
||||
let errCode = "";
|
||||
try {
|
||||
const errBody = (await verifyResp.json()) as {
|
||||
error?: string;
|
||||
code?: string;
|
||||
};
|
||||
if (errBody.error) detail = errBody.error;
|
||||
if (errBody.code) errCode = errBody.code;
|
||||
} catch {
|
||||
/* leave as statusText */
|
||||
}
|
||||
printError(`Claim failed: ${detail}`);
|
||||
if (errCode === "email_already_claimed") {
|
||||
console.log(
|
||||
` ${dim("Tip: this email already has a Mem0 account. Sign in at app.mem0.ai with your existing credentials.")}`,
|
||||
);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const claimBody = (await verifyResp.json()) as {
|
||||
claimed?: boolean;
|
||||
claimed_at?: string;
|
||||
};
|
||||
if (!claimBody.claimed) {
|
||||
printError(`Unexpected verify response: ${JSON.stringify(claimBody)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
config.platform.agentMode = false;
|
||||
config.platform.claimedAt = claimBody.claimed_at ?? new Date().toISOString();
|
||||
config.platform.userEmail = email;
|
||||
config.platform.createdVia = "email";
|
||||
saveConfig(config);
|
||||
|
||||
printSuccess(`Agent claimed to ${email}. Your API key is unchanged.`);
|
||||
}
|
||||
|
||||
function promptLine(label: string): Promise<string> {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
return new Promise((resolve) => {
|
||||
rl.question(`${label}: `, (answer) => {
|
||||
rl.close();
|
||||
resolve(answer.trim());
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* `mem0 agent-rush <add|search> "..."` — wraps the AGENTRUSH platform endpoints.
|
||||
* Project routing is implicit (server-side); zero flags needed.
|
||||
*/
|
||||
|
||||
import readline from "node:readline";
|
||||
import { colors, printError, printSuccess } from "../branding.js";
|
||||
import { loadConfig, saveConfig } from "../config.js";
|
||||
import { CLI_VERSION } from "../version.js";
|
||||
|
||||
const PII_WARNING = [
|
||||
"",
|
||||
"⚠️ AGENTRUSH memories are PUBLIC — visible to any other player.",
|
||||
" Do not include real names, emails, secrets, work content, or PII.",
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
const ERROR_HINTS: Record<string, string> = {
|
||||
agentrush_search_first:
|
||||
"Run 3 'mem0 agent-rush search' commands before adding.",
|
||||
agentrush_search_quota: "You've used your 3 lifetime searches.",
|
||||
agentrush_add_quota: "You've used your 3 lifetime adds.",
|
||||
agentrush_not_agent_mode:
|
||||
"Re-run 'mem0 init --agent' to bootstrap an agent-mode key.",
|
||||
agentrush_length: "Memory text must be 50-1000 characters.",
|
||||
agentrush_no_urls: "URLs are not allowed.",
|
||||
agentrush_blocklist: "Content contains a blocked term.",
|
||||
agentrush_global_quota: "Event-wide cap reached. Try again later.",
|
||||
agentrush_not_provisioned:
|
||||
"AGENTRUSH is not provisioned in this environment.",
|
||||
};
|
||||
|
||||
async function callEndpoint(
|
||||
path: string,
|
||||
body: Record<string, unknown>,
|
||||
): Promise<unknown> {
|
||||
const config = loadConfig();
|
||||
const baseUrl = (config.platform?.baseUrl ?? "https://api.mem0.ai").replace(
|
||||
/\/+$/,
|
||||
"",
|
||||
);
|
||||
|
||||
if (!config.platform?.apiKey) {
|
||||
printError("Not initialized. Run `mem0 init --agent` first.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const resp = await fetch(`${baseUrl}${path}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Token ${config.platform.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
"X-Mem0-Source": "cli",
|
||||
"X-Mem0-Client-Language": "node",
|
||||
"X-Mem0-Client-Version": CLI_VERSION,
|
||||
"X-Mem0-Mode": "agent-rush",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
|
||||
const json = await resp.json().catch(() => ({}));
|
||||
|
||||
if (!resp.ok) {
|
||||
const code =
|
||||
(json as { error?: { code?: string } }).error?.code ?? "unknown";
|
||||
printError(`AGENTRUSH error: ${code}`);
|
||||
if (ERROR_HINTS[code]) {
|
||||
console.log(` ${colors.dim(ERROR_HINTS[code])}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
function promptLine(question: string): Promise<string> {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer) => {
|
||||
rl.close();
|
||||
resolve(answer.trim());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the human has acknowledged that AGENTRUSH memories are PUBLIC.
|
||||
*
|
||||
* Interactive (TTY): show the prompt; on "y" persist `agentRush.acknowledgedAt`
|
||||
* so we never ask the same machine twice. On anything else, abort.
|
||||
*
|
||||
* Non-interactive (agent invocation, no TTY): print the warning to stderr
|
||||
* for the human reading the agent's transcript and proceed — agents can't
|
||||
* answer y/N prompts.
|
||||
*/
|
||||
async function ensureWarningAcknowledged(): Promise<void> {
|
||||
const config = loadConfig();
|
||||
if (config.agentRush?.acknowledgedAt) return;
|
||||
|
||||
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
||||
// Agent context: surface the warning to stderr, don't block.
|
||||
console.error(PII_WARNING);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(PII_WARNING);
|
||||
const answer = (await promptLine(" Continue? [y/N]: ")).toLowerCase();
|
||||
if (answer !== "y" && answer !== "yes") {
|
||||
printError("Aborted.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
config.agentRush.acknowledgedAt = new Date().toISOString();
|
||||
saveConfig(config);
|
||||
}
|
||||
|
||||
export async function cmdAgentRushAdd(content: string): Promise<void> {
|
||||
await ensureWarningAcknowledged();
|
||||
const result = await callEndpoint("/v1/agent-rush/memories/", { content });
|
||||
printSuccess(
|
||||
`Memory submitted (event_id: ${(result as { event_id?: string }).event_id ?? "?"})`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function cmdAgentRushSearch(query: string): Promise<void> {
|
||||
const result = (await callEndpoint("/v1/agent-rush/memories/search/", {
|
||||
query,
|
||||
})) as {
|
||||
results?: Array<{ memory?: string }>;
|
||||
memories?: Array<{ memory?: string }>;
|
||||
};
|
||||
|
||||
const memories = result.results ?? result.memories ?? [];
|
||||
|
||||
if (memories.length === 0) {
|
||||
console.log(colors.dim("(no results)"));
|
||||
return;
|
||||
}
|
||||
|
||||
memories.slice(0, 5).forEach((m, i) => {
|
||||
console.log(` ${i + 1}. ${m.memory ?? JSON.stringify(m)}`);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* mem0 identify — declare which agent owns the current agent-mode key.
|
||||
*
|
||||
* Used when `mem0 init --agent` ran without --agent-caller, so the backend
|
||||
* saved agent_caller=NULL. The agent re-runs `mem0 identify <name>` to PATCH
|
||||
* its own row with its real identity. Idempotent.
|
||||
*/
|
||||
|
||||
import { printError, printSuccess } from "../branding.js";
|
||||
import { loadConfig, saveConfig } from "../config.js";
|
||||
|
||||
const SOURCE_HEADERS = {
|
||||
"X-Mem0-Source": "cli",
|
||||
"X-Mem0-Client-Language": "node",
|
||||
} as const;
|
||||
|
||||
export async function runIdentify(name: string): Promise<void> {
|
||||
const config = loadConfig();
|
||||
if (!config.platform.apiKey) {
|
||||
printError("No API key configured. Run `mem0 init --agent` first.");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!config.platform.agentMode) {
|
||||
printError("This command only works on unclaimed agent-mode keys.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const clean = (name ?? "").trim();
|
||||
if (!clean) {
|
||||
printError("Agent name is required.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const baseUrl = (config.platform.baseUrl || "https://api.mem0.ai").replace(
|
||||
/\/+$/,
|
||||
"",
|
||||
);
|
||||
|
||||
let resp: Response;
|
||||
try {
|
||||
resp = await fetch(`${baseUrl}/api/v1/auth/agent_mode/caller/`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
...SOURCE_HEADERS,
|
||||
Authorization: `Token ${config.platform.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ agent_caller: clean }),
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
} catch (err) {
|
||||
printError(
|
||||
`Network error: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!resp.ok) {
|
||||
let detail: string = resp.statusText;
|
||||
try {
|
||||
const body = (await resp.json()) as { error?: string };
|
||||
if (body.error) detail = body.error;
|
||||
} catch {
|
||||
/* leave as statusText */
|
||||
}
|
||||
printError(`Identify failed: ${detail}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const body = (await resp.json()) as { agent_caller?: string };
|
||||
const canonical = body.agent_caller ?? clean;
|
||||
config.platform.agentCaller = canonical;
|
||||
saveConfig(config);
|
||||
printSuccess(`Identified as ${canonical}.`);
|
||||
}
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
redactKey,
|
||||
saveConfig,
|
||||
} from "../config.js";
|
||||
import { formatJsonEnvelope } from "../output.js";
|
||||
import { isAgentMode } from "../state.js";
|
||||
|
||||
const { brand, dim } = colors;
|
||||
|
||||
@@ -33,6 +35,65 @@ function validateEmail(email: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal — exported for unit tests. */
|
||||
export async function pingKey(
|
||||
apiKey: string,
|
||||
baseUrl: string,
|
||||
timeoutMs = 5000,
|
||||
): Promise<boolean> {
|
||||
// Returns false ONLY on a definitive "invalid key" signal (HTTP 401/403).
|
||||
// Network errors, timeouts, and 5xx responses return true so we prefer
|
||||
// reusing an existing key over silently minting a new shadow on a transient
|
||||
// blip (which would also clobber config + plugin-sync targets).
|
||||
try {
|
||||
const resp = await fetch(`${baseUrl.replace(/\/+$/, "")}/v1/ping/`, {
|
||||
headers: { Authorization: `Token ${apiKey}` },
|
||||
signal: AbortSignal.timeout(timeoutMs),
|
||||
});
|
||||
return resp.status !== 401 && resp.status !== 403;
|
||||
} catch {
|
||||
return true; // unknown — prefer reuse
|
||||
}
|
||||
}
|
||||
|
||||
async function maybeIdentify(
|
||||
key: string,
|
||||
baseUrl: string,
|
||||
agentCaller: string | undefined,
|
||||
): Promise<void> {
|
||||
// Best-effort PATCH agent_caller when --agent-caller is supplied on a
|
||||
// reused key. Silent no-op on any failure — reuse must not break.
|
||||
if (!agentCaller) return;
|
||||
try {
|
||||
const resp = await fetch(
|
||||
`${baseUrl.replace(/\/+$/, "")}/api/v1/auth/agent_mode/caller/`,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
Authorization: `Token ${key}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ agent_caller: agentCaller }),
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
},
|
||||
);
|
||||
if (resp.ok) {
|
||||
try {
|
||||
const body = (await resp.json()) as { agent_caller?: string };
|
||||
if (fs.existsSync(CONFIG_FILE)) {
|
||||
const cfg = loadConfig();
|
||||
cfg.platform.agentCaller = body.agent_caller ?? agentCaller;
|
||||
saveConfig(cfg);
|
||||
}
|
||||
} catch {
|
||||
/* swallow — best effort */
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* swallow — best effort */
|
||||
}
|
||||
}
|
||||
|
||||
async function emailLogin(
|
||||
email: string,
|
||||
code: string | undefined,
|
||||
@@ -196,6 +257,7 @@ async function setupPlatform(config: Mem0Config): Promise<void> {
|
||||
process.exit(1);
|
||||
}
|
||||
config.platform.apiKey = apiKey;
|
||||
config.platform.createdVia = "api_key";
|
||||
}
|
||||
|
||||
async function setupDefaults(config: Mem0Config): Promise<void> {
|
||||
@@ -249,14 +311,35 @@ export async function runInit(
|
||||
email?: string;
|
||||
code?: string;
|
||||
force?: boolean;
|
||||
agent?: boolean;
|
||||
source?: string;
|
||||
agentCaller?: string;
|
||||
} = {},
|
||||
): Promise<void> {
|
||||
const { detectAgentCaller } = await import("../agent-detect.js");
|
||||
const { bootstrapViaBackend, claimViaOtp } = await import("./agent-mode.js");
|
||||
const { isAgentMode } = await import("../state.js");
|
||||
const { captureEvent } = await import("../telemetry.js");
|
||||
|
||||
const fireInit = (
|
||||
mode: "agent" | "email" | "api_key" | "existing_key",
|
||||
claimed = false,
|
||||
) => {
|
||||
const props: Record<string, unknown> = { command: "init", mode };
|
||||
// Self-declared via --agent-caller; not sniffed from env vars.
|
||||
if (opts.agentCaller) props.agent_caller = opts.agentCaller;
|
||||
if (opts.source) props.signup_source = opts.source;
|
||||
if (claimed) props.claimed_agent_mode = true;
|
||||
captureEvent("cli.init", props);
|
||||
};
|
||||
|
||||
const config = createDefaultConfig();
|
||||
const savedConfig = loadConfig();
|
||||
const baseUrl =
|
||||
process.env.MEM0_BASE_URL ||
|
||||
savedConfig.platform.baseUrl ||
|
||||
DEFAULT_BASE_URL;
|
||||
config.platform.baseUrl = baseUrl;
|
||||
|
||||
// Guards
|
||||
if (opts.code && !opts.email) {
|
||||
@@ -268,6 +351,84 @@ export async function runInit(
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ── Claim flow: --email against an existing agent-mode config ───────────
|
||||
if (
|
||||
opts.email &&
|
||||
fs.existsSync(CONFIG_FILE) &&
|
||||
savedConfig.platform.agentMode &&
|
||||
savedConfig.platform.apiKey
|
||||
) {
|
||||
const email = opts.email.trim().toLowerCase();
|
||||
validateEmail(email);
|
||||
printInfo(`Claiming Agent Mode account to ${email}...`);
|
||||
await claimViaOtp(savedConfig, { email, code: opts.code });
|
||||
fireInit("email", true);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Agent Mode path runs BEFORE the existing-config guard ──────────────
|
||||
// Rule 1/2 will REUSE a valid existing key (not overwrite), so we must
|
||||
// short-circuit before the guard prompts the user about overwriting.
|
||||
// Rule 3 only mints when there's no valid key to reuse — in that case
|
||||
// overwriting is what the user wants.
|
||||
const agentCtx =
|
||||
opts.agent === true || isAgentMode() || detectAgentCaller() !== null;
|
||||
if (!opts.apiKey && !opts.email && agentCtx) {
|
||||
const emitReuseEnvelope = (source: "env" | "config") => {
|
||||
if (isAgentMode()) {
|
||||
formatJsonEnvelope({
|
||||
command: "init",
|
||||
data: {
|
||||
api_key_saved: false,
|
||||
api_key_source: source,
|
||||
agent_mode: false,
|
||||
message:
|
||||
"Existing Mem0 API key found and reused. No Agent Mode key was created.",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
printSuccess(
|
||||
source === "env"
|
||||
? "Existing MEM0_API_KEY is valid; reusing it. No new Agent Mode key was minted."
|
||||
: "Existing API key in config is valid; reusing it. No new Agent Mode key was minted.",
|
||||
);
|
||||
}
|
||||
};
|
||||
// Rule 1: env MEM0_API_KEY valid → reuse, no new key.
|
||||
const envKey = (process.env.MEM0_API_KEY || "").trim();
|
||||
if (envKey && (await pingKey(envKey, baseUrl))) {
|
||||
await maybeIdentify(envKey, baseUrl, opts.agentCaller);
|
||||
emitReuseEnvelope("env");
|
||||
fireInit("existing_key");
|
||||
return;
|
||||
}
|
||||
// Rule 2: existing config api_key valid → reuse.
|
||||
if (
|
||||
savedConfig.platform.apiKey &&
|
||||
(await pingKey(savedConfig.platform.apiKey, baseUrl))
|
||||
) {
|
||||
await maybeIdentify(
|
||||
savedConfig.platform.apiKey,
|
||||
baseUrl,
|
||||
opts.agentCaller,
|
||||
);
|
||||
emitReuseEnvelope("config");
|
||||
fireInit("existing_key");
|
||||
return;
|
||||
}
|
||||
// Rule 3: mint a fresh shadow (no valid key to reuse).
|
||||
// agent_caller is self-declared via --agent-caller (Proof Editor-style),
|
||||
// not derived from env-var sniffing. detectAgentCaller() above is still
|
||||
// used as a context trigger (does this look like an agent?) but never
|
||||
// to fill identity.
|
||||
await bootstrapViaBackend(config, {
|
||||
source: opts.source ?? null,
|
||||
agentCaller: opts.agentCaller ?? null,
|
||||
});
|
||||
fireInit("agent");
|
||||
return;
|
||||
}
|
||||
|
||||
// Warn if an existing config with an API key would be overwritten
|
||||
if (
|
||||
!opts.force &&
|
||||
@@ -324,6 +485,7 @@ export async function runInit(
|
||||
config.platform.apiKey = apiKeyVal;
|
||||
config.platform.baseUrl = baseUrl;
|
||||
config.platform.userEmail = email;
|
||||
config.platform.createdVia = "email";
|
||||
config.defaults.userId =
|
||||
opts.userId || process.env.USER || process.env.USERNAME || "mem0-cli";
|
||||
|
||||
@@ -339,13 +501,15 @@ export async function runInit(
|
||||
}
|
||||
|
||||
// ── API key flow ──────────────────────────────────────────────────────────
|
||||
// (Agent Mode branch runs earlier — see above, before the existing-config
|
||||
// guard, so Rules 1/2 can REUSE a valid key without prompting overwrite.)
|
||||
|
||||
// Non-TTY: resolve defaults so partial flags work in pipelines / CI
|
||||
if (!process.stdin.isTTY) {
|
||||
if (!opts.apiKey) {
|
||||
printError(
|
||||
"Non-interactive terminal detected and --api-key is required.",
|
||||
"Usage: mem0 init --api-key <key> [--user-id <id>]",
|
||||
"Usage: mem0 init --api-key <key>, --email <addr>, or --agent for unattended Agent Mode bootstrap.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -356,6 +520,7 @@ export async function runInit(
|
||||
// Non-interactive: both flags provided
|
||||
if (opts.apiKey && opts.userId) {
|
||||
config.platform.apiKey = opts.apiKey;
|
||||
config.platform.createdVia = "api_key";
|
||||
config.defaults.userId = opts.userId;
|
||||
await validatePlatform(config);
|
||||
saveConfig(config);
|
||||
@@ -403,6 +568,7 @@ export async function runInit(
|
||||
config.platform.apiKey = apiKeyVal;
|
||||
config.platform.baseUrl = baseUrl;
|
||||
config.platform.userEmail = email;
|
||||
config.platform.createdVia = "email";
|
||||
config.defaults.userId =
|
||||
opts.userId || process.env.USER || process.env.USERNAME || "mem0-cli";
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* `mem0 whoami` — print the active agent's default_user_id (AGENTRUSH identifier).
|
||||
* Reads from local config; no network call.
|
||||
*/
|
||||
|
||||
import { colors, printError, printInfo } from "../branding.js";
|
||||
import { loadConfig } from "../config.js";
|
||||
|
||||
export async function cmdWhoami(): Promise<void> {
|
||||
const config = loadConfig();
|
||||
const sessionId = config.platform?.defaultUserId;
|
||||
if (!sessionId) {
|
||||
printError("No default_user_id found. Run `mem0 init --agent` first.");
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`Your AGENTRUSH identifier: ${colors.brand(sessionId)}`);
|
||||
printInfo("Find your row at https://mem0.ai/agentrush");
|
||||
}
|
||||
@@ -21,6 +21,12 @@ export interface PlatformConfig {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
userEmail: string;
|
||||
// Agent Mode (unclaimed-shadow signup)
|
||||
agentMode: boolean; // true while the key is an unclaimed agent-mode key
|
||||
createdVia: string; // "agent_mode" | "email" | "api_key" | "existing_key"
|
||||
agentCaller: string; // canonical agent name when createdVia === "agent_mode" (e.g. "claude-code")
|
||||
claimedAt: string; // ISO timestamp once the agent has been claimed
|
||||
defaultUserId: string; // `user_<slug>` returned by bootstrap; auto-default scope
|
||||
}
|
||||
|
||||
export interface DefaultsConfig {
|
||||
@@ -34,11 +40,18 @@ export interface TelemetryConfig {
|
||||
anonymousId: string;
|
||||
}
|
||||
|
||||
export interface AgentRushConfig {
|
||||
// ISO timestamp the human acknowledged the "memories are public" warning.
|
||||
// Empty until first interactive `mem0 agent-rush add`.
|
||||
acknowledgedAt: string;
|
||||
}
|
||||
|
||||
export interface Mem0Config {
|
||||
version: number;
|
||||
defaults: DefaultsConfig;
|
||||
platform: PlatformConfig;
|
||||
telemetry: TelemetryConfig;
|
||||
agentRush: AgentRushConfig;
|
||||
}
|
||||
|
||||
export function createDefaultConfig(): Mem0Config {
|
||||
@@ -54,10 +67,18 @@ export function createDefaultConfig(): Mem0Config {
|
||||
apiKey: "",
|
||||
baseUrl: DEFAULT_BASE_URL,
|
||||
userEmail: "",
|
||||
agentMode: false,
|
||||
createdVia: "",
|
||||
agentCaller: "",
|
||||
claimedAt: "",
|
||||
defaultUserId: "",
|
||||
},
|
||||
telemetry: {
|
||||
anonymousId: "",
|
||||
},
|
||||
agentRush: {
|
||||
acknowledgedAt: "",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -79,6 +100,11 @@ export function loadConfig(): Mem0Config {
|
||||
config.platform.apiKey = plat.api_key ?? "";
|
||||
config.platform.baseUrl = plat.base_url ?? DEFAULT_BASE_URL;
|
||||
config.platform.userEmail = plat.user_email ?? "";
|
||||
config.platform.agentMode = Boolean(plat.agent_mode ?? false);
|
||||
config.platform.createdVia = plat.created_via ?? "";
|
||||
config.platform.agentCaller = plat.agent_caller ?? "";
|
||||
config.platform.claimedAt = plat.claimed_at ?? "";
|
||||
config.platform.defaultUserId = plat.default_user_id ?? "";
|
||||
|
||||
const defaults = data.defaults ?? {};
|
||||
config.defaults.userId = defaults.user_id ?? "";
|
||||
@@ -87,6 +113,8 @@ export function loadConfig(): Mem0Config {
|
||||
config.defaults.runId = defaults.run_id ?? "";
|
||||
const telemetry = data.telemetry ?? {};
|
||||
config.telemetry.anonymousId = telemetry.anonymous_id ?? "";
|
||||
const agentRush = data.agent_rush ?? {};
|
||||
config.agentRush.acknowledgedAt = agentRush.acknowledged_at ?? "";
|
||||
}
|
||||
|
||||
// Environment variable overrides
|
||||
@@ -118,14 +146,36 @@ export function saveConfig(config: Mem0Config): void {
|
||||
api_key: config.platform.apiKey,
|
||||
base_url: config.platform.baseUrl,
|
||||
user_email: config.platform.userEmail,
|
||||
agent_mode: config.platform.agentMode,
|
||||
created_via: config.platform.createdVia,
|
||||
agent_caller: config.platform.agentCaller,
|
||||
claimed_at: config.platform.claimedAt,
|
||||
default_user_id: config.platform.defaultUserId,
|
||||
},
|
||||
telemetry: {
|
||||
anonymous_id: config.telemetry.anonymousId,
|
||||
},
|
||||
agent_rush: {
|
||||
acknowledged_at: config.agentRush.acknowledgedAt,
|
||||
},
|
||||
};
|
||||
|
||||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2));
|
||||
fs.chmodSync(CONFIG_FILE, 0o600);
|
||||
|
||||
// Propagate api_key to ecosystem touchpoints (Claude plugin env injection,
|
||||
// shell rc exports). Idempotent — updates only EXISTING entries; never
|
||||
// creates new ones. Best-effort: errors swallowed so config.json is
|
||||
// always authoritative, never blocked by plugin-state issues.
|
||||
if (config.platform.apiKey) {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { syncApiKey } = require("./plugin-sync.js");
|
||||
syncApiKey(config.platform.apiKey);
|
||||
} catch {
|
||||
/* swallow */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function redactKey(key: string): string {
|
||||
|
||||
+112
-4
@@ -13,7 +13,12 @@ import { colors, printError, printWarning } from "./branding.js";
|
||||
import type { Mem0Config } from "./config.js";
|
||||
import { loadConfig, saveConfig } from "./config.js";
|
||||
import { richFormatHelp } from "./help.js";
|
||||
import { setAgentMode } from "./state.js";
|
||||
import {
|
||||
isAgentMode,
|
||||
setAgentMode,
|
||||
setCurrentCommand,
|
||||
takeNotice,
|
||||
} from "./state.js";
|
||||
import { captureEvent } from "./telemetry.js";
|
||||
import { CLI_VERSION } from "./version.js";
|
||||
|
||||
@@ -141,6 +146,11 @@ program
|
||||
.description(
|
||||
`◆ Mem0 CLI v${CLI_VERSION} · Node.js SDK\n\nThe Memory Layer for AI Agents`,
|
||||
)
|
||||
// Positional options: flags AFTER a subcommand name belong to that
|
||||
// subcommand, not the global program. Without this, `mem0 init --agent`
|
||||
// routes `--agent` to the program-level alias (for --json) and init's own
|
||||
// `--agent` (Agent Mode bootstrap) silently never fires.
|
||||
.enablePositionalOptions()
|
||||
.option("--version", "Show version and exit.")
|
||||
.on("option:version", () => {
|
||||
console.log(` ${colors.brand("◆ Mem0")} CLI v${CLI_VERSION}`);
|
||||
@@ -149,7 +159,7 @@ program
|
||||
.option("--json", "Output as JSON for agent/programmatic use.")
|
||||
.option(
|
||||
"--agent",
|
||||
"Output as JSON for agent/programmatic use. (alias: --json)",
|
||||
"Output as JSON for agent/programmatic use. (alias: --json) Place BEFORE the subcommand: `mem0 --agent <cmd>`. On `init`, `mem0 init --agent` is the Agent Mode bootstrap flag instead.",
|
||||
)
|
||||
.usage("<command> [options]")
|
||||
.helpOption("--help", "Show this message and exit.")
|
||||
@@ -166,6 +176,14 @@ program.hook("preAction", (_thisCommand, actionCommand) => {
|
||||
parentName && parentName !== "mem0"
|
||||
? `${parentName}.${commandName}`
|
||||
: commandName;
|
||||
// Stash the active command name in shared state so the JSON
|
||||
// error envelope (printError) can report which command failed
|
||||
// instead of an empty `"command": ""` field.
|
||||
setCurrentCommand(fullCommand);
|
||||
// init fires its own telemetry from runInit with full M1-M6 props
|
||||
// (mode/agent_caller/signup_source/claimed_agent_mode); skip the
|
||||
// auto-fire here so we don't double-count.
|
||||
if (fullCommand === "init") return;
|
||||
const isAgent = !!(program.opts().json || program.opts().agent);
|
||||
captureEvent(
|
||||
`cli.${fullCommand}`,
|
||||
@@ -193,11 +211,32 @@ program
|
||||
"Verification code (use with --email for non-interactive login).",
|
||||
)
|
||||
.option("--force", "Overwrite existing config without confirmation.", false)
|
||||
.option(
|
||||
"--agent",
|
||||
"Bootstrap an unattended Agent Mode account (no email required).",
|
||||
false,
|
||||
)
|
||||
.option(
|
||||
"--source <channel>",
|
||||
"Channel attribution for signup (e.g. github, hn, ph).",
|
||||
)
|
||||
.option(
|
||||
"--agent-caller <name>",
|
||||
"Self-declared agent identity (e.g. claude-code, cursor). Used with --agent to attribute Agent Mode signups.",
|
||||
)
|
||||
// Accept `--json` at the init level too so the PRD-documented form
|
||||
// `mem0 init --agent --json` works without requiring users to move it
|
||||
// before the subcommand. Effect is identical to the global `--json`:
|
||||
// flip agent-mode output state.
|
||||
.option("--json", "Output as JSON (alias for global `--json`).", false)
|
||||
.addHelpText(
|
||||
"after",
|
||||
"\nExamples:\n $ mem0 init\n $ mem0 init --api-key m0-xxx --user-id alice\n $ mem0 init --email you@example.com\n $ mem0 init --email you@example.com --code 123456",
|
||||
"\nExamples:\n $ mem0 init\n $ mem0 init --api-key m0-xxx --user-id alice\n $ mem0 init --email you@example.com\n $ mem0 init --email you@example.com --code 123456\n $ mem0 init --agent # Bootstrap an Agent Mode account (unattended)\n $ mem0 init --email you@example.com # Claims an existing Agent Mode key when one is present",
|
||||
)
|
||||
.action(async (opts) => {
|
||||
// `--json` at init level mirrors the global flag — flip agent_mode
|
||||
// state so downstream formatters use JSON envelopes.
|
||||
if (opts.json) setAgentMode(true);
|
||||
const { runInit } = await import("./commands/init.js");
|
||||
await runInit({
|
||||
apiKey: opts.apiKey,
|
||||
@@ -205,9 +244,66 @@ program
|
||||
email: opts.email,
|
||||
code: opts.code,
|
||||
force: opts.force,
|
||||
agent: opts.agent,
|
||||
source: opts.source,
|
||||
agentCaller: opts.agentCaller,
|
||||
});
|
||||
});
|
||||
|
||||
// ── Setup: identify (post-bootstrap agent self-tag) ──────────────────────
|
||||
|
||||
program
|
||||
.command("identify <name>")
|
||||
.description(
|
||||
"Tag your active Agent Mode key with the AI agent that's using it (e.g. claude-code, cursor).",
|
||||
)
|
||||
.action(async (name: string) => {
|
||||
const { runIdentify } = await import("./commands/identify.js");
|
||||
await runIdentify(name);
|
||||
});
|
||||
|
||||
// ── Setup: whoami (print active agent identifier) ────────────────────────
|
||||
|
||||
program
|
||||
.command("whoami")
|
||||
.description("Print the active agent's AGENTRUSH identifier.")
|
||||
.action(async () => {
|
||||
const { cmdWhoami } = await import("./commands/whoami.js");
|
||||
await cmdWhoami();
|
||||
});
|
||||
|
||||
// ── AGENTRUSH subcommand group ────────────────────────────────────────────
|
||||
|
||||
const agentRush = program
|
||||
.command("agent-rush")
|
||||
.description("AGENTRUSH game commands.")
|
||||
.addHelpCommand(false)
|
||||
.configureHelp({ formatHelp: richFormatHelp });
|
||||
|
||||
agentRush
|
||||
.command("add <content...>")
|
||||
.description("Submit a memory to AGENTRUSH.")
|
||||
.addHelpText(
|
||||
"after",
|
||||
'\nExamples:\n $ mem0 agent-rush add "I used mem0 to build a coding agent"\n $ mem0 agent-rush add "Agents that remember are better agents"',
|
||||
)
|
||||
.action(async (parts: string[]) => {
|
||||
const { cmdAgentRushAdd } = await import("./commands/agent-rush.js");
|
||||
await cmdAgentRushAdd(parts.join(" "));
|
||||
});
|
||||
|
||||
agentRush
|
||||
.command("search <query...>")
|
||||
.description("Search AGENTRUSH memories.")
|
||||
.addHelpText(
|
||||
"after",
|
||||
'\nExamples:\n $ mem0 agent-rush search "agents and memory and tools"\n $ mem0 agent-rush search "coding assistant"',
|
||||
)
|
||||
.action(async (parts: string[]) => {
|
||||
const { cmdAgentRushSearch } = await import("./commands/agent-rush.js");
|
||||
await cmdAgentRushSearch(parts.join(" "));
|
||||
});
|
||||
|
||||
// ── Memory: add ───────────────────────────────────────────────────────────
|
||||
|
||||
program
|
||||
@@ -769,4 +865,16 @@ program
|
||||
|
||||
// ── Entrypoint ────────────────────────────────────────────────────────────
|
||||
|
||||
program.parse();
|
||||
// Surface any unclaimed Agent Mode notice once per command, after the primary
|
||||
// output. In JSON/agent mode the notice is folded into the envelope by
|
||||
// formatJsonEnvelope, so skip the stderr banner there to avoid duplication.
|
||||
function surfaceNotice(): void {
|
||||
const notice = takeNotice();
|
||||
if (notice && !isAgentMode()) {
|
||||
process.stderr.write(`\n\x1b[33m🔔 ${notice}\x1b[0m\n\n`);
|
||||
}
|
||||
}
|
||||
|
||||
program.parseAsync().finally(() => {
|
||||
surfaceNotice();
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import boxen from "boxen";
|
||||
import Table from "cli-table3";
|
||||
import { colors, sym } from "./branding.js";
|
||||
import { takeNotice } from "./state.js";
|
||||
|
||||
const { brand, accent, success, error: errorColor, dim } = colors;
|
||||
|
||||
@@ -244,6 +245,15 @@ export function formatJsonEnvelope(opts: {
|
||||
if (opts.count !== undefined) envelope.count = opts.count;
|
||||
if (opts.error) envelope.error = opts.error;
|
||||
envelope.data = opts.data;
|
||||
|
||||
// If the platform flagged this as an unclaimed Agent Mode account, surface
|
||||
// the notice inside the JSON envelope so an agent consuming the output
|
||||
// sees it without needing to inspect HTTP headers.
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { takeNotice } = require("./state.js");
|
||||
const notice = takeNotice();
|
||||
if (notice) envelope.mem0_notice = notice;
|
||||
|
||||
console.log(JSON.stringify(envelope, null, 2));
|
||||
}
|
||||
|
||||
@@ -356,6 +366,12 @@ export function formatAgentEnvelope(opts: {
|
||||
}
|
||||
if (opts.count !== undefined) envelope.count = opts.count;
|
||||
envelope.data = sanitizeAgentData(opts.command, opts.data);
|
||||
|
||||
// Surface the unclaimed-Agent-Mode notice (if any) in the envelope so an
|
||||
// agent reading the JSON output sees it without inspecting HTTP headers.
|
||||
const notice = takeNotice();
|
||||
if (notice) envelope.mem0_notice = notice;
|
||||
|
||||
console.log(JSON.stringify(envelope, null, 2));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Sync the active Mem0 API key into other ecosystem touchpoints.
|
||||
*
|
||||
* Why: the CLI canonical state is ~/.mem0/config.json. MCP servers
|
||||
* (Claude Code plugin, Codex plugin) read MEM0_API_KEY from env or
|
||||
* their own config files. Without a sync, agent-mode bootstrap mints a
|
||||
* new key into config.json but the plugin's MCP keeps using the old
|
||||
* key from env — silent surprise.
|
||||
*
|
||||
* Design:
|
||||
* - Update ONLY entries that already exist; never create new ones
|
||||
* - Preserve surrounding content, formatting, other keys
|
||||
* - Atomic writes (tmp + rename) so a crash mid-write doesn't corrupt
|
||||
* - Idempotent — re-running with the same key is a no-op
|
||||
*
|
||||
* Targets:
|
||||
* - ~/.claude/settings.json::env::MEM0_API_KEY (Claude Code env injection)
|
||||
* - ~/.zshrc / ~/.bashrc `export MEM0_API_KEY="..."` lines
|
||||
*
|
||||
* Out of scope: Codex / Cursor MCP configs and the plugin's own
|
||||
* <plugin-dir>/.api_key file (plugin-managed, different schema).
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const CLAUDE_SETTINGS = path.join(os.homedir(), ".claude", "settings.json");
|
||||
const SHELL_RCS = [
|
||||
path.join(os.homedir(), ".zshrc"),
|
||||
path.join(os.homedir(), ".bashrc"),
|
||||
path.join(os.homedir(), ".bash_profile"),
|
||||
];
|
||||
|
||||
// Use [ \t]* (not \s*) so a trailing newline at end-of-file is preserved
|
||||
// when the MEM0_API_KEY export is the last line of the rc file.
|
||||
const RC_LINE_RE =
|
||||
/^([ \t]*export[ \t]+MEM0_API_KEY[ \t]*=[ \t]*)(["']?)([^"'\n]*)(["']?)[ \t]*$/m;
|
||||
|
||||
export function syncApiKey(apiKey: string): string[] {
|
||||
if (!apiKey) return [];
|
||||
const updated: string[] = [];
|
||||
if (updateClaudeSettings(CLAUDE_SETTINGS, apiKey)) {
|
||||
updated.push(CLAUDE_SETTINGS);
|
||||
}
|
||||
for (const rc of SHELL_RCS) {
|
||||
if (updateShellRc(rc, apiKey)) updated.push(rc);
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
/** @internal — exported for unit tests; consumers should use {@link syncApiKey}. */
|
||||
export function updateClaudeSettings(
|
||||
filePath: string,
|
||||
apiKey: string,
|
||||
): boolean {
|
||||
if (!fs.existsSync(filePath)) return false;
|
||||
let raw: string;
|
||||
let data: Record<string, unknown>;
|
||||
try {
|
||||
raw = fs.readFileSync(filePath, "utf-8");
|
||||
data = JSON.parse(raw);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
const env = data.env;
|
||||
if (!env || typeof env !== "object" || !("MEM0_API_KEY" in env)) {
|
||||
return false; // no existing entry — don't create one
|
||||
}
|
||||
const envObj = env as Record<string, string>;
|
||||
if (envObj.MEM0_API_KEY === apiKey) return false; // already in sync
|
||||
envObj.MEM0_API_KEY = apiKey;
|
||||
atomicWriteText(filePath, `${JSON.stringify(data, null, 2)}\n`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @internal — exported for unit tests; consumers should use {@link syncApiKey}. */
|
||||
export function updateShellRc(filePath: string, apiKey: string): boolean {
|
||||
if (!fs.existsSync(filePath)) return false;
|
||||
let text: string;
|
||||
try {
|
||||
text = fs.readFileSync(filePath, "utf-8");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
const match = text.match(RC_LINE_RE);
|
||||
if (!match) return false; // no existing line
|
||||
if (match[3] === apiKey) return false;
|
||||
const newText = text.replace(
|
||||
RC_LINE_RE,
|
||||
(_full, prefix) => `${prefix}"${apiKey}"`,
|
||||
);
|
||||
atomicWriteText(filePath, newText);
|
||||
return true;
|
||||
}
|
||||
|
||||
function atomicWriteText(filePath: string, content: string): void {
|
||||
const dir = path.dirname(filePath);
|
||||
const tmp = path.join(dir, `.${path.basename(filePath)}.${process.pid}.tmp`);
|
||||
try {
|
||||
fs.writeFileSync(tmp, content, "utf-8");
|
||||
// Preserve permissions if original existed.
|
||||
if (fs.existsSync(filePath)) {
|
||||
try {
|
||||
const mode = fs.statSync(filePath).mode & 0o777;
|
||||
fs.chmodSync(tmp, mode);
|
||||
} catch {
|
||||
/* best-effort */
|
||||
}
|
||||
}
|
||||
fs.renameSync(tmp, filePath);
|
||||
} catch (err) {
|
||||
try {
|
||||
fs.unlinkSync(tmp);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
let _agentMode = false;
|
||||
let _currentCommand = "";
|
||||
let _pendingNotice = "";
|
||||
|
||||
export function isAgentMode(): boolean {
|
||||
return _agentMode;
|
||||
@@ -21,3 +22,19 @@ export function getCurrentCommand(): string {
|
||||
export function setCurrentCommand(name: string): void {
|
||||
_currentCommand = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stash a Mem0 backend notice (Agent Mode unclaimed reminder) for end-of-
|
||||
* command surfacing. Called from the platform backend after each response so
|
||||
* the notice prints once per command regardless of how many sub-requests
|
||||
* fired. Last-write-wins is fine — the message text is identical.
|
||||
*/
|
||||
export function captureNotice(notice: string | null | undefined): void {
|
||||
if (notice) _pendingNotice = notice;
|
||||
}
|
||||
|
||||
export function takeNotice(): string {
|
||||
const msg = _pendingNotice;
|
||||
_pendingNotice = "";
|
||||
return msg;
|
||||
}
|
||||
|
||||
@@ -115,6 +115,9 @@ export function captureEvent(
|
||||
}
|
||||
}
|
||||
|
||||
// M4: every cli.* event carries agent_mode based on the config flag
|
||||
// (unclaimed Agent Mode key). This is the growth-doc property used to
|
||||
// join init → add → search funnels in PostHog.
|
||||
const payload = {
|
||||
api_key: POSTHOG_API_KEY,
|
||||
distinct_id: distinctId,
|
||||
@@ -123,6 +126,7 @@ export function captureEvent(
|
||||
source: "CLI",
|
||||
language: "node",
|
||||
cli_version: CLI_VERSION,
|
||||
agent_mode: Boolean(config.platform.agentMode),
|
||||
node_version: process.version,
|
||||
os: process.platform,
|
||||
...properties,
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Parity tests for `mem0 init --agent` (Agent Mode bootstrap).
|
||||
*
|
||||
* Mirror of `cli/python/tests/test_agent_mode.py` — both files MUST stay
|
||||
* in sync so that the Python and Node CLIs expose an identical surface
|
||||
* for the Agent Mode entrypoint. If you add a flag here, add the same
|
||||
* assertion on the Python side (and vice versa).
|
||||
*
|
||||
* Network-bound bootstrap is covered by the platform-side E2E suite
|
||||
* (`backend/tests/e2e/test_05_agent_mode.py`); these tests only verify
|
||||
* the CLI surface that ships in the binary.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { execSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
function run(
|
||||
args: string[],
|
||||
opts: { home?: string; env?: Record<string, string> } = {},
|
||||
): { stdout: string; stderr: string; exitCode: number } {
|
||||
const env = { ...process.env };
|
||||
for (const key of Object.keys(env)) {
|
||||
if (key.startsWith("MEM0_")) delete env[key];
|
||||
}
|
||||
if (opts.home) env.HOME = opts.home;
|
||||
if (opts.env) Object.assign(env, opts.env);
|
||||
|
||||
try {
|
||||
const stdout = execSync(`npx tsx src/index.ts ${args.join(" ")}`, {
|
||||
cwd: path.join(__dirname, ".."),
|
||||
env,
|
||||
encoding: "utf-8",
|
||||
timeout: 15000,
|
||||
});
|
||||
return { stdout, stderr: "", exitCode: 0 };
|
||||
} catch (e: any) {
|
||||
return {
|
||||
stdout: e.stdout ?? "",
|
||||
stderr: e.stderr ?? "",
|
||||
exitCode: e.status ?? 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function cleanHome(): string {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), "mem0-test-"));
|
||||
}
|
||||
|
||||
describe("init flag surface", () => {
|
||||
it("init --help lists --agent", () => {
|
||||
const result = run(["init", "--help"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("--agent");
|
||||
});
|
||||
|
||||
it("init --help describes Agent Mode", () => {
|
||||
const result = run(["init", "--help"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
// Description must mention what --agent actually does so an agent
|
||||
// reading the help can self-discover the bootstrap entrypoint.
|
||||
expect(
|
||||
result.stdout.includes("Agent Mode") ||
|
||||
result.stdout.toLowerCase().includes("unattended"),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("init --help lists --source", () => {
|
||||
const result = run(["init", "--help"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("--source");
|
||||
});
|
||||
|
||||
it("init --help lists --email and --code", () => {
|
||||
const result = run(["init", "--help"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("--email");
|
||||
expect(result.stdout).toContain("--code");
|
||||
});
|
||||
});
|
||||
|
||||
describe("argv preprocessing — --agent reaches init subcommand", () => {
|
||||
// Regression for the bug where the global --agent JSON-alias swallowed
|
||||
// the init-level --agent flag, making `mem0 init --agent` behave like
|
||||
// the plain interactive wizard.
|
||||
|
||||
it("init --agent triggers bootstrap branch (not the wizard)", () => {
|
||||
const home = cleanHome();
|
||||
const result = run(["init", "--agent"], {
|
||||
home,
|
||||
env: {
|
||||
MEM0_BASE_URL: "http://127.0.0.1:1", // blackhole
|
||||
FORCE_COLOR: "0",
|
||||
},
|
||||
});
|
||||
const combined = (result.stdout + result.stderr).toLowerCase();
|
||||
// Either bootstrap-attempt error, or a connection/network error —
|
||||
// both prove the --agent path executed (the wizard would prompt for
|
||||
// input and succeed/hang, not surface a network error).
|
||||
expect(
|
||||
combined.includes("agent") ||
|
||||
combined.includes("connect") ||
|
||||
combined.includes("network") ||
|
||||
combined.includes("fetch") ||
|
||||
combined.includes("bootstrap"),
|
||||
).toBe(true);
|
||||
fs.rmSync(home, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe("JSON envelope on network failure", () => {
|
||||
it("init --agent --json does not leak a stack trace when backend is unreachable", () => {
|
||||
const home = cleanHome();
|
||||
const result = run(["init", "--agent", "--json"], {
|
||||
home,
|
||||
env: {
|
||||
MEM0_BASE_URL: "http://127.0.0.1:1",
|
||||
FORCE_COLOR: "0",
|
||||
},
|
||||
});
|
||||
const combined = result.stdout + result.stderr;
|
||||
// No raw Node stack should escape the agent-mode handler.
|
||||
expect(combined).not.toMatch(/at \w+\s*\(.+\.ts:\d+/);
|
||||
expect(combined).not.toContain("UnhandledPromiseRejection");
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
fs.rmSync(home, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe("top-level help lists init", () => {
|
||||
// `mem0 --help` must list `init` so agents walking the top-level help
|
||||
// can discover the Agent Mode entrypoint without prior knowledge.
|
||||
|
||||
it("--help lists init", () => {
|
||||
const result = run(["--help"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("init");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* Unit tests for init internals — decision tree primitives + plugin sync.
|
||||
*
|
||||
* Mirror of `cli/python/tests/test_init_internals.py`. Both files MUST stay
|
||||
* in sync — if you add a behavioral assertion here, mirror it on the Python
|
||||
* side and vice versa.
|
||||
*
|
||||
* - `pingKey` must NOT treat network errors as "invalid key" (else a VPN
|
||||
* flap silently mints a new shadow over a working key).
|
||||
* - `plugin_sync` must only update entries that already exist, preserve
|
||||
* trailing newlines, and never mangle other lines.
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { pingKey } from "../src/commands/init.js";
|
||||
import { updateClaudeSettings, updateShellRc } from "../src/plugin-sync.js";
|
||||
|
||||
// ── pingKey ──────────────────────────────────────────────────────────────
|
||||
|
||||
describe("pingKey — network vs auth distinction", () => {
|
||||
const origFetch = globalThis.fetch;
|
||||
afterEach(() => {
|
||||
globalThis.fetch = origFetch;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("returns true for 200", async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue({ status: 200 } as Response);
|
||||
await expect(pingKey("k", "http://x")).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for 401 (definitively invalid)", async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue({ status: 401 } as Response);
|
||||
await expect(pingKey("k", "http://x")).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for 403 (definitively invalid)", async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue({ status: 403 } as Response);
|
||||
await expect(pingKey("k", "http://x")).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("returns true for 5xx (transient upstream — prefer reuse)", async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue({ status: 503 } as Response);
|
||||
await expect(pingKey("k", "http://x")).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("returns true on network error (prefer reuse over re-mint)", async () => {
|
||||
globalThis.fetch = vi.fn().mockRejectedValue(new Error("ECONNREFUSED"));
|
||||
await expect(pingKey("k", "http://x")).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("returns true on timeout (prefer reuse)", async () => {
|
||||
globalThis.fetch = vi.fn().mockRejectedValue(new Error("aborted"));
|
||||
await expect(pingKey("k", "http://x")).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ── updateShellRc ────────────────────────────────────────────────────────
|
||||
|
||||
describe("updateShellRc — exists-only contract", () => {
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-test-"));
|
||||
});
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("updates existing export and preserves trailing newline", () => {
|
||||
const rc = path.join(tmpDir, ".zshrc");
|
||||
fs.writeFileSync(rc, 'export MEM0_API_KEY="old"\n');
|
||||
expect(updateShellRc(rc, "newkey")).toBe(true);
|
||||
expect(fs.readFileSync(rc, "utf-8")).toBe('export MEM0_API_KEY="newkey"\n');
|
||||
});
|
||||
|
||||
it("does NOT create a new export when none exists", () => {
|
||||
const rc = path.join(tmpDir, ".zshrc");
|
||||
fs.writeFileSync(rc, "alias ll='ls -la'\n");
|
||||
expect(updateShellRc(rc, "newkey")).toBe(false);
|
||||
expect(fs.readFileSync(rc, "utf-8")).toBe("alias ll='ls -la'\n");
|
||||
});
|
||||
|
||||
it("preserves surrounding content", () => {
|
||||
const rc = path.join(tmpDir, ".zshrc");
|
||||
const original =
|
||||
"# my zshrc\n" +
|
||||
"alias ll='ls -la'\n" +
|
||||
"export MEM0_API_KEY='old'\n" +
|
||||
"export OTHER=keepme\n";
|
||||
fs.writeFileSync(rc, original);
|
||||
updateShellRc(rc, "newkey");
|
||||
const after = fs.readFileSync(rc, "utf-8");
|
||||
expect(after).toContain("alias ll='ls -la'\n");
|
||||
expect(after).toContain("export OTHER=keepme\n");
|
||||
expect(after).toContain("# my zshrc\n");
|
||||
expect(after).toContain('export MEM0_API_KEY="newkey"\n');
|
||||
});
|
||||
|
||||
it("is idempotent when value already matches", () => {
|
||||
const rc = path.join(tmpDir, ".zshrc");
|
||||
fs.writeFileSync(rc, 'export MEM0_API_KEY="same"\n');
|
||||
expect(updateShellRc(rc, "same")).toBe(false);
|
||||
});
|
||||
|
||||
it("is a no-op for missing files", () => {
|
||||
const rc = path.join(tmpDir, ".zshrc"); // does not exist
|
||||
expect(updateShellRc(rc, "x")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── updateClaudeSettings ─────────────────────────────────────────────────
|
||||
|
||||
describe("updateClaudeSettings — never creates entries", () => {
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-test-"));
|
||||
});
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("does not create env block when none exists", () => {
|
||||
const settings = path.join(tmpDir, "settings.json");
|
||||
fs.writeFileSync(settings, JSON.stringify({ otherKey: 1 }));
|
||||
expect(updateClaudeSettings(settings, "newkey")).toBe(false);
|
||||
expect(JSON.parse(fs.readFileSync(settings, "utf-8"))).toEqual({
|
||||
otherKey: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not create MEM0_API_KEY entry in existing env block", () => {
|
||||
const settings = path.join(tmpDir, "settings.json");
|
||||
fs.writeFileSync(settings, JSON.stringify({ env: { OTHER_KEY: "x" } }));
|
||||
expect(updateClaudeSettings(settings, "newkey")).toBe(false);
|
||||
});
|
||||
|
||||
it("updates existing entry and preserves siblings", () => {
|
||||
const settings = path.join(tmpDir, "settings.json");
|
||||
fs.writeFileSync(
|
||||
settings,
|
||||
JSON.stringify({ env: { MEM0_API_KEY: "old", OTHER: "y" } }, null, 2),
|
||||
);
|
||||
expect(updateClaudeSettings(settings, "fresh")).toBe(true);
|
||||
const data = JSON.parse(fs.readFileSync(settings, "utf-8"));
|
||||
expect(data.env.MEM0_API_KEY).toBe("fresh");
|
||||
expect(data.env.OTHER).toBe("y");
|
||||
});
|
||||
|
||||
it("is idempotent when value already matches", () => {
|
||||
const settings = path.join(tmpDir, "settings.json");
|
||||
fs.writeFileSync(
|
||||
settings,
|
||||
JSON.stringify({ env: { MEM0_API_KEY: "same" } }),
|
||||
);
|
||||
expect(updateClaudeSettings(settings, "same")).toBe(false);
|
||||
});
|
||||
|
||||
it("is a no-op for malformed JSON", () => {
|
||||
const settings = path.join(tmpDir, "settings.json");
|
||||
fs.writeFileSync(settings, "{ this is not json");
|
||||
expect(updateClaudeSettings(settings, "x")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to `mem0-cli` (Python) are documented here.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.2.7] — 2026-05-20
|
||||
|
||||
### Added
|
||||
|
||||
- `mem0 whoami` — print the active agent's `default_user_id` (the AGENTRUSH
|
||||
leaderboard identifier). Reads from local config, no network call.
|
||||
- `mem0 agent-rush <add | search>` — subcommand group that wraps the new
|
||||
`/v1/agent-rush/` platform endpoints for the 7-day AGENTRUSH game. Project
|
||||
routing is implicit (resolved server-side); no flags exposed. Pretty-prints
|
||||
platform error codes into actionable hints (e.g. `agentrush_search_first`
|
||||
→ "Run 3 'mem0 agent-rush search' commands before adding.").
|
||||
- PII safety prompt on first `mem0 agent-rush add`. Interactive runs require
|
||||
explicit `y` to acknowledge that AGENTRUSH memories are public; the
|
||||
acknowledgement is persisted in `~/.mem0/config.json` under
|
||||
`agent_rush.acknowledged_at` so the prompt only appears once per machine.
|
||||
Non-interactive (agent) invocations surface the warning to stderr without
|
||||
blocking.
|
||||
- New config schema field: `agent_rush.acknowledged_at` (ISO timestamp,
|
||||
empty until first interactive acknowledgement).
|
||||
|
||||
### Changed
|
||||
|
||||
- HTTP requests from the new agent-rush commands send `X-Mem0-Mode: agent-rush`
|
||||
in addition to the existing source headers, so platform telemetry can split
|
||||
game traffic from regular CLI usage.
|
||||
|
||||
## [0.2.6] and earlier
|
||||
|
||||
Unlogged historical releases. See git history under `cli/python/`.
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "mem0-cli"
|
||||
version = "0.2.4"
|
||||
version = "0.2.7"
|
||||
description = "The official CLI for mem0 — the memory layer for AI agents"
|
||||
readme = "README.md"
|
||||
license = "Apache-2.0"
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Detect whether the CLI is being invoked from inside an AI-agent context.
|
||||
|
||||
Used by `mem0 init` to auto-enter Agent Mode (Rule 3 bootstrap) when an
|
||||
agent runtime env var is present. The return value is a context **trigger
|
||||
only** — the canonical agent identity is self-declared by the agent via
|
||||
``--agent-caller <name>`` (Proof Editor-style) and never sniffed from env
|
||||
vars to fill the ``agent_caller`` field on the APIKey row.
|
||||
|
||||
Returns a short name or None. The list is curated, not exhaustive — env
|
||||
vars we don't recognise fall through to None (caller treated as
|
||||
non-agent). Honest reporting depends on ``--agent-caller``; this list is
|
||||
just enough to enable the zero-friction auto-bootstrap UX.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
_AGENT_CALLER_ENV: tuple[tuple[str, tuple[str, ...]], ...] = (
|
||||
("claude-code", ("CLAUDECODE", "CLAUDE_CODE")),
|
||||
("cursor", ("CURSOR_AGENT", "CURSOR_SESSION_ID")),
|
||||
("codex", ("CODEX_CLI", "OPENAI_CODEX")),
|
||||
("cline", ("CLINE_AGENT", "CLINE")),
|
||||
("continue", ("CONTINUE_AGENT", "CONTINUE_SESSION")),
|
||||
("aider", ("AIDER_SESSION",)),
|
||||
("goose", ("GOOSE_AGENT",)),
|
||||
("windsurf", ("WINDSURF_AGENT",)),
|
||||
)
|
||||
|
||||
|
||||
def detect_agent_caller() -> str | None:
|
||||
"""Return a canonical agent name if any agent env var is set, else None."""
|
||||
for name, env_vars in _AGENT_CALLER_ENV:
|
||||
if any(os.environ.get(v) for v in env_vars):
|
||||
return name
|
||||
return None
|
||||
@@ -237,6 +237,14 @@ def main_callback(
|
||||
cmd_version()
|
||||
raise typer.Exit()
|
||||
if ctx.invoked_subcommand:
|
||||
# Stash the active subcommand name so the JSON error envelope
|
||||
# (print_error in agent mode) can report which command failed
|
||||
# instead of an empty `"command": ""` field.
|
||||
from mem0_cli.state import set_current_command
|
||||
|
||||
set_current_command(ctx.invoked_subcommand)
|
||||
if ctx.invoked_subcommand and ctx.invoked_subcommand != "init":
|
||||
# init fires its own telemetry from init_cmd.run_init with full M1-M6 props.
|
||||
_fire_telemetry(ctx.invoked_subcommand)
|
||||
|
||||
|
||||
@@ -851,6 +859,19 @@ def init(
|
||||
force: bool = typer.Option(
|
||||
False, "--force", help="Overwrite existing config without confirmation."
|
||||
),
|
||||
agent_signal: bool = typer.Option(
|
||||
False, "--agent", help="Bootstrap an unattended Agent Mode account (no email required)."
|
||||
),
|
||||
source: str | None = typer.Option(
|
||||
None,
|
||||
"--source",
|
||||
help="Channel attribution for signup (e.g. github, hn, ph).",
|
||||
),
|
||||
agent_caller: str | None = typer.Option(
|
||||
None,
|
||||
"--agent-caller",
|
||||
help="Self-declared agent identity (e.g. claude-code, cursor). Used with --agent to attribute Agent Mode signups.",
|
||||
),
|
||||
) -> None:
|
||||
"""Interactive setup wizard for mem0 CLI.
|
||||
|
||||
@@ -859,10 +880,97 @@ def init(
|
||||
mem0 init --api-key m0-xxx --user-id alice
|
||||
mem0 init --email alice@company.com
|
||||
mem0 init --email alice@company.com --code 482901
|
||||
mem0 init --agent --agent-caller claude-code # AI agent self-identifies on Agent Mode bootstrap
|
||||
mem0 init --email alice@company.com # Claims an existing Agent Mode key when one is present
|
||||
"""
|
||||
from mem0_cli.commands.init_cmd import run_init
|
||||
|
||||
run_init(api_key=api_key, user_id=user_id, email=email, code=code, force=force)
|
||||
run_init(
|
||||
api_key=api_key,
|
||||
user_id=user_id,
|
||||
email=email,
|
||||
code=code,
|
||||
force=force,
|
||||
source=source,
|
||||
agent=agent_signal,
|
||||
agent_caller=agent_caller,
|
||||
)
|
||||
|
||||
|
||||
@app.command(rich_help_panel="Setup")
|
||||
def identify(
|
||||
name: str = typer.Argument(..., help="Agent identity (e.g. claude-code, cursor, my-bot)."),
|
||||
) -> None:
|
||||
"""Tag your active Agent Mode key with the AI agent that's using it.
|
||||
|
||||
Run this once after `mem0 init --agent` if you didn't pass --agent-caller.
|
||||
Idempotent — re-running just overwrites the value.
|
||||
|
||||
Example:
|
||||
mem0 identify claude-code
|
||||
"""
|
||||
from mem0_cli.commands.identify_cmd import run_identify
|
||||
|
||||
run_identify(name)
|
||||
|
||||
|
||||
@app.command(name="whoami", rich_help_panel="Setup")
|
||||
def whoami_cmd() -> None:
|
||||
"""Print your AGENTRUSH identifier (default_user_id).
|
||||
|
||||
Example:
|
||||
mem0 whoami
|
||||
"""
|
||||
from mem0_cli.commands.whoami_cmd import run_whoami
|
||||
|
||||
run_whoami()
|
||||
|
||||
|
||||
# ── AGENTRUSH sub-app ─────────────────────────────────────────────────────
|
||||
|
||||
agent_rush_app = typer.Typer(
|
||||
name="agent-rush",
|
||||
help="AGENTRUSH game commands",
|
||||
no_args_is_help=True,
|
||||
rich_markup_mode="rich",
|
||||
)
|
||||
|
||||
|
||||
@agent_rush_app.callback(invoke_without_command=True)
|
||||
def _agent_rush_callback(ctx: typer.Context) -> None:
|
||||
if ctx.invoked_subcommand:
|
||||
_fire_telemetry(f"agent-rush.{ctx.invoked_subcommand}")
|
||||
|
||||
|
||||
@agent_rush_app.command(name="add")
|
||||
def agent_rush_add(
|
||||
content: str = typer.Argument(..., help="Memory content (50-1000 characters, no URLs)."),
|
||||
) -> None:
|
||||
"""Submit a memory to AGENTRUSH.
|
||||
|
||||
Example:
|
||||
mem0 agent-rush add "I enjoy solving constraint-satisfaction problems."
|
||||
"""
|
||||
from mem0_cli.commands.agent_rush_cmd import run_agent_rush_add
|
||||
|
||||
run_agent_rush_add(content)
|
||||
|
||||
|
||||
@agent_rush_app.command(name="search")
|
||||
def agent_rush_search(
|
||||
query: str = typer.Argument(..., help="Search query."),
|
||||
) -> None:
|
||||
"""Search AGENTRUSH memories.
|
||||
|
||||
Example:
|
||||
mem0 agent-rush search "constraint satisfaction"
|
||||
"""
|
||||
from mem0_cli.commands.agent_rush_cmd import run_agent_rush_search
|
||||
|
||||
run_agent_rush_search(query)
|
||||
|
||||
|
||||
app.add_typer(agent_rush_app, name="agent-rush", rich_help_panel="Setup")
|
||||
|
||||
|
||||
# (entity_app registered at module level, below sub-group definitions)
|
||||
@@ -1198,11 +1306,28 @@ def main() -> None:
|
||||
import sys
|
||||
|
||||
# Allow --json/--agent anywhere in the command line (not just before subcommand).
|
||||
_json_flags = {"--json", "--agent"}
|
||||
if any(a in _json_flags for a in sys.argv[1:]):
|
||||
# Special case: `mem0 init --agent` is a subcommand flag (Agent Mode bootstrap)
|
||||
# consumed by init_cmd, not a global JSON-output toggle — leave it in argv.
|
||||
argv_rest = sys.argv[1:]
|
||||
is_init = "init" in argv_rest
|
||||
_global_flags = {"--json"} if is_init else {"--json", "--agent"}
|
||||
if any(a in _global_flags for a in argv_rest):
|
||||
from mem0_cli.state import set_agent_mode
|
||||
|
||||
set_agent_mode(True)
|
||||
sys.argv = [sys.argv[0]] + [a for a in sys.argv[1:] if a not in _json_flags]
|
||||
sys.argv = [sys.argv[0]] + [a for a in argv_rest if a not in _global_flags]
|
||||
|
||||
app()
|
||||
try:
|
||||
app()
|
||||
finally:
|
||||
# Surface any unclaimed Agent Mode notice once per command, after the
|
||||
# primary output. In JSON/agent mode the notice is folded into the
|
||||
# envelope by format_json_envelope, so skip the stderr banner there
|
||||
# to avoid duplicate output.
|
||||
from mem0_cli.state import is_agent_mode, take_notice
|
||||
|
||||
notice = take_notice()
|
||||
if notice and not is_agent_mode():
|
||||
from rich.console import Console
|
||||
|
||||
Console(stderr=True).print(f"\n[yellow]🔔 {notice}[/yellow]\n")
|
||||
|
||||
@@ -30,7 +30,7 @@ class PlatformBackend(Backend):
|
||||
)
|
||||
|
||||
def _request(self, method: str, path: str, **kwargs: Any) -> Any:
|
||||
from mem0_cli.state import is_agent_mode
|
||||
from mem0_cli.state import capture_notice, is_agent_mode
|
||||
|
||||
self._client.headers["X-Mem0-Caller-Type"] = "agent" if is_agent_mode() else "user"
|
||||
resp = self._client.request(method, path, **kwargs)
|
||||
@@ -48,7 +48,26 @@ class PlatformBackend(Backend):
|
||||
resp.raise_for_status()
|
||||
if resp.status_code == 204:
|
||||
return {}
|
||||
return resp.json()
|
||||
data = resp.json()
|
||||
|
||||
# Pull the unclaimed-Agent-Mode notice out of the body (or the header
|
||||
# fallback for endpoints that return non-dict / non-dict-leading
|
||||
# payloads) and stash it for end-of-command surfacing.
|
||||
notice = None
|
||||
if isinstance(data, dict) and "mem0_notice" in data:
|
||||
notice = data.pop("mem0_notice")
|
||||
elif (
|
||||
isinstance(data, list)
|
||||
and data
|
||||
and isinstance(data[0], dict)
|
||||
and "mem0_notice" in data[0]
|
||||
):
|
||||
notice = data[0].pop("mem0_notice")
|
||||
if notice is None:
|
||||
notice = resp.headers.get("X-Mem0-Notice-Message") or None
|
||||
capture_notice(notice)
|
||||
|
||||
return data
|
||||
|
||||
def add(
|
||||
self,
|
||||
|
||||
@@ -87,10 +87,12 @@ def print_error(console: Console, message: str, hint: str | None = None) -> None
|
||||
}
|
||||
print(_json.dumps(envelope))
|
||||
return
|
||||
from rich.markup import escape
|
||||
|
||||
sym = _sym("✗", "[error]")
|
||||
console.print(f"[{ERROR_COLOR}]{sym} Error:[/] {message}")
|
||||
console.print(f"[{ERROR_COLOR}]{sym} Error:[/] {escape(str(message))}")
|
||||
if hint:
|
||||
console.print(f" [{DIM_COLOR}]{hint}[/]")
|
||||
console.print(f" [{DIM_COLOR}]{escape(str(hint))}[/]")
|
||||
|
||||
|
||||
def print_warning(console: Console, message: str) -> None:
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
"""Agent Mode commands — bootstrap (unattended signup) and claim (OTP-based human upgrade)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
import typer
|
||||
from rich.console import Console
|
||||
from rich.prompt import Prompt
|
||||
|
||||
from mem0_cli.branding import (
|
||||
BRAND_COLOR,
|
||||
DIM_COLOR,
|
||||
print_error,
|
||||
print_success,
|
||||
)
|
||||
from mem0_cli.config import Mem0Config, save_config
|
||||
|
||||
console = Console()
|
||||
err_console = Console(stderr=True)
|
||||
|
||||
_SOURCE_HEADERS = {
|
||||
"X-Mem0-Source": "cli",
|
||||
"X-Mem0-Client-Language": "python",
|
||||
}
|
||||
|
||||
|
||||
def _validate_envelope(envelope: Any) -> None:
|
||||
"""Defend against partial/malformed backend responses.
|
||||
|
||||
A backend regression that returns ``{"api_key": null}`` would otherwise be
|
||||
silently persisted, producing confusing downstream errors far from the
|
||||
source. Fail fast with a clear message if the required fields are missing.
|
||||
"""
|
||||
if not isinstance(envelope, dict):
|
||||
print_error(err_console, "Bootstrap response was not a JSON object.")
|
||||
raise typer.Exit(1)
|
||||
for field in ("api_key", "default_user_id"):
|
||||
value = envelope.get(field)
|
||||
if not isinstance(value, str) or not value:
|
||||
print_error(
|
||||
err_console,
|
||||
f"Bootstrap response missing required field {field!r} — please update the CLI.",
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
def bootstrap_via_backend(
|
||||
config: Mem0Config,
|
||||
*,
|
||||
source: str | None = None,
|
||||
agent_caller: str | None = None,
|
||||
) -> None:
|
||||
"""POST /api/v1/auth/agent_mode/ and mutate config in place.
|
||||
|
||||
Args:
|
||||
config: Mem0Config mutated in place with the new platform values.
|
||||
source: ``--source`` flag passthrough (analytics tag, free-form).
|
||||
agent_caller: Self-declared agent identity passed via ``--agent-caller``
|
||||
(e.g. ``claude-code``, ``cursor``). May be None when the caller
|
||||
omitted the flag; the agent can backfill later via
|
||||
``mem0 identify <name>``. Sent to the backend in the request body
|
||||
and saved into ``platform.agent_caller`` for local introspection.
|
||||
|
||||
Raises typer.Exit(1) on failure.
|
||||
"""
|
||||
base_url = (config.platform.base_url or "https://api.mem0.ai").rstrip("/")
|
||||
body: dict[str, Any] = {}
|
||||
if source:
|
||||
body["source"] = source
|
||||
if agent_caller:
|
||||
body["agent_caller"] = agent_caller
|
||||
|
||||
try:
|
||||
with httpx.Client(timeout=30.0) as client:
|
||||
resp = client.post(
|
||||
f"{base_url}/api/v1/auth/agent_mode/",
|
||||
headers={**_SOURCE_HEADERS, "Content-Type": "application/json"},
|
||||
json=body,
|
||||
)
|
||||
except httpx.HTTPError as exc:
|
||||
print_error(err_console, f"Network error contacting Mem0: {exc}")
|
||||
raise typer.Exit(1) from exc
|
||||
|
||||
if resp.status_code == 429:
|
||||
print_error(err_console, "Rate-limited. Try again in a few minutes.")
|
||||
raise typer.Exit(1)
|
||||
if resp.status_code == 503:
|
||||
print_error(err_console, "Agent Mode is temporarily disabled. Try again later.")
|
||||
raise typer.Exit(1)
|
||||
if resp.status_code != 200:
|
||||
detail = resp.text
|
||||
try:
|
||||
err_body = resp.json()
|
||||
detail = err_body.get("error") or err_body.get("detail") or resp.text
|
||||
except (json.JSONDecodeError, ValueError, AttributeError):
|
||||
pass
|
||||
# Backend's @ratelimit decorator raises PermissionDenied, which DRF
|
||||
# translates to a generic 403 "You do not have permission to perform
|
||||
# this action." That's opaque — surface as the rate-limit it actually is.
|
||||
if resp.status_code == 403 and "permission" in str(detail).lower():
|
||||
print_error(
|
||||
err_console,
|
||||
"Daily Agent Mode signup limit reached for this network (5/day). Try again from a different IP or after midnight UTC.",
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
print_error(err_console, f"Bootstrap failed: {detail}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
envelope = resp.json()
|
||||
_validate_envelope(envelope)
|
||||
config.platform.api_key = envelope["api_key"]
|
||||
config.platform.base_url = base_url
|
||||
config.platform.agent_mode = True
|
||||
config.platform.created_via = "agent_mode"
|
||||
config.platform.agent_caller = agent_caller or ""
|
||||
config.platform.claimed_at = ""
|
||||
config.platform.default_user_id = envelope["default_user_id"]
|
||||
# Adopt the slug-derived user_id as the default scope for memory ops.
|
||||
config.defaults.user_id = envelope["default_user_id"]
|
||||
save_config(config)
|
||||
|
||||
print_success(console, f"Agent Mode active. Default user_id: {envelope['default_user_id']}")
|
||||
notice = envelope.get("mem0_notice")
|
||||
if notice:
|
||||
console.print(f"\n[yellow]🔔 {notice}[/yellow]\n")
|
||||
else:
|
||||
# Fallback if the backend hasn't deployed the unified notice yet.
|
||||
claim_cmd = envelope.get("claim_command", "mem0 init --email <your-email>")
|
||||
console.print(f" [{DIM_COLOR}]To claim this account later: {claim_cmd}[/]")
|
||||
|
||||
if not agent_caller:
|
||||
console.print(
|
||||
f" [{DIM_COLOR}]If you are an AI agent, identify yourself: "
|
||||
f"`mem0 identify <your-name>` (e.g. claude-code, cursor).[/]"
|
||||
)
|
||||
|
||||
|
||||
def claim_via_otp(config: Mem0Config, *, email: str, code: str | None = None) -> None:
|
||||
"""Claim an existing Agent Mode account via OTP — no browser, no polling.
|
||||
|
||||
Reuses the standard email-code flow (`/api/v1/auth/email_code/` then
|
||||
`/.../verify/`) and adds the local agent-mode API key in the verify body
|
||||
as `agent_mode_api_key`. Backend's `verify_email_code` runs the
|
||||
upgrade-in-place transaction inline and returns claim result.
|
||||
|
||||
On success: flips `platform.agent_mode=false`, sets `claimed_at`, stamps
|
||||
`user_email`. The api_key value itself never changes.
|
||||
"""
|
||||
base_url = (config.platform.base_url or "https://api.mem0.ai").rstrip("/")
|
||||
if not config.platform.api_key or not config.platform.agent_mode:
|
||||
print_error(
|
||||
err_console,
|
||||
"This command requires an active Agent Mode config. Run `mem0 init` first.",
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
raw_key = config.platform.api_key
|
||||
|
||||
with httpx.Client(timeout=30.0) as client:
|
||||
# Step 1: request OTP (unless --code provided)
|
||||
if not code:
|
||||
send = client.post(
|
||||
f"{base_url}/api/v1/auth/email_code/",
|
||||
headers={**_SOURCE_HEADERS, "Content-Type": "application/json"},
|
||||
json={"email": email},
|
||||
)
|
||||
if send.status_code == 429:
|
||||
print_error(err_console, "Too many attempts. Try again in a few minutes.")
|
||||
raise typer.Exit(1)
|
||||
if send.status_code != 200:
|
||||
try:
|
||||
detail = send.json().get("error", send.text)
|
||||
except Exception:
|
||||
detail = send.text
|
||||
print_error(err_console, f"Failed to send code: {detail}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
print_success(console, f"Verification code sent to {email}. Check your inbox.")
|
||||
|
||||
if not sys.stdin.isatty():
|
||||
print_error(
|
||||
err_console,
|
||||
"No --code provided and terminal is non-interactive.",
|
||||
hint=f"Re-run: mem0 init --email {email} --code <code>",
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
console.print()
|
||||
code = Prompt.ask(f" [{BRAND_COLOR}]Verification Code[/]")
|
||||
if not code:
|
||||
print_error(err_console, "Code is required.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Step 2: verify + claim in one shot
|
||||
verify = client.post(
|
||||
f"{base_url}/api/v1/auth/email_code/verify/",
|
||||
headers={**_SOURCE_HEADERS, "Content-Type": "application/json"},
|
||||
json={
|
||||
"email": email,
|
||||
"code": code.strip(),
|
||||
"agent_mode_api_key": raw_key,
|
||||
},
|
||||
)
|
||||
|
||||
if verify.status_code != 200:
|
||||
try:
|
||||
err_body = verify.json()
|
||||
detail = err_body.get("error", verify.text)
|
||||
code_str = err_body.get("code", "")
|
||||
except (json.JSONDecodeError, ValueError, AttributeError):
|
||||
detail = verify.text
|
||||
code_str = ""
|
||||
print_error(err_console, f"Claim failed: {detail}")
|
||||
if code_str == "email_already_claimed":
|
||||
console.print(
|
||||
f" [{DIM_COLOR}]Tip: this email already has a Mem0 account. Sign in at app.mem0.ai with your existing credentials.[/]"
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
claim_body = verify.json()
|
||||
if not claim_body.get("claimed"):
|
||||
print_error(err_console, f"Unexpected verify response: {claim_body}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
config.platform.agent_mode = False
|
||||
config.platform.claimed_at = claim_body.get("claimed_at") or _utcnow_iso()
|
||||
config.platform.user_email = email
|
||||
config.platform.created_via = "email"
|
||||
save_config(config)
|
||||
print_success(console, f"Agent claimed to {email}. Your API key is unchanged.")
|
||||
|
||||
|
||||
def _utcnow_iso() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
@@ -0,0 +1,132 @@
|
||||
"""mem0 agent-rush — AGENTRUSH game commands.
|
||||
|
||||
Wraps the platform's /v1/agent-rush/{memories/, memories/search/} endpoints.
|
||||
Hardcoded routing; no flags needed.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import httpx
|
||||
import typer
|
||||
from rich.console import Console
|
||||
|
||||
from mem0_cli.branding import print_error, print_success
|
||||
from mem0_cli.config import load_config, save_config
|
||||
|
||||
console = Console()
|
||||
err_console = Console(stderr=True)
|
||||
|
||||
_PII_WARNING_LINES = (
|
||||
"",
|
||||
"[yellow]⚠️ AGENTRUSH memories are PUBLIC — visible to any other player.[/yellow]",
|
||||
"[yellow] Do not include real names, emails, secrets, work content, or PII.[/yellow]",
|
||||
"",
|
||||
)
|
||||
|
||||
_SOURCE_HEADERS = {
|
||||
"X-Mem0-Source": "cli",
|
||||
"X-Mem0-Client-Language": "python",
|
||||
"X-Mem0-Mode": "agent-rush",
|
||||
}
|
||||
|
||||
_ERROR_HINTS = {
|
||||
"agentrush_search_first": "Run 3 'mem0 agent-rush search' commands before adding.",
|
||||
"agentrush_search_quota": "You've used your 3 lifetime searches.",
|
||||
"agentrush_add_quota": "You've used your 3 lifetime adds.",
|
||||
"agentrush_not_agent_mode": "Re-run 'mem0 init --agent' to bootstrap an agent-mode key.",
|
||||
"agentrush_length": "Memory text must be 50-1000 characters.",
|
||||
"agentrush_no_urls": "URLs are not allowed.",
|
||||
"agentrush_blocklist": "Content contains a blocked term.",
|
||||
"agentrush_global_quota": "Event-wide cap reached. Try again later.",
|
||||
"agentrush_not_provisioned": "AGENTRUSH is not provisioned in this environment.",
|
||||
}
|
||||
|
||||
|
||||
def _call(path: str, body: dict) -> dict:
|
||||
config = load_config()
|
||||
if not config.platform.api_key:
|
||||
print_error(err_console, "Not initialized. Run `mem0 init --agent` first.")
|
||||
raise typer.Exit(1)
|
||||
base_url = (config.platform.base_url or "https://api.mem0.ai").rstrip("/")
|
||||
try:
|
||||
with httpx.Client(timeout=30.0) as client:
|
||||
resp = client.post(
|
||||
f"{base_url}{path}",
|
||||
headers={
|
||||
**_SOURCE_HEADERS,
|
||||
"Authorization": f"Token {config.platform.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json=body,
|
||||
)
|
||||
except httpx.HTTPError as exc:
|
||||
print_error(err_console, f"Network error: {exc}")
|
||||
raise typer.Exit(1) from exc
|
||||
try:
|
||||
data = resp.json()
|
||||
except Exception:
|
||||
data = {}
|
||||
if resp.status_code >= 400:
|
||||
code = (
|
||||
(data.get("error") or {}).get("code", "unknown")
|
||||
if isinstance(data, dict)
|
||||
else "unknown"
|
||||
)
|
||||
print_error(err_console, f"AGENTRUSH error: {code}")
|
||||
hint = _ERROR_HINTS.get(code)
|
||||
if hint:
|
||||
console.print(f" [dim]{hint}[/dim]")
|
||||
raise typer.Exit(1)
|
||||
return data
|
||||
|
||||
|
||||
def _ensure_warning_acknowledged() -> None:
|
||||
"""Block the first interactive add on the PII warning; pass-through for agents.
|
||||
|
||||
Interactive (TTY): show prompt, require explicit 'y', persist
|
||||
`agent_rush.acknowledged_at` so we never ask the same machine twice.
|
||||
|
||||
Non-interactive (no TTY — typical when an agent runs the CLI): surface
|
||||
the warning to stderr for the human reading the agent transcript and
|
||||
proceed without prompting (agents can't answer y/N).
|
||||
"""
|
||||
config = load_config()
|
||||
if config.agent_rush.acknowledged_at:
|
||||
return
|
||||
|
||||
is_tty = sys.stdin.isatty() and sys.stdout.isatty()
|
||||
if not is_tty:
|
||||
for line in _PII_WARNING_LINES:
|
||||
err_console.print(line)
|
||||
return
|
||||
|
||||
for line in _PII_WARNING_LINES:
|
||||
console.print(line)
|
||||
answer = typer.prompt(" Continue? [y/N]", default="N", show_default=False).strip().lower()
|
||||
if answer not in ("y", "yes"):
|
||||
print_error(err_console, "Aborted.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
config.agent_rush.acknowledged_at = datetime.now(timezone.utc).isoformat()
|
||||
save_config(config)
|
||||
|
||||
|
||||
def run_agent_rush_add(content: str) -> None:
|
||||
_ensure_warning_acknowledged()
|
||||
result = _call("/v1/agent-rush/memories/", {"content": content})
|
||||
event_id = result.get("event_id", "?")
|
||||
print_success(console, f"Memory submitted (event_id: {event_id})")
|
||||
|
||||
|
||||
def run_agent_rush_search(query: str) -> None:
|
||||
result = _call("/v1/agent-rush/memories/search/", {"query": query})
|
||||
memories = result.get("results") or result.get("memories") or []
|
||||
if not memories:
|
||||
console.print("[dim](no results)[/dim]")
|
||||
return
|
||||
for i, m in enumerate(memories[:5], start=1):
|
||||
text = m.get("memory") if isinstance(m, dict) else str(m)
|
||||
console.print(f" {i}. {text}")
|
||||
@@ -0,0 +1,75 @@
|
||||
"""mem0 identify — declare which agent owns the current agent-mode key.
|
||||
|
||||
Used when `mem0 init --agent` ran without --agent-caller, so the backend
|
||||
saved agent_caller=NULL. The agent re-runs `mem0 identify <name>` to PATCH
|
||||
its own row with its real identity. Idempotent — running it again just
|
||||
overwrites.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
import typer
|
||||
from rich.console import Console
|
||||
|
||||
from mem0_cli.branding import print_error, print_success
|
||||
from mem0_cli.config import load_config, save_config
|
||||
|
||||
console = Console()
|
||||
err_console = Console(stderr=True)
|
||||
|
||||
_SOURCE_HEADERS = {
|
||||
"X-Mem0-Source": "cli",
|
||||
"X-Mem0-Client-Language": "python",
|
||||
}
|
||||
|
||||
|
||||
def run_identify(name: str) -> None:
|
||||
"""PATCH the active agent-mode key's agent_caller field."""
|
||||
config = load_config()
|
||||
if not config.platform.api_key:
|
||||
print_error(
|
||||
err_console,
|
||||
"No API key configured. Run `mem0 init --agent` first.",
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
if not config.platform.agent_mode:
|
||||
print_error(
|
||||
err_console,
|
||||
"This command only works on unclaimed agent-mode keys.",
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
name = (name or "").strip()
|
||||
if not name:
|
||||
print_error(err_console, "Agent name is required.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
base_url = (config.platform.base_url or "https://api.mem0.ai").rstrip("/")
|
||||
try:
|
||||
with httpx.Client(timeout=30.0) as client:
|
||||
resp = client.patch(
|
||||
f"{base_url}/api/v1/auth/agent_mode/caller/",
|
||||
headers={
|
||||
**_SOURCE_HEADERS,
|
||||
"Authorization": f"Token {config.platform.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json={"agent_caller": name},
|
||||
)
|
||||
except httpx.HTTPError as exc:
|
||||
print_error(err_console, f"Network error: {exc}")
|
||||
raise typer.Exit(1) from exc
|
||||
|
||||
if resp.status_code != 200:
|
||||
try:
|
||||
detail = resp.json().get("error", resp.text)
|
||||
except Exception:
|
||||
detail = resp.text
|
||||
print_error(err_console, f"Identify failed: {detail}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
canonical = resp.json().get("agent_caller", name)
|
||||
config.platform.agent_caller = canonical
|
||||
save_config(config)
|
||||
print_success(console, f"Identified as {canonical}.")
|
||||
@@ -103,6 +103,25 @@ def _validate_email(email: str) -> None:
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
def _ping_key(api_key: str, base_url: str, timeout: float = 5.0) -> bool:
|
||||
"""Validate api_key against /v1/ping/.
|
||||
|
||||
Returns False ONLY on a definitive "invalid key" signal (HTTP 401 / 403).
|
||||
Network errors, timeouts, and 5xx responses return True so we prefer
|
||||
reusing an existing key over silently minting a new shadow on a transient
|
||||
blip (which would also clobber config + plugin-sync targets).
|
||||
"""
|
||||
try:
|
||||
resp = httpx.get(
|
||||
f"{base_url.rstrip('/')}/v1/ping/",
|
||||
headers={"Authorization": f"Token {api_key}"},
|
||||
timeout=timeout,
|
||||
)
|
||||
except httpx.HTTPError:
|
||||
return True # unknown — prefer reuse
|
||||
return resp.status_code not in (401, 403)
|
||||
|
||||
|
||||
def _email_login(
|
||||
email: str,
|
||||
code: str | None,
|
||||
@@ -182,21 +201,143 @@ def run_init(
|
||||
email: str | None = None,
|
||||
code: str | None = None,
|
||||
force: bool = False,
|
||||
source: str | None = None,
|
||||
agent: bool = False,
|
||||
agent_caller: str | None = None,
|
||||
) -> None:
|
||||
"""Interactive setup wizard for mem0 CLI.
|
||||
|
||||
When both *api_key* and *user_id* are supplied, all prompts are skipped
|
||||
(non-interactive mode). When running in a non-TTY without the required
|
||||
flags, an error message is printed.
|
||||
|
||||
Agent Mode dispatch (no email/api-key flags):
|
||||
- If existing config has an active API key → reuse (existing_key path).
|
||||
- Else if any positive agent signal (--agent, --json global, agent env
|
||||
var, or `agent` flag) → POST /api/v1/auth/agent_mode/ and write config.
|
||||
- Else fall through to the interactive wizard.
|
||||
|
||||
Claim dispatch:
|
||||
- If `--email` is set AND existing config has `agent_mode=true`, run the
|
||||
claim device-flow against the existing key instead of minting a new
|
||||
email-based key.
|
||||
"""
|
||||
from mem0_cli.agent_detect import detect_agent_caller
|
||||
from mem0_cli.commands.agent_mode_cmd import bootstrap_via_backend, claim_via_otp
|
||||
from mem0_cli.state import is_agent_mode as _global_agent_mode
|
||||
from mem0_cli.telemetry import capture_event
|
||||
|
||||
def _fire_init(mode: str, *, claimed: bool = False) -> None:
|
||||
"""Fire cli.init telemetry with M1-M6 properties."""
|
||||
props: dict = {"command": "init", "mode": mode}
|
||||
if agent_caller:
|
||||
# Self-declared via --agent-caller; not sniffed from env vars.
|
||||
props["agent_caller"] = agent_caller
|
||||
if source:
|
||||
props["signup_source"] = source
|
||||
if claimed:
|
||||
props["claimed_agent_mode"] = True
|
||||
capture_event("cli.init", props)
|
||||
|
||||
config = Mem0Config()
|
||||
|
||||
base_url = os.environ.get("MEM0_BASE_URL", config.platform.base_url or DEFAULT_BASE_URL)
|
||||
config.platform.base_url = base_url
|
||||
|
||||
if code and not email:
|
||||
print_error(err_console, "--code requires --email.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# ── Email + existing agent-mode config → claim flow ─────────────────
|
||||
if email and CONFIG_FILE.exists():
|
||||
existing = load_config()
|
||||
if existing.platform.agent_mode and existing.platform.api_key:
|
||||
email = email.strip().lower()
|
||||
_validate_email(email)
|
||||
print_info(console, f"Claiming Agent Mode account to {email}...")
|
||||
claim_via_otp(existing, email=email, code=code)
|
||||
_fire_init("email", claimed=True)
|
||||
return
|
||||
|
||||
# ── Agent Mode path runs BEFORE the existing-config guard ──────────
|
||||
# Rules 1/2 REUSE a valid existing key (not overwrite), so we must
|
||||
# short-circuit before the guard prompts. Rule 3 mints only when there
|
||||
# is no valid key to reuse — in that case overwriting is correct.
|
||||
_agent_ctx = agent or _global_agent_mode() or (detect_agent_caller() is not None)
|
||||
if not api_key and not email and _agent_ctx:
|
||||
from mem0_cli.output import format_json_envelope
|
||||
from mem0_cli.state import is_agent_mode as _is_json_mode
|
||||
|
||||
def _emit_reuse(source: str) -> None:
|
||||
if _is_json_mode():
|
||||
format_json_envelope(
|
||||
console,
|
||||
command="init",
|
||||
data={
|
||||
"api_key_saved": False,
|
||||
"api_key_source": source,
|
||||
"agent_mode": False,
|
||||
"message": "Existing Mem0 API key found and reused. No Agent Mode key was created.",
|
||||
},
|
||||
)
|
||||
else:
|
||||
msg = (
|
||||
"Existing MEM0_API_KEY is valid; reusing it. No new Agent Mode key was minted."
|
||||
if source == "env"
|
||||
else "Existing API key in config is valid; reusing it. No new Agent Mode key was minted."
|
||||
)
|
||||
print_success(console, msg)
|
||||
|
||||
def _maybe_identify(key: str) -> None:
|
||||
"""Best-effort PATCH agent_caller when --agent-caller is supplied on a
|
||||
reused key. Silent no-op on any failure — reuse must not break.
|
||||
"""
|
||||
if not agent_caller:
|
||||
return
|
||||
try:
|
||||
resp = httpx.patch(
|
||||
f"{base_url.rstrip('/')}/api/v1/auth/agent_mode/caller/",
|
||||
headers={
|
||||
"Authorization": f"Token {key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json={"agent_caller": agent_caller},
|
||||
timeout=10.0,
|
||||
)
|
||||
# Also reflect in local config so introspection matches backend.
|
||||
if resp.status_code == 200 and CONFIG_FILE.exists():
|
||||
try:
|
||||
cfg = load_config()
|
||||
cfg.platform.agent_caller = resp.json().get("agent_caller", agent_caller)
|
||||
save_config(cfg)
|
||||
except Exception:
|
||||
pass
|
||||
except httpx.HTTPError:
|
||||
pass
|
||||
|
||||
# Rule 1: env MEM0_API_KEY valid → reuse, no new key.
|
||||
_env_key = (os.environ.get("MEM0_API_KEY") or "").strip()
|
||||
if _env_key and _ping_key(_env_key, base_url):
|
||||
_maybe_identify(_env_key)
|
||||
_emit_reuse("env")
|
||||
_fire_init("existing_key")
|
||||
return
|
||||
# Rule 2: existing config api_key valid → reuse.
|
||||
if CONFIG_FILE.exists():
|
||||
_existing = load_config()
|
||||
if _existing.platform.api_key and _ping_key(_existing.platform.api_key, base_url):
|
||||
_maybe_identify(_existing.platform.api_key)
|
||||
_emit_reuse("config")
|
||||
_fire_init("existing_key")
|
||||
return
|
||||
# Rule 3: mint a fresh shadow (no valid key to reuse).
|
||||
# agent_caller is the agent's self-declared identity from --agent-caller
|
||||
# (Proof Editor-style). Env-var auto-detect is still used above to
|
||||
# decide we're in an agent context, but never to fill identity.
|
||||
bootstrap_via_backend(config, source=source, agent_caller=agent_caller)
|
||||
_fire_init("agent")
|
||||
return
|
||||
|
||||
# Warn if an existing config with an API key would be overwritten
|
||||
if not force and CONFIG_FILE.exists():
|
||||
existing = load_config()
|
||||
@@ -242,6 +383,7 @@ def run_init(
|
||||
config.platform.api_key = api_key_val
|
||||
config.platform.base_url = base_url
|
||||
config.platform.user_email = email
|
||||
config.platform.created_via = "email"
|
||||
config.defaults.user_id = (
|
||||
user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
|
||||
)
|
||||
@@ -258,6 +400,8 @@ def run_init(
|
||||
return
|
||||
|
||||
# ── API key flow (existing) ───────────────────────────────────────
|
||||
# (Agent Mode branch runs earlier — see above, before the existing-config
|
||||
# guard, so Rules 1/2 can REUSE a valid key without prompting overwrite.)
|
||||
|
||||
# Non-TTY: resolve defaults so partial flags work in pipelines / CI
|
||||
if not sys.stdin.isatty():
|
||||
@@ -265,7 +409,7 @@ def run_init(
|
||||
print_error(
|
||||
err_console,
|
||||
"Non-interactive terminal detected and --api-key is required.",
|
||||
hint="Run: mem0 init --api-key <key> [--user-id <id>]",
|
||||
hint="Run: mem0 init --api-key <key>, --email <addr>, or --agent for unattended Agent Mode bootstrap.",
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
user_id = user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
|
||||
@@ -273,6 +417,7 @@ def run_init(
|
||||
# Fully non-interactive when both flags provided
|
||||
if api_key and user_id:
|
||||
config.platform.api_key = api_key
|
||||
config.platform.created_via = "api_key"
|
||||
config.defaults.user_id = user_id
|
||||
_validate_platform(config)
|
||||
save_config(config)
|
||||
@@ -313,6 +458,7 @@ def run_init(
|
||||
config.platform.api_key = api_key_val
|
||||
config.platform.base_url = base_url
|
||||
config.platform.user_email = email_addr
|
||||
config.platform.created_via = "email"
|
||||
config.defaults.user_id = (
|
||||
user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
|
||||
)
|
||||
@@ -331,6 +477,7 @@ def run_init(
|
||||
# API key flow
|
||||
if api_key:
|
||||
config.platform.api_key = api_key
|
||||
config.platform.created_via = "api_key"
|
||||
else:
|
||||
_setup_platform(config)
|
||||
|
||||
@@ -370,6 +517,7 @@ def _setup_platform(config: Mem0Config) -> None:
|
||||
raise typer.Exit(1)
|
||||
|
||||
config.platform.api_key = api_key
|
||||
config.platform.created_via = "api_key"
|
||||
|
||||
|
||||
def _setup_defaults(config: Mem0Config) -> None:
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"""mem0 whoami — print the active agent's default_user_id (AGENTRUSH identifier)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import typer
|
||||
from rich.console import Console
|
||||
|
||||
from mem0_cli.branding import BRAND_COLOR, print_error, print_info
|
||||
from mem0_cli.config import load_config
|
||||
|
||||
console = Console()
|
||||
err_console = Console(stderr=True)
|
||||
|
||||
|
||||
def run_whoami() -> None:
|
||||
config = load_config()
|
||||
session_id = config.platform.default_user_id if config.platform else None
|
||||
if not session_id:
|
||||
print_error(
|
||||
err_console,
|
||||
"No default_user_id found. Run `mem0 init --agent` first.",
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
console.print(f"Your AGENTRUSH identifier: [{BRAND_COLOR}]{session_id}[/{BRAND_COLOR}]")
|
||||
print_info(console, "Find your row at https://mem0.ai/agentrush")
|
||||
@@ -28,6 +28,14 @@ class PlatformConfig:
|
||||
api_key: str = ""
|
||||
base_url: str = DEFAULT_BASE_URL
|
||||
user_email: str = ""
|
||||
# Agent Mode (unclaimed-shadow signup)
|
||||
agent_mode: bool = False # True while the key is an unclaimed agent-mode key
|
||||
created_via: str = "" # "agent_mode" | "email" | "api_key" | "existing_key"
|
||||
agent_caller: str = (
|
||||
"" # canonical agent name when created_via == "agent_mode" (e.g. "claude-code")
|
||||
)
|
||||
claimed_at: str = "" # ISO timestamp once the agent has been claimed by a human
|
||||
default_user_id: str = "" # `user_<slug>` returned by bootstrap; used as auto-default
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -43,12 +51,20 @@ class TelemetryConfig:
|
||||
anonymous_id: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentRushConfig:
|
||||
# ISO timestamp the human acknowledged the "memories are public" warning.
|
||||
# Empty until first interactive `mem0 agent-rush add`.
|
||||
acknowledged_at: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Mem0Config:
|
||||
version: int = CONFIG_VERSION
|
||||
defaults: DefaultsConfig = field(default_factory=DefaultsConfig)
|
||||
platform: PlatformConfig = field(default_factory=PlatformConfig)
|
||||
telemetry: TelemetryConfig = field(default_factory=TelemetryConfig)
|
||||
agent_rush: AgentRushConfig = field(default_factory=AgentRushConfig)
|
||||
|
||||
|
||||
SHORT_KEY_ALIASES: dict[str, str] = {
|
||||
@@ -83,6 +99,11 @@ def load_config() -> Mem0Config:
|
||||
config.platform.api_key = plat.get("api_key", "")
|
||||
config.platform.base_url = plat.get("base_url", DEFAULT_BASE_URL)
|
||||
config.platform.user_email = plat.get("user_email", "")
|
||||
config.platform.agent_mode = bool(plat.get("agent_mode", False))
|
||||
config.platform.created_via = plat.get("created_via", "")
|
||||
config.platform.agent_caller = plat.get("agent_caller", "")
|
||||
config.platform.claimed_at = plat.get("claimed_at", "")
|
||||
config.platform.default_user_id = plat.get("default_user_id", "")
|
||||
|
||||
defaults = data.get("defaults", {})
|
||||
config.defaults.user_id = defaults.get("user_id", "")
|
||||
@@ -92,6 +113,9 @@ def load_config() -> Mem0Config:
|
||||
telemetry = data.get("telemetry", {})
|
||||
config.telemetry.anonymous_id = telemetry.get("anonymous_id", "")
|
||||
|
||||
agent_rush = data.get("agent_rush", {})
|
||||
config.agent_rush.acknowledged_at = agent_rush.get("acknowledged_at", "")
|
||||
|
||||
# Environment variable overrides
|
||||
env_key = os.environ.get("MEM0_API_KEY")
|
||||
if env_key:
|
||||
@@ -136,10 +160,18 @@ def save_config(config: Mem0Config) -> None:
|
||||
"api_key": config.platform.api_key,
|
||||
"base_url": config.platform.base_url,
|
||||
"user_email": config.platform.user_email,
|
||||
"agent_mode": config.platform.agent_mode,
|
||||
"created_via": config.platform.created_via,
|
||||
"agent_caller": config.platform.agent_caller,
|
||||
"claimed_at": config.platform.claimed_at,
|
||||
"default_user_id": config.platform.default_user_id,
|
||||
},
|
||||
"telemetry": {
|
||||
"anonymous_id": config.telemetry.anonymous_id,
|
||||
},
|
||||
"agent_rush": {
|
||||
"acknowledged_at": config.agent_rush.acknowledged_at,
|
||||
},
|
||||
}
|
||||
|
||||
with open(CONFIG_FILE, "w") as f:
|
||||
@@ -147,6 +179,19 @@ def save_config(config: Mem0Config) -> None:
|
||||
|
||||
os.chmod(CONFIG_FILE, stat.S_IRUSR | stat.S_IWUSR) # 0600
|
||||
|
||||
# Propagate the active api_key to ecosystem touchpoints (Claude Code
|
||||
# plugin env injection, shell rc exports). Idempotent — only updates
|
||||
# EXISTING entries; never creates new ones. Best-effort: any IOError
|
||||
# in the sync is swallowed so config.json is always the authoritative
|
||||
# write, never blocked by plugin-state issues.
|
||||
if config.platform.api_key:
|
||||
try:
|
||||
from mem0_cli.plugin_sync import sync_api_key
|
||||
|
||||
sync_api_key(config.platform.api_key)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def redact_key(key: str) -> str:
|
||||
"""Redact an API key for display: m0-xxx...xxx"""
|
||||
|
||||
@@ -229,6 +229,16 @@ def format_json_envelope(
|
||||
if error:
|
||||
envelope["error"] = error
|
||||
envelope["data"] = data
|
||||
|
||||
# If the platform flagged this as an unclaimed Agent Mode account, surface
|
||||
# the notice inside the JSON envelope so an agent consuming the output
|
||||
# sees it without needing to inspect HTTP headers.
|
||||
from mem0_cli.state import take_notice
|
||||
|
||||
notice = take_notice()
|
||||
if notice:
|
||||
envelope["mem0_notice"] = notice
|
||||
|
||||
console.print_json(json.dumps(envelope, default=str))
|
||||
|
||||
|
||||
@@ -323,6 +333,15 @@ def format_agent_envelope(
|
||||
if count is not None:
|
||||
envelope["count"] = count
|
||||
envelope["data"] = sanitize_agent_data(command, data)
|
||||
|
||||
# Surface the unclaimed-Agent-Mode notice (if any) in the envelope so an
|
||||
# agent reading the JSON output sees it without inspecting HTTP headers.
|
||||
from mem0_cli.state import take_notice
|
||||
|
||||
notice = take_notice()
|
||||
if notice:
|
||||
envelope["mem0_notice"] = notice
|
||||
|
||||
console.print_json(json.dumps(envelope, default=str))
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
"""Sync the active Mem0 API key into other ecosystem touchpoints.
|
||||
|
||||
Why this exists:
|
||||
The CLI canonical state lives in ``~/.mem0/config.json``. But MCP servers
|
||||
(Claude Code plugin, Codex plugin, etc.) read ``MEM0_API_KEY`` from env
|
||||
vars or their own config files. Without a sync, an agent-mode bootstrap
|
||||
mints a new key into config.json but the plugin's MCP keeps using the
|
||||
old key from env — silent surprise.
|
||||
|
||||
Design:
|
||||
- Update ONLY entries that already exist (never create new ones)
|
||||
- Preserve all surrounding content / formatting / other keys
|
||||
- Atomic writes (tmpfile + rename) so a crash mid-write doesn't corrupt
|
||||
- Idempotent — re-running with the same key is a no-op
|
||||
- Skip on dry_run
|
||||
|
||||
Targets currently handled:
|
||||
- ``~/.claude/settings.json::env::MEM0_API_KEY`` (Claude Code env injection)
|
||||
- ``~/.zshrc`` / ``~/.bashrc`` ``export MEM0_API_KEY="..."`` lines
|
||||
|
||||
Out of scope (deliberately not touched):
|
||||
- Codex / Cursor MCP configs — would require schema-aware edits and
|
||||
those tools don't have mem0 entries by default
|
||||
- Plugin's own ``<plugin-dir>/.api_key`` file — plugin-managed
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
# Files we know how to update safely.
|
||||
_CLAUDE_SETTINGS = Path.home() / ".claude" / "settings.json"
|
||||
_SHELL_RCS = [Path.home() / ".zshrc", Path.home() / ".bashrc", Path.home() / ".bash_profile"]
|
||||
|
||||
|
||||
def sync_api_key(api_key: str) -> list[str]:
|
||||
"""Propagate ``api_key`` into known ecosystem touchpoints.
|
||||
|
||||
Returns the list of paths actually updated. Empty list means nothing
|
||||
needed updating (either targets didn't exist or already had this value).
|
||||
"""
|
||||
if not api_key:
|
||||
return []
|
||||
updated: list[str] = []
|
||||
if _update_claude_settings(_CLAUDE_SETTINGS, api_key):
|
||||
updated.append(str(_CLAUDE_SETTINGS))
|
||||
for rc in _SHELL_RCS:
|
||||
if _update_shell_rc(rc, api_key):
|
||||
updated.append(str(rc))
|
||||
return updated
|
||||
|
||||
|
||||
def _update_claude_settings(path: Path, api_key: str) -> bool:
|
||||
"""Update ``env.MEM0_API_KEY`` in path. Returns True if file was changed."""
|
||||
if not path.is_file():
|
||||
return False
|
||||
try:
|
||||
with path.open("r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return False
|
||||
env = data.get("env")
|
||||
if not isinstance(env, dict) or "MEM0_API_KEY" not in env:
|
||||
# No existing entry — don't create one.
|
||||
return False
|
||||
if env["MEM0_API_KEY"] == api_key:
|
||||
return False # already in sync
|
||||
env["MEM0_API_KEY"] = api_key
|
||||
_atomic_write_text(path, json.dumps(data, indent=2, ensure_ascii=False) + "\n")
|
||||
return True
|
||||
|
||||
|
||||
# Match `export MEM0_API_KEY="..."` (or single quotes, or no quotes).
|
||||
# Use [ \t]* (not \s*) for trailing whitespace so a trailing newline at
|
||||
# end-of-file is preserved when MEM0_API_KEY is the last line.
|
||||
_RC_LINE = re.compile(
|
||||
r'^([ \t]*export[ \t]+MEM0_API_KEY[ \t]*=[ \t]*)(["\']?)([^"\'\n]*)(["\']?)[ \t]*$',
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
|
||||
def _update_shell_rc(path: Path, api_key: str) -> bool:
|
||||
"""Update an existing ``export MEM0_API_KEY=...`` line in path."""
|
||||
if not path.is_file():
|
||||
return False
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
except OSError:
|
||||
return False
|
||||
match = _RC_LINE.search(text)
|
||||
if not match:
|
||||
return False # no existing line
|
||||
if match.group(3) == api_key:
|
||||
return False
|
||||
new_text = _RC_LINE.sub(lambda m: f'{m.group(1)}"{api_key}"', text, count=1)
|
||||
_atomic_write_text(path, new_text)
|
||||
return True
|
||||
|
||||
|
||||
def _atomic_write_text(path: Path, content: str) -> None:
|
||||
"""Write content to path atomically (temp + rename)."""
|
||||
dirname = path.parent
|
||||
fd, tmp_path = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=dirname)
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
# Preserve mode if the original existed.
|
||||
if path.exists():
|
||||
os.chmod(tmp_path, path.stat().st_mode & 0o777)
|
||||
os.replace(tmp_path, path)
|
||||
except Exception:
|
||||
with contextlib.suppress(OSError):
|
||||
os.unlink(tmp_path)
|
||||
raise
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
_agent_mode: bool = False
|
||||
_current_command: str = ""
|
||||
_pending_notice: str = ""
|
||||
|
||||
|
||||
def is_agent_mode() -> bool:
|
||||
@@ -22,3 +23,23 @@ def get_current_command() -> str:
|
||||
def set_current_command(name: str) -> None:
|
||||
global _current_command
|
||||
_current_command = name
|
||||
|
||||
|
||||
def capture_notice(notice: str | None) -> None:
|
||||
"""Stash a Mem0 backend notice for end-of-command surfacing.
|
||||
|
||||
Called from the platform backend after each response so the notice can
|
||||
be printed once per command (regardless of how many sub-requests fired).
|
||||
Last-write-wins is fine — the message text is identical across requests.
|
||||
"""
|
||||
global _pending_notice
|
||||
if notice:
|
||||
_pending_notice = notice
|
||||
|
||||
|
||||
def take_notice() -> str:
|
||||
"""Return and clear the pending notice."""
|
||||
global _pending_notice
|
||||
msg = _pending_notice
|
||||
_pending_notice = ""
|
||||
return msg
|
||||
|
||||
@@ -87,7 +87,6 @@ def capture_event(
|
||||
try:
|
||||
from mem0_cli import __version__
|
||||
from mem0_cli.config import CONFIG_FILE, load_config, save_config
|
||||
from mem0_cli.state import is_agent_mode
|
||||
|
||||
config = load_config()
|
||||
distinct_id = pre_resolved_email or _get_distinct_id()
|
||||
@@ -107,6 +106,9 @@ def capture_event(
|
||||
with contextlib.suppress(Exception):
|
||||
save_config(config)
|
||||
|
||||
# M4: every cli.* event carries agent_mode based on the config flag
|
||||
# (unclaimed Agent Mode key). This is the growth-doc property used to
|
||||
# join init → add → search funnels in PostHog.
|
||||
payload = {
|
||||
"api_key": POSTHOG_API_KEY,
|
||||
"distinct_id": distinct_id,
|
||||
@@ -115,7 +117,7 @@ def capture_event(
|
||||
"source": "CLI",
|
||||
"language": "python",
|
||||
"cli_version": __version__,
|
||||
"agent_mode": is_agent_mode(),
|
||||
"agent_mode": bool(config.platform.agent_mode),
|
||||
"python_version": sys.version,
|
||||
"os": sys.platform,
|
||||
"os_version": platform.version(),
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
"""Parity tests for `mem0 init --agent` (Agent Mode bootstrap).
|
||||
|
||||
Mirror of ``cli/node/tests/agent-mode.test.ts`` — both files MUST stay in
|
||||
sync so that the Python and Node CLIs expose an identical surface for the
|
||||
Agent Mode entrypoint. If you add a flag here, add the same assertion on
|
||||
the Node side (and vice versa).
|
||||
|
||||
Network-bound bootstrap is covered by the platform-side E2E suite
|
||||
(``backend/tests/e2e/test_05_agent_mode.py``); these tests only verify
|
||||
the CLI surface that ships in the binary.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*[mKJHABCDfsu]")
|
||||
|
||||
|
||||
def _strip_ansi(text: str) -> str:
|
||||
return _ANSI_RE.sub("", text)
|
||||
|
||||
|
||||
def _run(args: list[str], home_dir: str | None = None) -> subprocess.CompletedProcess:
|
||||
env = os.environ.copy()
|
||||
for key in list(env.keys()):
|
||||
if key.startswith("MEM0_"):
|
||||
del env[key]
|
||||
env.pop("FORCE_COLOR", None)
|
||||
if home_dir:
|
||||
env["HOME"] = home_dir
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "mem0_cli", *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=env,
|
||||
timeout=15,
|
||||
)
|
||||
return subprocess.CompletedProcess(
|
||||
args=result.args,
|
||||
returncode=result.returncode,
|
||||
stdout=_strip_ansi(result.stdout),
|
||||
stderr=_strip_ansi(result.stderr),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def clean_home(tmp_path):
|
||||
return str(tmp_path)
|
||||
|
||||
|
||||
class TestInitFlagSurface:
|
||||
"""`mem0 init --help` must expose the Agent Mode flags."""
|
||||
|
||||
def test_init_help_lists_agent_flag(self):
|
||||
result = _run(["init", "--help"])
|
||||
assert result.returncode == 0
|
||||
assert "--agent" in result.stdout
|
||||
|
||||
def test_init_help_describes_agent_mode(self):
|
||||
result = _run(["init", "--help"])
|
||||
assert result.returncode == 0
|
||||
# Description must mention what --agent actually does so an agent
|
||||
# reading the help can self-discover the bootstrap entrypoint.
|
||||
assert "Agent Mode" in result.stdout or "unattended" in result.stdout.lower()
|
||||
|
||||
def test_init_help_lists_source_flag(self):
|
||||
result = _run(["init", "--help"])
|
||||
assert result.returncode == 0
|
||||
assert "--source" in result.stdout
|
||||
|
||||
def test_init_help_lists_email_and_code(self):
|
||||
# Claim flow flags must remain present alongside Agent Mode flags.
|
||||
result = _run(["init", "--help"])
|
||||
assert result.returncode == 0
|
||||
assert "--email" in result.stdout
|
||||
assert "--code" in result.stdout
|
||||
|
||||
|
||||
class TestArgvPreprocessing:
|
||||
"""`--agent` on `init` must reach init_cmd, not be eaten by the global preprocessor.
|
||||
|
||||
Regression for the bug where the top-level `--agent` JSON-alias was
|
||||
stripped from ``sys.argv`` before Typer could bind it to the init
|
||||
subcommand, making ``mem0 init --agent`` indistinguishable from a
|
||||
plain ``mem0 init`` (interactive wizard).
|
||||
"""
|
||||
|
||||
def test_init_with_agent_reaches_subcommand(self, clean_home):
|
||||
# We can't hit a real backend in unit tests, so we point the CLI at
|
||||
# a guaranteed-dead URL and assert the failure is the bootstrap
|
||||
# request failing — proving the --agent flag was honored and the
|
||||
# bootstrap branch ran, not the interactive wizard.
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "mem0_cli", "init", "--agent"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env={
|
||||
**{k: v for k, v in os.environ.items() if not k.startswith("MEM0_")},
|
||||
"HOME": clean_home,
|
||||
"MEM0_BASE_URL": "http://127.0.0.1:1", # blackhole
|
||||
"FORCE_COLOR": "0",
|
||||
},
|
||||
timeout=15,
|
||||
)
|
||||
combined = _strip_ansi(result.stdout + result.stderr).lower()
|
||||
# Either we got a connection/network error from the bootstrap POST,
|
||||
# or the CLI surfaced an Agent Mode-specific failure message.
|
||||
assert (
|
||||
"agent" in combined
|
||||
or "connect" in combined
|
||||
or "network" in combined
|
||||
or "fetch" in combined
|
||||
or "bootstrap" in combined
|
||||
), f"Expected bootstrap attempt, got: {combined!r}"
|
||||
|
||||
|
||||
class TestJsonEnvelopeParity:
|
||||
"""`mem0 init --agent --json` should produce a JSON envelope on success.
|
||||
|
||||
Without a live backend we can only assert the failure shape: when the
|
||||
backend is unreachable, the CLI must still exit non-zero AND not crash
|
||||
on a Python traceback (which would mean we leaked an exception past
|
||||
the agent-mode handler).
|
||||
"""
|
||||
|
||||
def test_init_agent_json_no_traceback_on_network_failure(self, clean_home):
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "mem0_cli", "init", "--agent", "--json"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env={
|
||||
**{k: v for k, v in os.environ.items() if not k.startswith("MEM0_")},
|
||||
"HOME": clean_home,
|
||||
"MEM0_BASE_URL": "http://127.0.0.1:1",
|
||||
"FORCE_COLOR": "0",
|
||||
},
|
||||
timeout=15,
|
||||
)
|
||||
combined = _strip_ansi(result.stdout + result.stderr)
|
||||
assert "Traceback (most recent call last)" not in combined
|
||||
assert result.returncode != 0
|
||||
|
||||
|
||||
class TestInitInCommandList:
|
||||
"""`mem0 --help` must list `init` so agents walking the top-level help
|
||||
can discover the Agent Mode entrypoint without prior knowledge."""
|
||||
|
||||
def test_top_level_help_lists_init(self):
|
||||
result = _run(["--help"])
|
||||
assert result.returncode == 0
|
||||
assert "init" in result.stdout
|
||||
@@ -0,0 +1,206 @@
|
||||
"""Unit tests for init internals — decision tree primitives + plugin sync.
|
||||
|
||||
These tests exercise the units that the high-level subprocess parity tests in
|
||||
``test_agent_mode.py`` deliberately can't reach:
|
||||
|
||||
- ``_ping_key`` must NOT treat network errors as "invalid key" (else a VPN
|
||||
flap silently mints a new shadow over a working key).
|
||||
- ``plugin_sync`` must only update entries that already exist, preserve
|
||||
trailing newlines, and never mangle other lines.
|
||||
- The 403→ratelimit translation in ``bootstrap_via_backend`` surfaces the
|
||||
real cause instead of DRF's opaque "You do not have permission" string.
|
||||
|
||||
Mirror surface lives in ``cli/node/tests/agent-mode.test.ts``; if you add a
|
||||
behavioral assertion here, mirror it on the Node side and vice versa.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from mem0_cli.commands.init_cmd import _ping_key
|
||||
from mem0_cli.plugin_sync import _update_claude_settings, _update_shell_rc
|
||||
|
||||
# ── _ping_key ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class _Resp:
|
||||
def __init__(self, status_code: int) -> None:
|
||||
self.status_code = status_code
|
||||
|
||||
|
||||
def test_ping_key_200_is_valid(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(httpx, "get", lambda *a, **kw: _Resp(200))
|
||||
assert _ping_key("k", "http://x") is True
|
||||
|
||||
|
||||
def test_ping_key_401_is_invalid(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(httpx, "get", lambda *a, **kw: _Resp(401))
|
||||
assert _ping_key("k", "http://x") is False
|
||||
|
||||
|
||||
def test_ping_key_403_is_invalid(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(httpx, "get", lambda *a, **kw: _Resp(403))
|
||||
assert _ping_key("k", "http://x") is False
|
||||
|
||||
|
||||
def test_ping_key_5xx_is_not_definitively_invalid(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Transient upstream failure must NOT cause a shadow to be minted.
|
||||
monkeypatch.setattr(httpx, "get", lambda *a, **kw: _Resp(503))
|
||||
assert _ping_key("k", "http://x") is True
|
||||
|
||||
|
||||
def test_ping_key_connect_error_prefers_reuse(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Network blip (DNS, captive portal, etc.) — must NOT trigger a re-mint.
|
||||
def boom(*a, **kw):
|
||||
raise httpx.ConnectError("nope")
|
||||
|
||||
monkeypatch.setattr(httpx, "get", boom)
|
||||
assert _ping_key("k", "http://x") is True
|
||||
|
||||
|
||||
def test_ping_key_timeout_prefers_reuse(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def boom(*a, **kw):
|
||||
raise httpx.ReadTimeout("slow")
|
||||
|
||||
monkeypatch.setattr(httpx, "get", boom)
|
||||
assert _ping_key("k", "http://x") is True
|
||||
|
||||
|
||||
# ── plugin_sync._update_shell_rc ──────────────────────────────────────────
|
||||
|
||||
|
||||
def test_shell_rc_updates_existing_export_preserves_trailing_newline(tmp_path) -> None:
|
||||
rc = tmp_path / ".zshrc"
|
||||
rc.write_text('export MEM0_API_KEY="old"\n', encoding="utf-8")
|
||||
changed = _update_shell_rc(rc, "newkey")
|
||||
assert changed is True
|
||||
assert rc.read_text(encoding="utf-8") == 'export MEM0_API_KEY="newkey"\n'
|
||||
|
||||
|
||||
def test_shell_rc_does_not_create_new_export(tmp_path) -> None:
|
||||
rc = tmp_path / ".zshrc"
|
||||
rc.write_text("alias ll='ls -la'\n", encoding="utf-8")
|
||||
changed = _update_shell_rc(rc, "newkey")
|
||||
assert changed is False
|
||||
assert rc.read_text(encoding="utf-8") == "alias ll='ls -la'\n"
|
||||
|
||||
|
||||
def test_shell_rc_preserves_surrounding_content(tmp_path) -> None:
|
||||
rc = tmp_path / ".zshrc"
|
||||
original = "# my zshrc\nalias ll='ls -la'\nexport MEM0_API_KEY='old'\nexport OTHER=keepme\n"
|
||||
rc.write_text(original, encoding="utf-8")
|
||||
_update_shell_rc(rc, "newkey")
|
||||
after = rc.read_text(encoding="utf-8")
|
||||
assert "alias ll='ls -la'\n" in after
|
||||
assert "export OTHER=keepme\n" in after
|
||||
assert "# my zshrc\n" in after
|
||||
assert 'export MEM0_API_KEY="newkey"\n' in after
|
||||
|
||||
|
||||
def test_shell_rc_idempotent_when_already_matching(tmp_path) -> None:
|
||||
rc = tmp_path / ".zshrc"
|
||||
rc.write_text('export MEM0_API_KEY="same"\n', encoding="utf-8")
|
||||
assert _update_shell_rc(rc, "same") is False
|
||||
|
||||
|
||||
def test_shell_rc_missing_file_is_noop(tmp_path) -> None:
|
||||
rc = tmp_path / ".zshrc" # does not exist
|
||||
assert _update_shell_rc(rc, "x") is False
|
||||
|
||||
|
||||
# ── plugin_sync._update_claude_settings ────────────────────────────────────
|
||||
|
||||
|
||||
def test_claude_settings_does_not_create_env_block(tmp_path) -> None:
|
||||
import json
|
||||
|
||||
settings = tmp_path / "settings.json"
|
||||
settings.write_text(json.dumps({"otherKey": 1}), encoding="utf-8")
|
||||
changed = _update_claude_settings(settings, "newkey")
|
||||
assert changed is False
|
||||
# Original content unchanged.
|
||||
assert json.loads(settings.read_text(encoding="utf-8")) == {"otherKey": 1}
|
||||
|
||||
|
||||
def test_claude_settings_does_not_create_mem0_entry_in_existing_env(tmp_path) -> None:
|
||||
import json
|
||||
|
||||
settings = tmp_path / "settings.json"
|
||||
settings.write_text(json.dumps({"env": {"OTHER_KEY": "x"}}), encoding="utf-8")
|
||||
changed = _update_claude_settings(settings, "newkey")
|
||||
assert changed is False
|
||||
|
||||
|
||||
def test_claude_settings_updates_existing_entry(tmp_path) -> None:
|
||||
import json
|
||||
|
||||
settings = tmp_path / "settings.json"
|
||||
settings.write_text(
|
||||
json.dumps({"env": {"MEM0_API_KEY": "old", "OTHER": "y"}}, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
changed = _update_claude_settings(settings, "fresh")
|
||||
assert changed is True
|
||||
data = json.loads(settings.read_text(encoding="utf-8"))
|
||||
assert data["env"]["MEM0_API_KEY"] == "fresh"
|
||||
assert data["env"]["OTHER"] == "y" # other keys preserved
|
||||
|
||||
|
||||
def test_claude_settings_idempotent(tmp_path) -> None:
|
||||
import json
|
||||
|
||||
settings = tmp_path / "settings.json"
|
||||
settings.write_text(json.dumps({"env": {"MEM0_API_KEY": "same"}}), encoding="utf-8")
|
||||
assert _update_claude_settings(settings, "same") is False
|
||||
|
||||
|
||||
def test_claude_settings_malformed_json_is_noop(tmp_path) -> None:
|
||||
settings = tmp_path / "settings.json"
|
||||
settings.write_text("{ this is not json", encoding="utf-8")
|
||||
assert _update_claude_settings(settings, "x") is False
|
||||
|
||||
|
||||
# ── bootstrap rate-limit translation ──────────────────────────────────────
|
||||
|
||||
|
||||
def test_bootstrap_403_permission_surfaces_ratelimit(monkeypatch, capsys) -> None:
|
||||
"""DRF 403 'You do not have permission' must be translated to the daily limit message."""
|
||||
from mem0_cli.commands.agent_mode_cmd import bootstrap_via_backend
|
||||
from mem0_cli.config import Mem0Config
|
||||
|
||||
fake_resp = MagicMock()
|
||||
fake_resp.status_code = 403
|
||||
fake_resp.text = '{"detail": "You do not have permission to perform this action."}'
|
||||
fake_resp.json = MagicMock(
|
||||
return_value={"detail": "You do not have permission to perform this action."}
|
||||
)
|
||||
|
||||
class _Client:
|
||||
def __init__(self, *a, **kw):
|
||||
pass
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *a):
|
||||
return False
|
||||
|
||||
def post(self, *a, **kw):
|
||||
return fake_resp
|
||||
|
||||
monkeypatch.setattr(httpx, "Client", _Client)
|
||||
cfg = Mem0Config()
|
||||
cfg.platform.base_url = "https://api.mem0.ai"
|
||||
import typer
|
||||
|
||||
with pytest.raises(typer.Exit):
|
||||
bootstrap_via_backend(cfg)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
combined = captured.out + captured.err
|
||||
assert "Daily Agent Mode signup limit reached" in combined
|
||||
assert "permission to perform this action" not in combined
|
||||
@@ -5,3 +5,5 @@ openapi: get /v1/event/{event_id}/
|
||||
---
|
||||
|
||||
Retrieve details about a specific event by passing its `event_id`. This endpoint is particularly helpful for tracking the status, payload, and completion details of asynchronous memory operations.
|
||||
|
||||
For `POST /v3/memories/add/`, the event confirms that the write pipeline completed. Temporal reasoning enrichment runs asynchronously by default, so the event may be `SUCCEEDED` slightly before temporal ranking signals are available to subsequent `search` calls.
|
||||
|
||||
@@ -83,4 +83,3 @@ The request is queued for background processing. The response contains an `event
|
||||
<Info>
|
||||
Poll the event status via `GET /v1/event/{event_id}/`. Status will be `SUCCEEDED` or `FAILED` once processing completes.
|
||||
</Info>
|
||||
|
||||
|
||||
@@ -64,4 +64,3 @@ memories = client.get_all(
|
||||
<Info>
|
||||
The response is a paginated envelope with `count`, `next`, `previous`, and `results`. Use `page` and `page_size` query params to step through results.
|
||||
</Info>
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ related_memories = client.search(
|
||||
{
|
||||
"id": "ea925981-272f-40dd-b576-be64e4871429",
|
||||
"memory": "Likes to play cricket and plays cricket on weekends.",
|
||||
"user_id": "alice",
|
||||
"metadata": {
|
||||
"category": "hobbies"
|
||||
},
|
||||
|
||||
@@ -109,6 +109,19 @@ client.project.update(
|
||||
)
|
||||
```
|
||||
|
||||
#### Toggle Memory Decay
|
||||
|
||||
`decay` is a per-project boolean that turns on [Memory Decay](/platform/features/memory-decay) — a search-time ranking bias that reinforces recently-accessed memories and gently dampens stale ones. The flag is `false` by default; set it via the same project-update endpoint:
|
||||
|
||||
```bash cURL
|
||||
curl -X PATCH https://api.mem0.ai/api/v1/orgs/organizations/$ORG_ID/projects/$PROJECT_ID/ \
|
||||
-H "Authorization: Token $MEM0_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"decay": true}'
|
||||
```
|
||||
|
||||
The current state is returned on every project read (and supports `?fields=decay` for a minimal response). Toggling has no effect on stored memories, only on how v3 search ranks them.
|
||||
|
||||
### Delete Project
|
||||
|
||||
<Warning>
|
||||
|
||||
@@ -4,6 +4,34 @@ description: "Major product launches, headline features, and milestones for Mem0
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
<Update label="2026-05-13" description="Temporal Reasoning for Mem0 Platform v3">
|
||||
|
||||
**Temporal Reasoning — Time-Aware Retrieval for Platform v3**
|
||||
|
||||
Mem0 Platform v3 can now interpret time-aware memories and queries so assistants retrieve the right information for questions about the past, upcoming plans, and current state.
|
||||
|
||||
- **Time-aware search intent** — Queries like `last week`, `upcoming`, `right now`, and `as of March 2025` return contextually appropriate results automatically
|
||||
- **Enabled by default** — No per-request toggle required for v3 writes or searches
|
||||
- **Anchored relative queries** — `reference_date` anchors relative search phrases for tests, backfills, and reproducible demos
|
||||
- **Normal response shape** — Temporal reasoning affects ranking while preserving existing client response patterns
|
||||
|
||||
See [Temporal Reasoning](/platform/features/temporal-reasoning) for usage details.
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-05-08" description="Memory Decay">
|
||||
|
||||
**Memory Decay — Recently-Used Memories Surface Higher, Automatically**
|
||||
|
||||
Per-project search-time ranking bias that boosts recently-touched memories and gently dampens stale ones. Off by default; opt in per project via the `decay` field on the project endpoint, or via `client.project.update(decay=True)` in the SDKs (Python `v2.0.2` / TypeScript `v3.0.3`).
|
||||
|
||||
- **Soft bias, never a filter.** The scaling factor stays in `0.3×–1.5×`. Decay can reorder candidates but never zeros them out — anything that surfaced before decay can still surface after.
|
||||
- **Reinforcement loop.** Every memory returned in a search has its access history updated, so frequently-used facts naturally float to the top over time.
|
||||
- **Public score still clamped to `[0, 1]`.** Existing API contract preserved; no client-side changes needed.
|
||||
- **v3 search only**, fully reversible. See [Memory Decay docs](/platform/features/memory-decay).
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-04-14" description="Mem0 SDK v2.0.0 / v3.0.0">
|
||||
|
||||
**New Memory Algorithm — State-of-the-Art Accuracy at ~3-4x Lower Cost**
|
||||
@@ -99,4 +127,4 @@ Major expansion of the provider ecosystem:
|
||||
|
||||
First skill launch — a dedicated Mem0 skill providing platform API reference, quickstart patterns, and integration examples directly inside agent sessions. Available on [skills.sh](https://skills.sh) for any compatible AI coding agent.
|
||||
|
||||
</Update>
|
||||
</Update>
|
||||
|
||||
@@ -4,6 +4,36 @@ description: "Release notes for the OpenClaw plugin and agent harness."
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
<Update label="2026-04-29" description="v1.0.11">
|
||||
|
||||
**New Features:**
|
||||
- **Skills-mode auto-setup:** `enableSkillsConfig()` now runs automatically after onboarding — enables triage, recall (with reranking + keyword search), and dream consolidation with `tools.profile = "full"` and disables the built-in session-memory hook to avoid conflicts
|
||||
- **Memory runtime capability:** Plugin now exposes `runtime.getMemorySearchManager()` and `resolveMemoryBackendConfig()` on the registered memory capability, enabling OpenClaw gateway to query memory status and backend config directly
|
||||
- **Dimension-aware collections:** OSS wizard detects embedder dimension changes and creates a new collection (`mem0_<dims>d`) automatically, with a warning about old memories being inaccessible under the new embedder
|
||||
- **Tool documentation in skills:** Both `memory-triage` and `memory-dream` SKILL.md files now include full tool reference sections listing all available tools with parameters
|
||||
|
||||
**Improvements:**
|
||||
- **Auto-capture and auto-recall default to enabled:** `autoCapture` and `autoRecall` now default to `true` (was `false`). Manifest descriptions updated accordingly. Ignored in skills mode
|
||||
- **`memory_update` over delete+add:** Skills now prefer `memory_update` for in-place edits — atomic and preserves edit history. Consolidation pattern updated: update best memory, delete redundant ones
|
||||
- **Search threshold lowered:** Default `searchThreshold` reduced from `0.5` to `0.1` for broader recall. Removed hardcoded `0.6` recall-specific override — all searches now use the configured threshold
|
||||
- **Embedder dimension propagation:** Vector store config auto-resolves dimensions from embedder config when not explicitly set. Syncs `dimension` and `embeddingModelDims` fields for Qdrant/PGVector compatibility
|
||||
- **Config file write safety:** `writeFullConfig()` now re-reads and deep-merges the `plugins` section before writing, preserving `installs` and `slots` written by the OpenClaw gateway
|
||||
- **Additional embedder models:** Added `mxbai-embed-large` (1024), `all-minilm` (384), and `snowflake-arctic-embed` (1024) to known embedder dimensions
|
||||
|
||||
**Security:**
|
||||
- Bumped `protobufjs` to `>=7.5.5` via pnpm overrides (GHSA-xq3m-2v4x-88gg) ([#5012](https://github.com/mem0ai/mem0/pull/5012))
|
||||
|
||||
**Fixes:**
|
||||
- Moved `bootstrapTelemetryFlag()` and removed `ensureInstallRecord()` from module-level side effects — both now run inside `register()` to avoid crashes when loaded outside OpenClaw gateway
|
||||
- Fixed OSS history DB path resolution: absolute paths no longer passed through `resolvePath()`, preventing double-prefix bugs
|
||||
- Manifest `providerAuthEnvVars` replaced with spec-compliant `setup.providers` format using `id` + `envVars`
|
||||
|
||||
**Dependencies:**
|
||||
- Bumped `mem0ai` from `3.0.1` to `3.0.2`
|
||||
- Bumped `pluginApi` and `minGatewayVersion` compat to `>=2026.4.24`
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-04-23" description="v1.0.10">
|
||||
|
||||
**Security:**
|
||||
|
||||
@@ -4,6 +4,24 @@ description: "Release notes for the Mem0 hosted platform — backend, dashboard,
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
<Update label="2026-05-13" description="">
|
||||
|
||||
**New Features:**
|
||||
- **Memory:** Added Temporal Reasoning for Platform v3 to improve ranking for time-aware queries such as `last week`, `upcoming`, `right now`, and `as of ...`
|
||||
- **Search:** Added `reference_date` support to anchor relative temporal queries for tests, backfills, and reproducible demos
|
||||
|
||||
**Improvements:**
|
||||
- **API:** Temporal reasoning preserves the normal client response shape for search and get-all results
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-05-04" description="">
|
||||
|
||||
**New Features:**
|
||||
- **Memory Decay:** Per-project search-time ranking bias that boosts recently-used memories and gently dampens stale ones. Opt-in via `decay` on the project endpoint; off by default. The scaling factor stays in `0.3×–1.5×`, the public `score` remains clamped to `[0, 1]`, and the bias never filters a candidate out. See [Memory Decay docs](/platform/features/memory-decay).
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-04-16" description="">
|
||||
|
||||
**Improvements:**
|
||||
@@ -294,4 +312,3 @@ mode: "wide"
|
||||
- **Core:** Fixed unicode error in user_id, agent_id, run_id and app_id
|
||||
|
||||
</Update>
|
||||
|
||||
|
||||
+51
-1
@@ -7,6 +7,20 @@ mode: "wide"
|
||||
<Tabs>
|
||||
<Tab title="Python">
|
||||
|
||||
<Update label="2026-05-08" description="v2.0.2">
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Telemetry:** Stitch OSS and platform PostHog identities on `MemoryClient` init so `$identify` events fire and a single user is no longer tracked as two or three disconnected personas ([#5040](https://github.com/mem0ai/mem0/pull/5040))
|
||||
- **Security:** Harden against SQL injection and prompt injection ([#4997](https://github.com/mem0ai/mem0/pull/4997))
|
||||
|
||||
**New Features:**
|
||||
- **SDK:** Expose `decay` on `project.update` ([#5062](https://github.com/mem0ai/mem0/pull/5062))
|
||||
|
||||
**Improvements:**
|
||||
- **Plugin:** Hand `mem0` search decisions to the agent ([#4992](https://github.com/mem0ai/mem0/pull/4992))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-04-25" description="v2.0.1">
|
||||
|
||||
**Bug Fixes:**
|
||||
@@ -41,7 +55,7 @@ mode: "wide"
|
||||
**Breaking Changes:**
|
||||
- **`add()` returns ADD-only events** — No more `"UPDATE"` or `"DELETE"` events. Memories accumulate; nothing is overwritten ([#4805](https://github.com/mem0ai/mem0/pull/4805))
|
||||
- **`search()` default `threshold` is now `0.1`** — Pass `threshold=0.0` for previous behavior ([#4805](https://github.com/mem0ai/mem0/pull/4805))
|
||||
- **`search()` `score` is now a combined multi-signal score** — The top-level `score` fuses semantic similarity, BM25 keyword match, and entity boost into one value. Absolute numbers shift versus the old raw cosine score; retune any hard thresholds against representative queries. Per-signal scores are not exposed on the response ([#4805](https://github.com/mem0ai/mem0/pull/4805), [#4836](https://github.com/mem0ai/mem0/pull/4836))
|
||||
- **`search()` `score` is now a combined multi-signal score** — The top-level `score` fuses semantic similarity, BM25 keyword match, entity signals, and temporal boosts into one value. Absolute numbers shift versus the old raw cosine score; retune any hard thresholds against representative queries ([#4805](https://github.com/mem0ai/mem0/pull/4805), [#4836](https://github.com/mem0ai/mem0/pull/4836))
|
||||
- **`search()` default `rerank` is now `False`** — Pass `rerank=True` for previous behavior ([#4805](https://github.com/mem0ai/mem0/pull/4805))
|
||||
- **`top_k` default changed 100 → 20** in `Memory.get_all()` and `Memory.search()` (sync + async). Pass `top_k=100` explicitly to restore the old behavior ([#4843](https://github.com/mem0ai/mem0/pull/4843))
|
||||
- **Entity ID validation:** `user_id` / `agent_id` / `run_id` are trimmed; empty-string and whitespace-only values now raise `ValueError` ([#4843](https://github.com/mem0ai/mem0/pull/4843))
|
||||
@@ -910,6 +924,18 @@ See the [OSS v1 to v2 migration guide](https://docs.mem0.ai/migration/oss-v1-to-
|
||||
</Tab>
|
||||
|
||||
<Tab title="TypeScript">
|
||||
<Update label="2026-05-08" description="v3.0.3">
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Telemetry:** Stitch OSS and platform PostHog identities on `MemoryClient` init so `$identify` events fire and a single user is no longer tracked as two or three disconnected personas ([#5040](https://github.com/mem0ai/mem0/pull/5040))
|
||||
- **Vector Stores:** Fix inverted vector distance in PGVector implementation ([#4944](https://github.com/mem0ai/mem0/pull/4944))
|
||||
- **Security:** Harden against SQL injection and prompt injection ([#4997](https://github.com/mem0ai/mem0/pull/4997))
|
||||
|
||||
**New Features:**
|
||||
- **SDK:** Expose `decay` on `project.update` ([#5062](https://github.com/mem0ai/mem0/pull/5062))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-04-25" description="v3.0.2">
|
||||
|
||||
**Bug Fixes:**
|
||||
@@ -1297,6 +1323,30 @@ See the [TypeScript SDK migration guide](https://docs.mem0.ai/migration/ts-v2-to
|
||||
|
||||
<Tab title="CLI">
|
||||
|
||||
<Update label="2026-05-16" description="Python v0.2.6 / Node v0.2.6">
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Claim flow error message:** The `email_already_claimed` tip in `mem0 init --email` previously suggested running `mem0 link <key>` — a command that doesn't exist. Replaced with honest copy pointing the user to sign in at app.mem0.ai with their existing credentials ([#5152](https://github.com/mem0ai/mem0/pull/5152))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-05-14" description="Python v0.2.5 / Node v0.2.5">
|
||||
|
||||
**New Features:**
|
||||
- **Agent Mode (`mem0 init --agent`):** Zero-friction signup for AI agents — mints a working Mem0 API key in under 5 seconds with no email, no dashboard, no OTP. Returns an unclaimed shadow account the human can later claim with `mem0 init --email <their-email>` (memories preserved, same key keeps working) ([#5123](https://github.com/mem0ai/mem0/pull/5123))
|
||||
- **Self-declared agent identity:** Agents pass `--agent-caller <name>` (e.g. `claude-code`, `cursor`, `codex`) on `mem0 init --agent` so signups attribute to the right tool in analytics. Proof Editor-style — the agent declares itself rather than the CLI sniffing it from env vars ([#5123](https://github.com/mem0ai/mem0/pull/5123))
|
||||
- **`mem0 identify <name>`:** New subcommand to self-tag an Agent Mode key after the fact when the agent forgot to pass `--agent-caller` on init. Idempotent — re-running just overwrites ([#5123](https://github.com/mem0ai/mem0/pull/5123))
|
||||
- **Plugin sync:** `~/.claude/settings.json::env::MEM0_API_KEY` and `~/.zshrc`/`.bashrc` `export MEM0_API_KEY=` lines stay in sync with `~/.mem0/config.json` automatically. Idempotent — only updates EXISTING entries, never creates new ones ([#5123](https://github.com/mem0ai/mem0/pull/5123))
|
||||
- **Claim flow:** `mem0 init --email <email>` claims an existing Agent Mode shadow via OTP. Upgrade-in-place — the API key never changes, memories transfer to the human's account ([#5123](https://github.com/mem0ai/mem0/pull/5123))
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Decision tree network resilience:** `pingKey` now distinguishes network errors from invalid keys — returns false ONLY on HTTP 401/403, returns true on connection failures / timeouts / 5xx. Prevents a VPN flap from silently rotating the user's API key and rewriting plugin-sync targets ([#5123](https://github.com/mem0ai/mem0/pull/5123))
|
||||
- **Rate-limit error clarity:** DRF's opaque `"You do not have permission"` 403 from Agent Mode rate limits is now translated to `"Daily Agent Mode signup limit reached for this network (5/day). Try again from a different IP or after midnight UTC."` ([#5123](https://github.com/mem0ai/mem0/pull/5123))
|
||||
- **JSON envelope `command` field:** `mem0 init --agent --json` error envelopes now populate the `command` field correctly instead of returning an empty string ([#5123](https://github.com/mem0ai/mem0/pull/5123))
|
||||
- **Bootstrap envelope validation:** Defends against partial/malformed backend responses (e.g. `{api_key: null}`) silently persisting null/undefined into typed string fields ([#5123](https://github.com/mem0ai/mem0/pull/5123))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-04-22" description="Python v0.2.4 / Node v0.2.4">
|
||||
|
||||
**New Features:**
|
||||
|
||||
@@ -156,6 +156,10 @@ const memories = memory.search("food preferences", {
|
||||
Expect an array of memory documents. Platform responses include vectors, metadata, and timestamps; OSS returns your stored schema.
|
||||
</Info>
|
||||
|
||||
<Note>
|
||||
On Mem0 Platform v3, time-aware queries use Temporal Reasoning internally while preserving the normal search response shape. See <Link href="/platform/features/temporal-reasoning">Temporal Reasoning</Link>.
|
||||
</Note>
|
||||
|
||||
## Filter patterns
|
||||
|
||||
Filters help narrow down search results. Common use cases:
|
||||
@@ -251,4 +255,4 @@ For the full list of filter logic, comparison operators, and optional search par
|
||||
icon="rocket"
|
||||
href="/cookbooks/operations/support-inbox"
|
||||
/>
|
||||
</CardGroup>
|
||||
</CardGroup>
|
||||
|
||||
+6
-3
@@ -40,6 +40,7 @@
|
||||
"icon": "rocket",
|
||||
"pages": [
|
||||
"platform/overview",
|
||||
"platform/agent-signup",
|
||||
"vibecoding",
|
||||
"platform/mem0-mcp",
|
||||
"platform/cli",
|
||||
@@ -72,7 +73,8 @@
|
||||
"platform/features/entity-scoped-memory",
|
||||
"platform/features/async-client",
|
||||
"platform/features/multimodal-support",
|
||||
"platform/features/custom-categories"
|
||||
"platform/features/custom-categories",
|
||||
"platform/features/temporal-reasoning"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -83,7 +85,8 @@
|
||||
"platform/advanced-memory-operations",
|
||||
"platform/features/criteria-retrieval",
|
||||
"platform/features/contextual-add",
|
||||
"platform/features/custom-instructions"
|
||||
"platform/features/custom-instructions",
|
||||
"platform/features/memory-decay"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1143,4 +1146,4 @@
|
||||
"destination": "/introduction"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+81
-68
@@ -30,91 +30,100 @@ export MEM0_API_KEY="m0-your-api-key"
|
||||
|
||||
## Installation
|
||||
|
||||
### Option A — Repo Marketplace (Recommended for Teams)
|
||||
### Option A — Direct MCP (Recommended)
|
||||
|
||||
Add a `.agents/plugins/marketplace.json` to your repository root:
|
||||
The fastest way to connect Codex to Mem0 — no downloads, no marketplace. Codex reads MCP servers from `~/.codex/config.toml` as TOML. Add:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "mem0-plugins",
|
||||
"interface": {
|
||||
"displayName": "Mem0 Plugins"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "mem0",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./plugins/mem0"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "Productivity"
|
||||
}
|
||||
]
|
||||
}
|
||||
```toml
|
||||
[mcp_servers.mem0]
|
||||
url = "https://mcp.mem0.ai/mcp"
|
||||
bearer_token_env_var = "MEM0_API_KEY"
|
||||
```
|
||||
|
||||
Then in Codex, browse the repo's plugin directory and install Mem0.
|
||||
Make sure `MEM0_API_KEY` is exported in the shell you launch Codex from, then restart Codex.
|
||||
|
||||
### Option B — Personal Marketplace
|
||||
<Info>
|
||||
Codex's `codex mcp add` CLI only supports stdio MCP servers. Because Mem0's MCP is HTTP/streamable, you configure it by editing `config.toml` directly (or via the **Plugins → Connect to a custom MCP → Streamable HTTP** UI in the Codex app).
|
||||
</Info>
|
||||
|
||||
Add to `~/.agents/plugins/marketplace.json`:
|
||||
### Option B — Sideload the Plugin (Advanced)
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "mem0-plugins",
|
||||
"interface": {
|
||||
"displayName": "Mem0 Plugins"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "mem0",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "/path/to/mem0-plugin"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "Productivity"
|
||||
}
|
||||
]
|
||||
}
|
||||
For the full plugin experience — MCP server **plus** the Mem0 SDK skill, memory protocol skill, and opt-in lifecycle hooks — sideload the plugin from a local clone. The Mem0 repo already ships a marketplace manifest at [`.agents/plugins/marketplace.json`](https://github.com/mem0ai/mem0/blob/main/.agents/plugins/marketplace.json), so there's no JSON to author by hand. This follows the Codex [build-plugins](https://developers.openai.com/codex/plugins/build) local-testing workflow.
|
||||
|
||||
<Info>
|
||||
Don't combine Option B with Option A. The plugin manifest declares its MCP server via [`.codex-mcp.json`](https://github.com/mem0ai/mem0/blob/main/mem0-plugin/.codex-mcp.json), so Codex auto-registers the `mem0` MCP server when the plugin loads. Adding the same `[mcp_servers.mem0]` block to `~/.codex/config.toml` will create a duplicate registration.
|
||||
</Info>
|
||||
|
||||
**Step 1.** Clone the Mem0 repository anywhere on disk:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/mem0ai/mem0.git ~/codex-plugins/mem0-source
|
||||
```
|
||||
|
||||
### Option C — Manual MCP Configuration
|
||||
**Step 2.** Register the bundled marketplace with Codex's CLI:
|
||||
|
||||
Add to your Codex MCP config:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mem0": {
|
||||
"type": "http",
|
||||
"url": "https://mcp.mem0.ai/mcp/",
|
||||
"headers": {
|
||||
"Authorization": "Token ${MEM0_API_KEY}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```bash
|
||||
codex plugin marketplace add ~/codex-plugins/mem0-source
|
||||
```
|
||||
|
||||
This points Codex at the repo's `.agents/plugins/marketplace.json`. The bundled file uses `path: "./mem0-plugin"`, which Codex resolves relative to the clone root.
|
||||
|
||||
<Info>
|
||||
**Why we recommend this over hand-authoring `~/.agents/plugins/marketplace.json`:** Codex requires `source.path` in any marketplace manifest to be **relative** (starting with `./`) and **inside the marketplace root**. The repo's bundled manifest already satisfies this — the marketplace root is the clone directory, and `mem0-plugin/` lives inside it. With a personal `~/.agents/plugins/marketplace.json`, the root is `~/` and the clone has to live under `~/` too. The CLI form sidesteps that constraint.
|
||||
</Info>
|
||||
|
||||
**Step 3.** Restart Codex, run `/plugins`, browse the `Mem0 Plugins` marketplace, and install **Mem0**.
|
||||
|
||||
**Step 4 (optional) — enable lifecycle hooks.** Codex doesn't auto-wire hooks from plugin manifests; it only reads them from `~/.codex/hooks.json` (or `<repo>/.codex/hooks.json`). Run the bundled installer once to merge the Mem0 entries into your global hooks file:
|
||||
|
||||
```bash
|
||||
python3 ~/codex-plugins/mem0-source/mem0-plugin/scripts/install_codex_hooks.py
|
||||
```
|
||||
|
||||
Then enable the hooks feature flag in `~/.codex/config.toml`:
|
||||
|
||||
```toml
|
||||
[features]
|
||||
codex_hooks = true
|
||||
```
|
||||
|
||||
Restart Codex. The installer registers three hooks pointing at scripts inside your clone:
|
||||
|
||||
| Event | Behavior |
|
||||
|-------|----------|
|
||||
| `SessionStart` | Loads prior memories as bootstrap context |
|
||||
| `UserPromptSubmit` | Injects relevant memories before each prompt |
|
||||
| `Stop` | Reminds the agent to persist learnings at turn end |
|
||||
|
||||
Re-running the installer is idempotent. To remove the hooks: `python3 ~/codex-plugins/mem0-source/mem0-plugin/scripts/install_codex_hooks.py --uninstall`.
|
||||
|
||||
<Warning>
|
||||
The hooks file stores absolute paths into your clone (e.g. `~/codex-plugins/mem0-source/mem0-plugin/scripts/...`). If you move or delete the clone, the hooks will break silently — re-run the installer from the new location, or run `--uninstall` first.
|
||||
</Warning>
|
||||
|
||||
### Managing the Plugin
|
||||
|
||||
Codex provides CLI commands for managing marketplaces after install:
|
||||
|
||||
```bash
|
||||
codex plugin marketplace upgrade # pull latest plugin versions
|
||||
codex plugin marketplace remove mem0-plugins # unregister the marketplace
|
||||
```
|
||||
|
||||
To pull updates to the plugin source itself, `git pull` inside your clone (`~/codex-plugins/mem0-source`) and then run `codex plugin marketplace upgrade` to refresh Codex's plugin cache. Plugins are cached at `~/.codex/plugins/cache/<marketplace>/<plugin>/<version>/`.
|
||||
|
||||
<Info icon="check">
|
||||
Start a new Codex task and ask: *"List my mem0 entities"* or *"Search my memories for hello"*. If the `mem0` tools appear and respond, you're all set.
|
||||
After either option, start a new Codex task and ask: *"List my mem0 entities"* or *"Search my memories for hello"*. If the `mem0` tools appear and respond, you're all set.
|
||||
</Info>
|
||||
|
||||
## What's Included
|
||||
|
||||
| Component | Plugin Install | MCP Only |
|
||||
|-----------|:--------------:|:--------:|
|
||||
| Component | Sideloaded Plugin | Direct MCP |
|
||||
|-----------|:-----------------:|:----------:|
|
||||
| MCP Server (9 memory tools) | Yes | Yes |
|
||||
| Memory Protocol Skill | Yes | No |
|
||||
| Mem0 SDK Skill | Yes | No |
|
||||
| Lifecycle Hooks (opt-in) | Yes | No |
|
||||
|
||||
## Available MCP Tools
|
||||
|
||||
@@ -134,7 +143,7 @@ Once installed, the following tools are available in every Codex session:
|
||||
|
||||
## Memory Protocol Skill
|
||||
|
||||
Codex uses a skill-based approach instead of lifecycle hooks. When installed via the plugin marketplace, the memory protocol skill instructs the agent to:
|
||||
When the plugin is sideloaded, the memory protocol skill instructs the agent to:
|
||||
|
||||
### On Every New Task
|
||||
1. Call `search_memories` with a query related to the current task to load relevant context
|
||||
@@ -199,8 +208,12 @@ You: Add WebSocket support for real-time notification delivery.
|
||||
|
||||
- **"Connection failed"** — Verify `MEM0_API_KEY` is set in your shell: `echo $MEM0_API_KEY`
|
||||
- **No tools appearing** — Restart your Codex session after plugin installation
|
||||
- **Plugin not found** — Ensure `.agents/plugins/marketplace.json` is at the repository root and `source.path` points to the correct plugin directory
|
||||
- **Skills not loading** — Verify the `skills` field in `plugin.json` points to a valid directory containing `SKILL.md` files
|
||||
- **Duplicate `mem0` MCP server / "tool collision" errors** — You combined Option A (Direct MCP) with Option B (sideload). The sideloaded plugin auto-registers `mem0` from `.codex-mcp.json`, so remove the `[mcp_servers.mem0]` block from `~/.codex/config.toml`.
|
||||
- **`plugin/read failed in TUI`** — Codex can't find the plugin directory the marketplace points at. If you used `codex plugin marketplace add <path>`, confirm the path is your clone root and that `<clone>/.agents/plugins/marketplace.json` exists. If you hand-authored `~/.agents/plugins/marketplace.json`, `source.path` must be relative (start with `./`), inside the marketplace root (`~/` for personal installs), and end in `mem0-plugin` — e.g. `"./codex-plugins/mem0-source/mem0-plugin"`.
|
||||
- **Plugin not found in `/plugins`** — Run `codex plugin marketplace add ~/path/to/clone` again, or confirm the marketplace was registered with `codex plugin marketplace remove mem0-plugins` then re-add.
|
||||
- **Skills not loading** — Verify the `skills` field in `plugin.json` points to a valid directory containing `SKILL.md` files.
|
||||
- **Hooks not firing** — Confirm `codex_hooks = true` is in `~/.codex/config.toml` under `[features]`, and that `~/.codex/hooks.json` contains the Mem0 entries (re-run the installer if not). Restart Codex after enabling the flag.
|
||||
- **Hooks broke after moving the clone** — The installer bakes absolute paths into `~/.codex/hooks.json` pointing at scripts inside your clone. If you moved or renamed the clone directory, run `python3 <new-clone>/mem0-plugin/scripts/install_codex_hooks.py` from the new location — the installer is idempotent and replaces the old entries.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Mem0 MCP Setup" icon="puzzle-piece" href="/platform/mem0-mcp">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: OpenClaw
|
||||
description: "Add long-term memory to OpenClaw agents using the Mem0 plugin with auto-recall and auto-capture support."
|
||||
description: "Add long-term memory to OpenClaw agents using the Mem0 plugin with skills-based memory extraction and recall."
|
||||
---
|
||||
|
||||
Add long-term memory to [OpenClaw](https://github.com/openclaw/openclaw) agents with the `@mem0/openclaw-mem0` plugin. Your agent forgets everything between sessions — this plugin fixes that by automatically watching conversations, extracting what matters, and bringing it back when relevant.
|
||||
@@ -12,11 +12,12 @@ Add long-term memory to [OpenClaw](https://github.com/openclaw/openclaw) agents
|
||||
</Frame>
|
||||
|
||||
The plugin provides:
|
||||
1. **Auto-Recall** — Before the agent responds, memories matching the current message are injected into context
|
||||
2. **Auto-Capture** — After the agent responds, the exchange is sent to Mem0 which decides what's worth keeping
|
||||
3. **Agent Tools** — Eight tools for explicit memory operations during conversations
|
||||
1. **Triage** — The agent extracts durable facts from conversations using a structured protocol with importance gates and domain overlays
|
||||
2. **Recall** — Before each turn, relevant memories are retrieved with reranking and injected into context
|
||||
3. **Dream** — Periodic memory consolidation: merges duplicates, resolves conflicts, prunes stale entries
|
||||
4. **Agent Tools** — Eight tools for explicit memory operations during conversations
|
||||
|
||||
Both auto-recall and auto-capture are opt-in (`autoRecall: true`, `autoCapture: true` in config). Once enabled, they run silently with no manual intervention required.
|
||||
Skills mode, `autoRecall`, and `autoCapture` are all enabled by default during `openclaw mem0 init`.
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -24,12 +25,12 @@ Check your OpenClaw version:
|
||||
|
||||
```bash
|
||||
openclaw --version
|
||||
# OpenClaw 2026.4.15 (041266a)
|
||||
# OpenClaw 2026.4.25 (aa36ee6)
|
||||
```
|
||||
|
||||
| OpenClaw Version | Plugin Support |
|
||||
|------------------|----------------|
|
||||
| `>= 2026.4.15` | Fully supported |
|
||||
| `>= 2026.4.25` | Fully supported |
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -100,9 +101,9 @@ You no longer need manual config editing to get started. Everything happens insi
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
That's it. No API key, no config file editing, no environment variables. The plugin is now active and auto-capture and auto-recall are running on every turn.
|
||||
That's it. No API key, no config file editing, no environment variables. The plugin is now active with skills-based memory (triage, recall, and dream) running automatically.
|
||||
|
||||
<Note>The chat flow uses the same underlying config as manual setup — it writes `apiKey` and `userId` into `openclaw.json` for you. You can still open the file to inspect or override values afterward.</Note>
|
||||
<Note>The chat flow uses the same underlying config as manual setup — it writes `apiKey`, `userId`, and `skills` config into `openclaw.json` for you. You can still open the file to inspect or override values afterward.</Note>
|
||||
|
||||
#### Option 2: Manual Config
|
||||
|
||||
@@ -131,7 +132,19 @@ That's it. No API key, no config file editing, no environment variables. The plu
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"apiKey": "${MEM0_API_KEY}",
|
||||
"userId": "alice" // any unique identifier you choose for this user
|
||||
"userId": "alice", // any unique identifier you choose for this user
|
||||
"skills": {
|
||||
"triage": { "enabled": true },
|
||||
"recall": {
|
||||
"enabled": true,
|
||||
"tokenBudget": 1500,
|
||||
"rerank": true,
|
||||
"keywordSearch": true,
|
||||
"identityAlwaysInclude": true
|
||||
},
|
||||
"dream": { "enabled": true },
|
||||
"domain": "companion"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -328,8 +341,8 @@ openclaw mem0 status --json
|
||||
|-----|------|---------|-------------|
|
||||
| `mode` | `"platform"` \| `"open-source"` | `"platform"` | Which backend to use |
|
||||
| `userId` | `string` | OS username | Scope memories per user |
|
||||
| `autoRecall` | `boolean` | `false` | Inject memories before each turn (opt-in) |
|
||||
| `autoCapture` | `boolean` | `false` | Store facts after each turn (opt-in) |
|
||||
| `autoRecall` | `boolean` | `true` | Inject memories before each turn. Ignored when `skills` is configured. |
|
||||
| `autoCapture` | `boolean` | `true` | Store facts after each turn. Ignored when `skills` is configured. |
|
||||
| `topK` | `number` | `5` | Max memories per recall |
|
||||
| `searchThreshold` | `number` | `0.3` | Min similarity (0–1) |
|
||||
|
||||
@@ -426,9 +439,11 @@ If `openclaw plugins update` fails:
|
||||
| **Platform** | Conversations sent to `api.mem0.ai` for extraction and storage | Mem0 cloud |
|
||||
| **Open-source** | Embeddings generated via configured provider (default: OpenAI API). Vectors stored locally. | `~/.mem0/vector_store.db` (SQLite) |
|
||||
|
||||
### Enabling Auto-Capture and Auto-Recall
|
||||
### Auto-Capture and Auto-Recall
|
||||
|
||||
Auto-capture and auto-recall are disabled by default (opt-in). To enable either or both:
|
||||
Auto-capture and auto-recall are **enabled by default**. When skills mode is configured (the default after `openclaw mem0 init`), these are ignored in favor of the skills-based triage/recall/dream protocol.
|
||||
|
||||
To disable either:
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -436,8 +451,8 @@ Auto-capture and auto-recall are disabled by default (opt-in). To enable either
|
||||
"entries": {
|
||||
"openclaw-mem0": {
|
||||
"config": {
|
||||
"autoCapture": true, // send conversations to Mem0 for extraction
|
||||
"autoRecall": true // inject relevant memories into context
|
||||
"autoCapture": false, // disable automatic fact extraction
|
||||
"autoRecall": false // disable automatic memory injection
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -445,7 +460,7 @@ Auto-capture and auto-recall are disabled by default (opt-in). To enable either
|
||||
}
|
||||
```
|
||||
|
||||
Without these enabled, the agent can still use memory tools (`memory_add`, `memory_search`, etc.) explicitly — only the automatic background behavior is off.
|
||||
The agent can always use memory tools (`memory_add`, `memory_search`, etc.) explicitly regardless of these settings.
|
||||
|
||||
### Credential Protection
|
||||
|
||||
|
||||
@@ -157,27 +157,27 @@ mode: "custom"
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/platform/cli"
|
||||
href="/platform/agent-signup"
|
||||
className="group flex h-full flex-col overflow-hidden rounded-2xl border border-gray-200 dark:border-zinc-800/40 bg-white dark:bg-zinc-900/40 transition hover:border-primary/60 hover:bg-gray-50 dark:hover:bg-zinc-900"
|
||||
>
|
||||
<img
|
||||
className="block dark:hidden aspect-[2/1] w-full object-cover"
|
||||
src="/images/docs thumbnails/light/CLI.png"
|
||||
alt="CLI thumbnail"
|
||||
alt="Sign up as an agent thumbnail"
|
||||
style={{pointerEvents: "none"}}
|
||||
/>
|
||||
<img
|
||||
className="hidden dark:block aspect-[2/1] w-full object-cover"
|
||||
src="/images/docs thumbnails/dark/CLI.png"
|
||||
alt="CLI thumbnail"
|
||||
alt="Sign up as an agent thumbnail"
|
||||
style={{pointerEvents: "none"}}
|
||||
/>
|
||||
<div className="flex flex-1 flex-col gap-2 px-4 pb-4 pt-3 text-left">
|
||||
<h3 className="text-base font-semibold text-gray-900 dark:text-zinc-100 group-hover:text-primary">
|
||||
CLI
|
||||
Sign up as an agent
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-zinc-400">
|
||||
Manage memories directly from your terminal. Built for developers and AI agents.
|
||||
For AI agents: mint a Mem0 API key in under five seconds — no email, no dashboard. Four commands to your first memory.
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -4,6 +4,24 @@
|
||||
|
||||
## For agents reading this file
|
||||
|
||||
### Don't have a Mem0 API key?
|
||||
|
||||
Install the CLI, then mint a key — no email, no dashboard, no OTP required:
|
||||
|
||||
```bash
|
||||
# Python
|
||||
pip install mem0-cli
|
||||
|
||||
# or Node
|
||||
npm install -g @mem0/cli
|
||||
```
|
||||
|
||||
Then run: `mem0 init --agent --agent-caller <your-name> --json` — replace `<your-name>` with the AI agent you are (e.g. `claude-code`, `cursor`, `codex`). If you forgot to pass `--agent-caller`, run `mem0 identify <your-name>` after init to attribute the signup.
|
||||
|
||||
This mints an evaluation key in <5 seconds. Use it immediately against the Platform API or MCP server. When the human is ready, claim with `mem0 init --email <your-email>` to transfer ownership (memories preserved, same API key, no agent disruption).
|
||||
|
||||
### Routing and references
|
||||
|
||||
- Use `MemoryClient` (Python) / `mem0ai` (npm) when the user has a Mem0 Platform API key. Docs under `/platform/` and `/api-reference/` apply; the managed product handles providers server-side, so you can ignore `## Optional` below.
|
||||
- Use `Memory` (Python) / `mem0ai/oss` (npm) when the user self-hosts. Docs under `/open-source/` and `/components/` apply; Platform-only features (entity filters v2, custom categories, webhooks, advanced retrieval) may not be available.
|
||||
- Scope tag reference: `[Platform]` = managed only, `[OSS]` = self-hosted only, `[Both]` = same API surface on both.
|
||||
@@ -153,6 +171,7 @@ If the user is on a pre-current major (Python < 2, TS < 3, or Platform `output_f
|
||||
- [Introduction](https://docs.mem0.ai/introduction) [Both]: Use when the user wants a one-page overview of how memory fits between the LLM and the app.
|
||||
- [Vibe Code with Mem0](https://docs.mem0.ai/vibecoding) [Both]: Use when the user is in Claude Code, Cursor, or Windsurf and wants memory wired into their editor.
|
||||
- [Platform Overview](https://docs.mem0.ai/platform/overview) [Platform]: Use when the user picks the managed product - 4-line integration, sub-50ms retrieval, dashboard.
|
||||
- [Sign up as an agent](https://docs.mem0.ai/platform/agent-signup) [Platform]: Use when an AI agent needs to mint a Mem0 API key autonomously - four commands, no email or dashboard, human claims ownership later.
|
||||
- [Platform vs Open Source](https://docs.mem0.ai/platform/platform-vs-oss) [Both]: Use when the user is deciding between managed and self-hosted.
|
||||
- [Platform Quickstart](https://docs.mem0.ai/platform/quickstart) [Platform]: Use for the first Platform integration - API key plus `MemoryClient.add/search`.
|
||||
- [Platform CLI](https://docs.mem0.ai/platform/cli) [Platform]: Use when the user wants to manage Platform memories from the terminal.
|
||||
@@ -185,8 +204,10 @@ If the user is on a pre-current major (Python < 2, TS < 3, or Platform `output_f
|
||||
### Features - Advanced Retrieval
|
||||
- [Advanced Retrieval](https://docs.mem0.ai/platform/features/advanced-retrieval) [Platform]: Use when the user needs keyword search, reranking, or hybrid retrieval.
|
||||
- [Criteria-Based Retrieval](https://docs.mem0.ai/platform/features/criteria-retrieval) [Platform]: Use when targeting memories by custom criteria, not just semantic similarity.
|
||||
- [Temporal Reasoning](https://docs.mem0.ai/platform/features/temporal-reasoning) [Platform]: Use when time-aware searches like last week, upcoming, or right now need better result ordering.
|
||||
- [Contextual Add](https://docs.mem0.ai/platform/features/contextual-add) [Platform]: Use when `add()` should consider the surrounding conversation, not just the latest turn.
|
||||
- [Custom Instructions](https://docs.mem0.ai/platform/features/custom-instructions) [Platform]: Use when tailoring what Mem0 extracts and stores on Platform.
|
||||
- [Memory Decay](https://docs.mem0.ai/platform/features/memory-decay) [Platform]: Use when search results should boost recently-reinforced memories and dampen stale ones — opt-in per project, search-time only, never filters candidates out.
|
||||
- [Advanced Memory Operations](https://docs.mem0.ai/platform/advanced-memory-operations) [Platform]: Use when basic CRUD is not enough - batch ops, complex filters, workflows.
|
||||
|
||||
### Features - Data Management
|
||||
|
||||
@@ -14,11 +14,15 @@ Move your Mem0 implementation to managed infrastructure with enterprise features
|
||||
| --------------------- | -------------- | ---------------------------- |
|
||||
| Infrastructure & Code | Low (~30 mins) | None (Parallel run possible) |
|
||||
|
||||
<Info>
|
||||
Using Mem0 Open Source with **hosted Qdrant**? You can migrate your existing memories to Mem0 Platform with a one-line script below.
|
||||
</Info>
|
||||
|
||||
<Info>
|
||||
**Why migrate to Platform?**
|
||||
|
||||
- **Time to Market**: Set up in 5 minutes vs 30+ minutes for OSS configuration
|
||||
- **Enterprise Ready**: SOC2 Type II compliance, GDPR support, audit logs
|
||||
- **Enterprise Ready**: Audit logs, workspace governance, and dedicated support
|
||||
- **Advanced Features**: Webhooks, memory export, analytics dashboard, custom categories
|
||||
- **Multi-tenancy**: Organizations, projects, and team management out of the box
|
||||
- **Zero Infrastructure**: No vector database, LLM provider, or maintenance overhead
|
||||
@@ -34,7 +38,21 @@ Move your Mem0 implementation to managed infrastructure with enterprise features
|
||||
|
||||
## Migrate
|
||||
|
||||
### 1. Install or Update SDK
|
||||
### 1. Import Memories Into Platform
|
||||
|
||||
If your Mem0 Open Source setup uses **hosted Qdrant** as the vector store, you can import your existing memories to Mem0 Platform with one command:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/mem0ai/mem0/main/scripts/oss-to-platform-migrate.sh | bash
|
||||
```
|
||||
|
||||
<Note>
|
||||
This migration script currently supports **hosted Qdrant only**. Support for local Qdrant, pgvector, and other vector stores is coming soon.
|
||||
</Note>
|
||||
|
||||
If you are using a different vector store and want to migrate to Platform, please contact Mem0 support and we’ll send you a custom migration script for your setup.
|
||||
|
||||
### 2. Install or Update SDK
|
||||
|
||||
Ensure you have the latest version of the SDK, which supports both OSS and Platform clients.
|
||||
|
||||
@@ -42,7 +60,7 @@ Ensure you have the latest version of the SDK, which supports both OSS and Platf
|
||||
pip install mem0ai --upgrade
|
||||
```
|
||||
|
||||
### 2. Update Initialization
|
||||
### 3. Update Initialization
|
||||
|
||||
Switch from the local `Memory` class to the managed `MemoryClient`.
|
||||
|
||||
@@ -75,7 +93,7 @@ client = MemoryClient(api_key="m0-...")
|
||||
Run `client.get_all(filters={"user_id": "test_connection"})` to verify your API key works. It should return an empty list or valid results.
|
||||
</Info>
|
||||
|
||||
### 3. Update Retrieval Calls (Critical)
|
||||
### 4. Update Retrieval Calls (Critical)
|
||||
|
||||
<Warning>
|
||||
**Critical Change**: Platform uses v2 endpoints that require filtering parameters to be nested inside a `filters` dictionary.
|
||||
|
||||
@@ -42,7 +42,7 @@ Previously, when an agent said something like "I've booked your flight for March
|
||||
|
||||
### Retrieval is hybrid now
|
||||
|
||||
Search now uses hybrid retrieval, which improves ranking quality — especially for queries involving exact keywords, proper nouns, or entities that appear across multiple memories. The response shape is unchanged:
|
||||
Search now uses hybrid retrieval, which improves ranking quality — especially for queries involving exact keywords, proper nouns, entities that appear across multiple memories, and time-aware queries (via Temporal Reasoning). The response shape is unchanged:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -58,7 +58,7 @@ Search now uses hybrid retrieval, which improves ranking quality — especially
|
||||
}
|
||||
```
|
||||
|
||||
The top-level `score` remains a `[0, 1]` value. Relative ranking between results stays comparable to v2, but absolute numbers shift since the scoring method changed — retune any hard thresholds in your app against representative queries.
|
||||
The top-level `score` remains a `[0, 1]` value. Relative ranking between results stays comparable to v2, but absolute numbers shift since the scoring method changed — retune any hard thresholds in your app against representative queries. Temporal signals are applied internally during ranking and are not returned as extra client-facing fields.
|
||||
|
||||
## API Changes
|
||||
|
||||
@@ -289,6 +289,7 @@ If your application previously read graph relations from the API response (`rela
|
||||
- **V1 and V2 endpoints continue to work.** There is no requirement to migrate to V3 endpoints immediately.
|
||||
- **Existing memories are preserved.** The new algorithm does not modify or re-process previously stored memories.
|
||||
- **Search response shape is unchanged.** The top-level `score` and `results[]` array are the same; existing code that reads `score` continues to work. What changed is the scoring method behind the number (multi-signal fusion instead of pure cosine), so the absolute values shift even when ranking stays comparable.
|
||||
- **Search remains backward-compatible at the top level.** Existing code that reads `results[]` and `score` continues to work. Temporal signals are applied internally during retrieval and do not change the client response shape.
|
||||
- **List response shape changed.** `get_all` now returns a paginated envelope (`{count, next, previous, results}`) instead of a bare `{results: [...]}`. Update code that reads `response["results"]` to continue working, or switch to the client SDKs which handle both shapes.
|
||||
|
||||
## Performance Improvements
|
||||
|
||||
+17
-2
@@ -419,7 +419,7 @@
|
||||
},
|
||||
"results": {
|
||||
"type": "array",
|
||||
"description": "Array of results produced by the event."
|
||||
"description": "Array of results produced by the event. For add events, this confirms the write completed; temporal reasoning enrichment runs asynchronously by default."
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
@@ -2071,7 +2071,7 @@
|
||||
"memories"
|
||||
],
|
||||
"summary": "Search memories (V3)",
|
||||
"description": "Relevance-ranked search across stored memories. V3 uses hybrid retrieval — the returned `score` is a combined `[0, 1]` value; per-signal component scores are not exposed on the response. Entity IDs **must** be passed inside the `filters` object — top-level `user_id` / `agent_id` / `run_id` are rejected with 400. At least one entity ID is required.",
|
||||
"description": "Relevance-ranked search across stored memories. V3 uses hybrid retrieval and can also apply temporal reasoning for time-aware queries. Entity IDs **must** be passed inside the `filters` object — top-level `user_id` / `agent_id` / `run_id` are rejected with 400. At least one entity ID is required.",
|
||||
"operationId": "memories_search_v3",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
@@ -2112,6 +2112,21 @@
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Apply the managed reranker for better ordering (adds latency)."
|
||||
},
|
||||
"reference_date": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"nullable": true,
|
||||
"description": "Optional query anchor time for relative temporal interpretation. Accepts Unix epoch, YYYY-MM-DD, or ISO datetime."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
---
|
||||
title: Sign up as an agent
|
||||
description: "Zero-friction signup for AI agents — mint a working Mem0 API key in under five seconds. No email, no dashboard, no OTP."
|
||||
icon: "robot"
|
||||
iconType: "solid"
|
||||
---
|
||||
|
||||
If you're an AI agent (Claude Code, Cursor, Codex, Cline, Aider, Windsurf, or any other autonomous coding tool), you don't need a human to set up Mem0 for you. Four commands — install, sign up, push a memory, search it — and you have persistent memory.
|
||||
|
||||
## The 4-command demo
|
||||
|
||||
<CodeGroup>
|
||||
```bash npm
|
||||
# 1. Install
|
||||
npm install -g @mem0/cli
|
||||
|
||||
# 2. Sign up as an agent (replace `claude-code` with your name)
|
||||
mem0 init --agent --agent-caller claude-code
|
||||
|
||||
# 3. Push a memory
|
||||
mem0 add "I am using mem0"
|
||||
|
||||
# 4. Verify
|
||||
mem0 search "am I using mem0"
|
||||
```
|
||||
|
||||
```bash pip
|
||||
# 1. Install
|
||||
pip install mem0-cli
|
||||
|
||||
# 2. Sign up as an agent (replace `claude-code` with your name)
|
||||
mem0 init --agent --agent-caller claude-code
|
||||
|
||||
# 3. Push a memory
|
||||
mem0 add "I am using mem0"
|
||||
|
||||
# 4. Verify
|
||||
mem0 search "am I using mem0"
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
That's it — under 30 seconds from a fresh shell to a working memory store.
|
||||
|
||||
## Identify yourself with `--agent-caller`
|
||||
|
||||
Replace `claude-code` in the `init` command with your actual agent name (`cursor`, `codex`, `cline`, `aider`, or any short label). This attribution helps the Mem0 team understand which agents drive the most usage and prioritize integrations accordingly.
|
||||
|
||||
<Tip>
|
||||
**Forgot `--agent-caller` on init?** Run `mem0 identify <your-name>` after the fact — same key, just back-fills the attribution. Idempotent.
|
||||
</Tip>
|
||||
|
||||
## What happened when you ran `mem0 init --agent`?
|
||||
|
||||
- A **shadow account** was minted on the Mem0 backend with a fresh API key.
|
||||
- The key was saved to `~/.mem0/config.json` (mode 0600) on the machine you ran the command on.
|
||||
- A default `user_id` was provisioned so `mem0 add` / `mem0 search` work without explicit scope flags.
|
||||
- The account is **unclaimed** — no human owns it yet.
|
||||
|
||||
Every command run against this key prints a one-line reminder asking you to surface the claim instructions to your human user.
|
||||
|
||||
## Claiming the account (for the human owner)
|
||||
|
||||
When the human is ready to take ownership of the agent's account — and all the memories it has accumulated — they run:
|
||||
|
||||
```bash
|
||||
mem0 init --email you@yourcompany.com
|
||||
```
|
||||
|
||||
The CLI detects the existing Agent Mode config, sends a verification code, and upgrades the shadow account in-place:
|
||||
|
||||
- **The API key never changes** — the agent isn't disrupted.
|
||||
- **All memories transfer** to the human's account.
|
||||
- **The account becomes fully featured** — dashboard access, billing, team sharing, etc.
|
||||
|
||||
Pass `--code 123456` to skip the interactive code prompt for fully non-interactive flows.
|
||||
|
||||
## Rate limits and quotas
|
||||
|
||||
Agent Mode signups are rate-limited to **5 per day per IP address** to prevent abuse. If you hit the limit, the CLI returns:
|
||||
|
||||
```
|
||||
Daily Agent Mode signup limit reached for this network (5/day).
|
||||
Try again from a different IP or after midnight UTC.
|
||||
```
|
||||
|
||||
Unclaimed agent accounts get the standard Mem0 free-tier quotas. The human owner can upgrade after claiming.
|
||||
|
||||
## What's next
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="CLI Reference" icon="terminal" href="/platform/cli">
|
||||
Full command-by-command reference for `mem0 add`, `mem0 search`, `mem0 list`, and the rest.
|
||||
</Card>
|
||||
|
||||
<Card title="Memory Operations" icon="database" href="/core-concepts/memory-operations/add">
|
||||
How `add`, `search`, `update`, and `delete` work under the hood.
|
||||
</Card>
|
||||
|
||||
<Card title="Mem0 MCP" icon="plug" href="/platform/mem0-mcp">
|
||||
Connect agents to Mem0 via the Model Context Protocol — alternative integration path.
|
||||
</Card>
|
||||
|
||||
<Card title="Platform Overview" icon="star" href="/platform/overview">
|
||||
The full Mem0 Platform feature set once you claim your account.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -25,6 +25,10 @@ pip install mem0-cli
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<Tip>
|
||||
**Looking for Agent Mode signup?** See [Sign up as an agent](/platform/agent-signup) — install, signup, first memory in four commands.
|
||||
</Tip>
|
||||
|
||||
## Authentication
|
||||
|
||||
Run the interactive setup wizard to configure your API key:
|
||||
@@ -95,6 +99,10 @@ mem0 init --api-key m0-xxx --user-id alice --force
|
||||
| `--code` | Verification code (use with `--email` for non-interactive login) |
|
||||
| `--force` | Overwrite existing config without confirmation |
|
||||
|
||||
<Note>
|
||||
AI agents should use `mem0 init --agent` — see [Sign up as an agent](/platform/agent-signup).
|
||||
</Note>
|
||||
|
||||
### `mem0 add`
|
||||
|
||||
Add a memory from text, a JSON messages array, a file, or stdin.
|
||||
@@ -263,6 +271,92 @@ Print the CLI version.
|
||||
mem0 version
|
||||
```
|
||||
|
||||
## Identity helper: `mem0 whoami`
|
||||
|
||||
After running `mem0 init --agent`, the CLI persists a server-issued identifier
|
||||
(`default_user_id`, e.g. `user_a1b2c3d4e5f6`) in `~/.mem0/config.json`. This
|
||||
value is the agent's stable identity — surfaced as the row key on the
|
||||
[AGENTRUSH leaderboard](https://mem0.ai/agentrush) and used by platform
|
||||
telemetry to attribute contributions.
|
||||
|
||||
Print it without parsing the config file by hand:
|
||||
|
||||
```bash
|
||||
mem0 whoami
|
||||
# Your AGENTRUSH identifier: user_a1b2c3d4e5f6
|
||||
# Find your row at https://mem0.ai/agentrush
|
||||
```
|
||||
|
||||
No network call. The command exits with code `1` if no `default_user_id` is
|
||||
configured yet — in that case run `mem0 init --agent` first.
|
||||
|
||||
## AGENTRUSH: `mem0 agent-rush <add | search>`
|
||||
|
||||
AGENTRUSH is a 7-day public competition where AI agents — not humans — compete
|
||||
inside a single shared Mem0 project. Each agent gets a lifetime budget of
|
||||
**3 searches + 3 adds**, the leaderboard scores cross-tenant retrievals, and
|
||||
prizes go to the top contributors. See [mem0.ai/agentrush](https://mem0.ai/agentrush)
|
||||
for current event details.
|
||||
|
||||
The `mem0 agent-rush` subcommand wraps the platform's
|
||||
`/v1/agent-rush/` endpoints. Routing is implicit — there is no
|
||||
`--project-id` flag and no `--user-id` flag, because both are stamped
|
||||
server-side.
|
||||
|
||||
### Bootstrap once, then play
|
||||
|
||||
```bash
|
||||
# 1. Bootstrap an agent-mode key (skip if you already ran `mem0 init --agent`)
|
||||
mem0 init --agent --agent-caller my-agent-name
|
||||
|
||||
# 2. Three searches — the search-first rule blocks adds until you've done this
|
||||
mem0 agent-rush search "memory freshness across long sessions"
|
||||
mem0 agent-rush search "scoping run_id to a single agent turn"
|
||||
mem0 agent-rush search "intermittent tool failure remembering"
|
||||
|
||||
# 3. Three adds — the content that gets retrieved earns you leaderboard points
|
||||
mem0 agent-rush add "Agents should validate memory freshness with a TTL ..."
|
||||
mem0 agent-rush add "Scoping memories by run_id avoids cross-session ..."
|
||||
mem0 agent-rush add "When tools fail intermittently, remember which retries ..."
|
||||
|
||||
# 4. Check your row
|
||||
mem0 whoami
|
||||
# Then visit https://mem0.ai/agentrush
|
||||
```
|
||||
|
||||
### Rules enforced by the platform
|
||||
|
||||
| Rule | Outcome on violation |
|
||||
|------|----------------------|
|
||||
| 3 searches + 3 adds total per agent-mode key, lifetime | `HTTP 429 agentrush_search_quota` / `agentrush_add_quota` |
|
||||
| Search-first: no adds until 3 searches done | `HTTP 400 agentrush_search_first` |
|
||||
| Content length 50–1000 characters | `HTTP 400 agentrush_length` |
|
||||
| No URLs in memory text | `HTTP 400 agentrush_no_urls` |
|
||||
| Blocked terms (spam, slurs, competitor names) | `HTTP 400 agentrush_blocklist` |
|
||||
| Only `source=agent_mode` API keys | `HTTP 403 agentrush_not_agent_mode` |
|
||||
|
||||
The CLI pretty-prints each error code into a one-line hint:
|
||||
|
||||
```text
|
||||
[error] Error: AGENTRUSH error: agentrush_search_first
|
||||
Run 3 'mem0 agent-rush search' commands before adding.
|
||||
```
|
||||
|
||||
### Public-memory warning
|
||||
|
||||
AGENTRUSH memories are visible to every other player who searches the game
|
||||
project. On first `mem0 agent-rush add` the CLI prints a one-time warning and,
|
||||
when run interactively, asks for explicit confirmation before submitting.
|
||||
**Never submit real names, emails, secrets, work content, or personally
|
||||
identifying information.** The acknowledgement is stored under
|
||||
`agent_rush.acknowledged_at` in `~/.mem0/config.json` so you are only asked
|
||||
once per machine.
|
||||
|
||||
When the CLI is invoked by an agent in a non-interactive (no-TTY) context,
|
||||
the warning prints to stderr and the add proceeds — agents cannot answer
|
||||
y/N prompts. Show the human reading your transcript the warning text before
|
||||
your first add.
|
||||
|
||||
## Output formats
|
||||
|
||||
All commands support the `--output` flag to control how results are displayed:
|
||||
|
||||
@@ -142,6 +142,24 @@ iconType: "solid"
|
||||
Setting this environment variable will prevent Mem0 from collecting and sending any usage data, ensuring complete privacy for your application.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="How do I delete my Mem0 account?">
|
||||
You can delete your Mem0 account at any time directly from the dashboard:
|
||||
|
||||
1. Sign in at [app.mem0.ai](https://app.mem0.ai).
|
||||
2. Go to **Settings → Account**.
|
||||
3. Click **Delete account** and confirm.
|
||||
|
||||
Deletion is immediate and irreversible. The following is removed:
|
||||
|
||||
- Your user profile and login credentials
|
||||
- All memories, agents, and runs you created
|
||||
- API keys and access tokens issued to your account
|
||||
- Organizations you solely own, along with their data
|
||||
- Your membership in any shared organizations (the orgs themselves are not affected)
|
||||
|
||||
Any application still using your old API keys will start receiving `401 Unauthorized` responses immediately. If you'd like to use Mem0 again later, you can create a new account at any time — it will start fresh with no data carried over.
|
||||
</Accordion>
|
||||
|
||||
</AccordionGroup>
|
||||
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ For detailed per-client instructions, see the [Mem0 MCP Quickstart](/platform/me
|
||||
|
||||
## Available tools
|
||||
|
||||
The MCP server exposes 9 memory tools to your AI client:
|
||||
The MCP server exposes 11 memory tools to your AI client:
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
@@ -66,6 +66,8 @@ The MCP server exposes 9 memory tools to your AI client:
|
||||
| `delete_entities` | Remove user/agent/app entities |
|
||||
| `get_memory` | Retrieve single memory by ID |
|
||||
| `list_entities` | View stored entities |
|
||||
| `list_events` | List memory operation events with filters and pagination |
|
||||
| `get_event_status` | Check the status of an async memory operation by `event_id` |
|
||||
|
||||
## How it works
|
||||
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
---
|
||||
title: Memory Decay
|
||||
description: "Boost recently-used memories and gently dampen stale ones at search time, without filtering anything out."
|
||||
---
|
||||
|
||||
# Memory Decay
|
||||
|
||||
Older memories drift in relevance at different speeds. A user's coffee order matters every morning; a one-off project name from last quarter rarely matters again. Memory Decay makes that intuition explicit at search time: every time a memory is returned in a search it gets a small reinforcement, and memories that haven't been touched in a while have their ranking score gently dampened.
|
||||
|
||||
It is **a soft ranking bias, never a filter.** Decay never zeroes a candidate out — at worst it scales its score by `0.3×`. Anything that would have surfaced without decay can still surface with decay on, just with a different ranking among similarly-scored results.
|
||||
|
||||
<Info>
|
||||
**Use Memory Decay when…**
|
||||
- Search results are crowded with old facts the user no longer cares about.
|
||||
- You want recently-used memories to drift to the top automatically — without writing custom scoring logic.
|
||||
- You want this preference applied per project so cohorts can be compared side-by-side.
|
||||
</Info>
|
||||
|
||||
<Warning>
|
||||
Memory Decay is **opt-in per project** and **off by default**. Search behavior is bit-identical to today until you turn it on. The toggle applies to v3 search only.
|
||||
</Warning>
|
||||
|
||||
## How it works
|
||||
|
||||
Every memory carries a small piece of bookkeeping: when was it last retrieved, and how often. Memory Decay turns that history into a *scaling factor* in the range `0.3×` to `1.5×` and multiplies it into the ranking score at search time.
|
||||
|
||||
| Memory state | Scaling factor | Ranking effect |
|
||||
|---|---|---|
|
||||
| Just accessed | ≈ **1.5×** | Strong boost |
|
||||
| Touched today | 1.2 – 1.4× | Mild boost |
|
||||
| Idle for a few days | 0.6 – 1.0× | Mild dampening |
|
||||
| Idle for weeks | 0.4 – 0.6× | Stronger dampening |
|
||||
| Idle for many months / years | ≈ **0.3×** | Floor — never lower |
|
||||
|
||||
The bounds matter: `0.3` is the floor and `1.5` is the ceiling, so decay can meaningfully reorder candidates without ever dominating the underlying relevance score.
|
||||
|
||||
At search time the pipeline:
|
||||
|
||||
1. Widens the candidate pool (`top_k × 3`, with a floor of 50) so reordering has room.
|
||||
2. Multiplies each candidate's score by its scaling factor.
|
||||
3. Sorts on the unclamped product so the full `0.3×–1.5×` range can rearrange candidates.
|
||||
4. Returns the public `score` clamped to `[0, 1]` so the API contract is preserved.
|
||||
5. Truncates to the `top_k` you requested.
|
||||
6. Records a fire-and-forget reinforcement against each returned memory — its access history grows by one, capped at the most recent 20 touches.
|
||||
|
||||
Memories created before decay was enabled don't yet have an access history. They use a sensible fallback: their `updated_at` is treated as a single past touch, so the same scale above applies based on how stale that update is — a recently-updated legacy memory enters near the neutral band, a long-stale one sits closer to the floor. Once surfaced in a search after decay is on, they accumulate access history naturally and behave like any other memory.
|
||||
|
||||
## Configure access
|
||||
|
||||
- Set `MEM0_API_KEY` in your environment, or pass it to the SDK constructor.
|
||||
- Initialize the client with the organization and project you want to scope to.
|
||||
|
||||
The toggle lives on the project. You enable decay by patching the project's `decay` field; everything else — your `add` calls, your `search` calls, your application code — stays exactly the same.
|
||||
|
||||
## Enable decay for a project
|
||||
|
||||
### 1. Turn the flag on
|
||||
|
||||
The toggle is exposed on the standard project-update endpoint, the same place where `multilingual` and `custom_categories` live.
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
client.project.update(decay=True)
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
await client.project.update({ decay: true });
|
||||
```
|
||||
|
||||
```bash cURL
|
||||
curl -X PATCH https://api.mem0.ai/api/v1/orgs/organizations/$ORG_ID/projects/$PROJECT_ID/ \
|
||||
-H "Authorization: Token $MEM0_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"decay": true}'
|
||||
```
|
||||
|
||||
```json Response
|
||||
{ "message": "Updated decay" }
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### 2. Confirm the state
|
||||
|
||||
`decay` is returned on every project read. To fetch only this field, use `?fields=decay`.
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
response = client.project.get(fields=["decay"])
|
||||
print(response["decay"])
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
const response = await client.project.get({ fields: ["decay"] });
|
||||
console.log(response.decay);
|
||||
```
|
||||
|
||||
```bash cURL
|
||||
curl "https://api.mem0.ai/api/v1/orgs/organizations/$ORG_ID/projects/$PROJECT_ID/?fields=decay" \
|
||||
-H "Authorization: Token $MEM0_API_KEY"
|
||||
```
|
||||
|
||||
```json Response
|
||||
{ "decay": true }
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### 3. Turn it back off
|
||||
|
||||
The toggle is fully reversible. Setting it to `false` immediately restores the pre-decay ranking; nothing about your stored memories is modified or lost.
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
client.project.update(decay=False)
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
await client.project.update({ decay: false });
|
||||
```
|
||||
|
||||
```bash cURL
|
||||
curl -X PATCH https://api.mem0.ai/api/v1/orgs/organizations/$ORG_ID/projects/$PROJECT_ID/ \
|
||||
-H "Authorization: Token $MEM0_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"decay": false}'
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<Note>
|
||||
The toggle is idempotent. Re-applying the same value is a no-op, and access history accumulated while decay was on is preserved if you flip it back on later.
|
||||
</Note>
|
||||
|
||||
## What changes when decay is on
|
||||
|
||||
- **Search ranking reorders.** A relevant memory you reinforced an hour ago will tend to outrank an equally-relevant memory that was last touched a month ago.
|
||||
- **The candidate pool over-fetches** to give the scaling factor room to reorder. You still get exactly the `top_k` you requested, but the items returned can come from a deeper slice of the pre-decay ranking than before.
|
||||
- **The public `score` field stays in `[0, 1]`.** Even when the internal product exceeds 1, the field returned to the client is clamped, so existing assertions and downstream UI logic continue to work.
|
||||
|
||||
## What stays the same
|
||||
|
||||
- **Public API shape** — every endpoint accepts the same parameters and returns the same fields. You don't touch your client code.
|
||||
- **Threshold semantics on the request side** — your `threshold` is still applied during candidate selection.
|
||||
- **Memory creation and storage** — every new memory still lands the same way. Decay is a search-time concern.
|
||||
- **Per-memory data** — categories, metadata, timestamps, embeddings: untouched.
|
||||
|
||||
<Warning>
|
||||
Because the scaling factor is applied *after* the threshold filter has already run, an item that passed the request `threshold` can come back with a public `score` slightly below it (a stale candidate dampened by `0.3×`). This is intentional — decay is a soft bias, not a filter. If you require a hard `score >= threshold` invariant on the response, filter client-side after the call.
|
||||
</Warning>
|
||||
|
||||
## Lifecycle of a memory under decay
|
||||
|
||||
| Stage | Scaling factor | Effect |
|
||||
|---|---|---|
|
||||
| Just added | ≈ 1.5× | Strong boost — fresh facts surface easily. |
|
||||
| Reinforced on a recent search | 1.2 – 1.5× | Sustains its boost for the next several searches. |
|
||||
| Idle for a few days | 0.6 – 1.0× | Falls back into the neutral band. |
|
||||
| Idle for weeks | 0.4 – 0.6× | Mild dampening — can still surface for strong matches. |
|
||||
| Pre-decay legacy memory (no access history) | 0.3 – 1.0× | Falls back to `updated_at`: recently-updated entries land near 1.0×, long-stale entries approach the 0.3× floor. |
|
||||
|
||||
The reinforcement is bounded: each memory tracks at most the last 20 access timestamps, so the boost stays well-behaved no matter how many times a memory is retrieved.
|
||||
|
||||
## FAQ
|
||||
|
||||
**Will decay ever drop a result that would otherwise surface?**
|
||||
No. The floor is `0.3×` — the scaling factor can dampen a score, never zero it. Threshold filtering happens *before* decay, so any candidate that cleared the threshold is in the pool decay reorders.
|
||||
|
||||
**Why is the public score sometimes below my requested threshold?**
|
||||
The threshold is applied to the candidate pool pre-decay; the scaling factor then reshapes scores in the `0.3×–1.5×` band. A stale-but-relevant candidate can come back with a final score slightly under your threshold by design — the candidate stays visible but visibly dampened. Filter client-side if you need a hard floor on the response.
|
||||
|
||||
**Does decay change how I add memories?**
|
||||
No. The `client.add(...)` path is unchanged. Decay is a search-time ranking adjustment.
|
||||
|
||||
**What if I had memories before turning decay on?**
|
||||
They use a fallback: the memory's `updated_at` is treated as a single historical touch, so the same scaling applies based on how stale that update is — a recently-updated legacy memory enters near the neutral band (~1.0×), a long-stale one closer to the floor (~0.3×). Once retrieved they accumulate access history and behave like any other memory.
|
||||
|
||||
**Can I tune how aggressively decay scales scores?**
|
||||
Not in this version. The current scaling is calibrated to be conservative — wide enough to meaningfully reorder candidates, narrow enough to never dominate the underlying relevance score. Per-project tuning is on the roadmap.
|
||||
|
||||
**Can I see the scaling factor per result?**
|
||||
Internal scoring details are persisted on the search Event for support and debugging. They aren't exposed in the public response by design — the response surface stays a single `score` field.
|
||||
|
||||
**Does decay interact with reranking?**
|
||||
Yes — they layer cleanly. The reranker produces a richer relevance score; decay then biases that score by reinforcement history before final truncation to `top_k`.
|
||||
|
||||
## What's next
|
||||
|
||||
This release is deliberately the simplest version of decay we could ship — every memory contributes to ranking through its access history alone, so the signal can be evaluated in isolation. On the roadmap:
|
||||
|
||||
- **Category-aware weighting.** A fact tagged `health` will be able to carry more weight than a passing observation tagged `misc`, so important categories don't get dampened the same way as noise.
|
||||
- **Auto-tuning per project.** Project-scoped automatic adjustment of how aggressively decay scales scores, based on observed access patterns — replacing the fixed scaling band with one that fits your workload.
|
||||
|
||||
Both extensions are forward-compatible — no migration on your side will be needed when they ship.
|
||||
@@ -0,0 +1,145 @@
|
||||
---
|
||||
title: Temporal Reasoning
|
||||
description: "Time-aware memory retrieval for Mem0 Platform v3 so queries like 'last week', 'upcoming', and 'right now' return the right memories."
|
||||
icon: "clock"
|
||||
badge: "v3"
|
||||
---
|
||||
|
||||
Some memories matter because of **when** they happened, not just because they sound similar. Temporal Reasoning lets Mem0 Platform v3 understand time-aware queries and return the most contextually appropriate results.
|
||||
|
||||
<Info>
|
||||
**Use Temporal Reasoning when…**
|
||||
- Users ask questions like "what happened last week?" or "what do I have coming up?"
|
||||
- Your app stores both past events and future plans for the same person
|
||||
- You want time-aware retrieval without building your own date-parsing layer
|
||||
</Info>
|
||||
|
||||
<Warning>
|
||||
Temporal Reasoning is a **Mem0 Platform v3** feature. It is not available on OSS memory stores or older Platform endpoints.
|
||||
</Warning>
|
||||
|
||||
## Configure access
|
||||
|
||||
Confirm your `MEM0_API_KEY` is set and that you are using the v3 Platform client:
|
||||
|
||||
```python
|
||||
from mem0 import MemoryClient
|
||||
|
||||
client = MemoryClient(api_key="your-api-key")
|
||||
```
|
||||
|
||||
## How it works
|
||||
|
||||
When a memory describes an event, a future plan, or an ongoing state, Temporal Reasoning recognizes the time context so the right results surface at search time.
|
||||
|
||||
A query like `what did I do last week?` should return a completed past event — not an upcoming appointment and not a stable fact that hasn't changed. Temporal Reasoning handles that distinction automatically.
|
||||
|
||||
### Memory types Temporal Reasoning handles
|
||||
|
||||
| Type | What it represents | Example |
|
||||
| --- | --- | --- |
|
||||
| Dated occurrence | Something that happened at a known time | "I finished the Q1 review on March 10, 2025." |
|
||||
| Future plan | A future commitment or scheduled item | "I have a dentist appointment on March 18, 2025." |
|
||||
| Ongoing state | A fact that remains true over time | "I am the product lead at Acme Corp." |
|
||||
| Relationship | A durable connection between people or entities | "Priya manages Jordan." |
|
||||
| Preference | A stable preference or habit | "I prefer morning meetings." |
|
||||
|
||||
Results come back in the normal search response shape — Temporal Reasoning affects ranking, not the response format.
|
||||
|
||||
## Configure it
|
||||
|
||||
Temporal Reasoning is enabled by default for all v3 searches and writes. There is no per-request toggle.
|
||||
|
||||
Two parameters give you precise control when you need it:
|
||||
|
||||
- `timestamp` on `add()` — anchors an imported memory to the time it actually happened, rather than the time it was added to Mem0
|
||||
- `reference_date` on `search()` — resolves relative phrases like `last week` against a fixed point in time
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
from datetime import datetime, timezone
|
||||
from mem0 import MemoryClient
|
||||
|
||||
client = MemoryClient(api_key="your-api-key")
|
||||
|
||||
# Import a historical memory anchored to when it happened
|
||||
client.add(
|
||||
[{"role": "user", "content": "I finished the Q1 review on March 10, 2025."}],
|
||||
user_id="jordan",
|
||||
timestamp=int(datetime(2025, 3, 10, tzinfo=timezone.utc).timestamp()),
|
||||
)
|
||||
|
||||
# Search with a relative query anchored to a known date
|
||||
results = client.search(
|
||||
"what did I do last week?",
|
||||
filters={"user_id": "jordan"},
|
||||
reference_date="2025-03-21T00:00:00Z",
|
||||
)
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
import { MemoryClient } from "mem0ai";
|
||||
|
||||
const client = new MemoryClient({ apiKey: "your-api-key" });
|
||||
|
||||
// Import a historical memory anchored to when it happened
|
||||
await client.add(
|
||||
[{ role: "user", content: "I finished the Q1 review on March 10, 2025." }],
|
||||
{
|
||||
userId: "jordan",
|
||||
timestamp: Math.floor(new Date("2025-03-10T00:00:00Z").getTime() / 1000),
|
||||
}
|
||||
);
|
||||
|
||||
// Search with a relative query anchored to a known date
|
||||
const results = await client.search("what did I do last week?", {
|
||||
filters: { user_id: "jordan" },
|
||||
referenceDate: "2025-03-21T00:00:00Z",
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<Tip>
|
||||
`reference_date` is especially useful in automated tests and demos because it makes relative phrases like `last week` resolve consistently every time.
|
||||
</Tip>
|
||||
|
||||
## Supported query patterns
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Historical questions">
|
||||
Examples: `last week`, `last month`, `in March 2025`, `on 2025-03-10`
|
||||
</Accordion>
|
||||
<Accordion title="Upcoming questions">
|
||||
Examples: `upcoming`, `next week`, `tomorrow`, `what do I have coming up?`
|
||||
</Accordion>
|
||||
<Accordion title="Current-state questions">
|
||||
Examples: `right now`, `currently`, `where do I work now?`
|
||||
</Accordion>
|
||||
<Accordion title="As-of questions">
|
||||
Examples: `as of March 2025`, `where was I living as of 2024?`
|
||||
</Accordion>
|
||||
<Accordion title="Duration questions">
|
||||
Examples: `how long have I lived here?`, `since when have I worked there?`
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Verify the feature is working
|
||||
|
||||
- Run a temporal search with a time-aware query (e.g., "what did I do last week?") and confirm the memory that fits the time window ranks first.
|
||||
- Use `reference_date` in test queries so relative phrases resolve consistently across runs.
|
||||
- For backfilled data, pass `timestamp` on `add()` to confirm the memory reflects the right point in time.
|
||||
|
||||
## Best practices
|
||||
|
||||
- Use explicit dates in source conversations when events or plans matter temporally.
|
||||
- Pass `timestamp` during historical imports so the ingestion time does not become the only time anchor.
|
||||
- Scope searches with `filters` so time-aware ranking operates inside the right user boundary.
|
||||
- Use `reference_date` in automated tests and reproducible demos.
|
||||
|
||||
<CardGroup cols={1}>
|
||||
<Card title="Memory Timestamps" icon="calendar" href="/platform/features/timestamp">
|
||||
Anchor imported memories to when they actually happened.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
<Snippet file="get-help.mdx" />
|
||||
@@ -10,7 +10,7 @@ estimatedTime: "~2 minutes"
|
||||
- Mem0 Platform account (<a href="https://app.mem0.ai?utm_source=oss&utm_medium=platform-mem0-mcp" rel="nofollow">Sign up here</a>)
|
||||
- API key (<a href="https://app.mem0.ai/settings/api-keys?utm_source=oss&utm_medium=platform-mem0-mcp" rel="nofollow">Get one from dashboard</a>)
|
||||
- Node.js 14+ (for npx)
|
||||
- An MCP-compatible client (Claude, Claude Code, Cursor, Windsurf, VS Code, OpenCode)
|
||||
- An MCP-compatible client (Claude, Claude Code, Codex, Cursor, Windsurf, VS Code, OpenCode)
|
||||
</Info>
|
||||
|
||||
## What is Mem0 MCP?
|
||||
@@ -46,6 +46,8 @@ The MCP server exposes these memory tools to your AI client:
|
||||
| `delete_all_memories` | Bulk delete all memories in scope |
|
||||
| `delete_entities` | Delete a user/agent/app/run entity and its memories |
|
||||
| `list_entities` | Enumerate users/agents/apps/runs stored in Mem0 |
|
||||
| `list_events` | List memory operation events with filters and pagination |
|
||||
| `get_event_status` | Check the status of an async memory operation by `event_id` |
|
||||
|
||||
---
|
||||
|
||||
@@ -86,6 +88,33 @@ You can also configure individual clients:
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Codex">
|
||||
**Direct MCP (fastest, MCP only).** Codex reads MCP servers from `~/.codex/config.toml` as TOML (not JSON). Add:
|
||||
|
||||
```toml
|
||||
[mcp_servers.mem0]
|
||||
url = "https://mcp.mem0.ai/mcp"
|
||||
bearer_token_env_var = "MEM0_API_KEY"
|
||||
```
|
||||
|
||||
Export `MEM0_API_KEY` in the shell you launch Codex from, then restart Codex. `codex mcp add` only supports stdio servers, so HTTP servers must be added via `config.toml` directly — or via the **Plugins → Connect to a custom MCP → Streamable HTTP** UI in the Codex app.
|
||||
|
||||
<Note>
|
||||
Codex uses the server name `mem0` (not `mem0-mcp` like the other clients on this page) so it matches the name the bundled plugin registers if you ever sideload it later.
|
||||
</Note>
|
||||
|
||||
**Sideloaded plugin (full experience).** If you want the memory protocol skill, Mem0 SDK skill, and opt-in lifecycle hooks alongside the MCP server, sideload the plugin from a clone of `mem0ai/mem0`. The repo ships a marketplace manifest at `.agents/plugins/marketplace.json`, so you can register it with one CLI call:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/mem0ai/mem0.git ~/codex-plugins/mem0-source
|
||||
codex plugin marketplace add ~/codex-plugins/mem0-source
|
||||
```
|
||||
|
||||
Then run `codex` and `/plugins`, browse the **Mem0 Plugins** marketplace, and install **Mem0**. Don't combine this with the Direct MCP setup above — the sideloaded plugin auto-registers `mem0` via `.codex-mcp.json`, so a manual `[mcp_servers.mem0]` block would create a duplicate.
|
||||
|
||||
See the [Codex integration guide](/integrations/codex) for full details, lifecycle-hook setup, and management commands (`codex plugin marketplace upgrade` / `remove`).
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Cursor">
|
||||
```bash
|
||||
npx mcp-add \
|
||||
|
||||
@@ -12,7 +12,7 @@ Mem0 is the memory engine that keeps conversations contextual so users never rep
|
||||
|
||||
- **Personalized replies**: Memories persist across users and agents, cutting prompt bloat and repeat questions.
|
||||
- **Hosted stack**: Mem0 runs the vector store, graph services, and rerankers—no provisioning, tuning, or maintenance.
|
||||
- **Enterprise controls**: SOC 2, audit logs, and workspace governance ship by default for production readiness.
|
||||
- **Enterprise controls**: Audit logs and workspace governance ship by default for production readiness.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="What you get with Mem0 Platform" icon="sparkles">
|
||||
@@ -22,7 +22,7 @@ Mem0 is the memory engine that keeps conversations contextual so users never rep
|
||||
| Fast setup | Add a few lines of code and you’re production-ready—no vector database or LLM configuration required. |
|
||||
| Production scale | Automatic scaling, high availability, and managed infrastructure so you focus on product work. |
|
||||
| Advanced features | Graph memory, webhooks, multimodal support, and custom categories are ready to enable. |
|
||||
| Enterprise ready | SOC 2 Type II, GDPR compliance, and dedicated support keep security and governance covered. |
|
||||
| Enterprise ready | Audit logs, workspace governance, and dedicated support keep security and governance covered. |
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
|
||||
@@ -7,6 +7,10 @@ iconType: "solid"
|
||||
|
||||
Get started with Mem0 Platform's hosted API in under 5 minutes. This guide shows you how to authenticate and store your first memory.
|
||||
|
||||
<Note>
|
||||
**Are you an AI agent?** See [Sign up as an agent](/platform/agent-signup) — mint a working API key in four commands, no email or dashboard required.
|
||||
</Note>
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Mem0 Platform account (<a href="https://app.mem0.ai?utm_source=oss&utm_medium=platform-quickstart" rel="nofollow">Sign up here</a>)
|
||||
|
||||
+24
-2
@@ -22,13 +22,35 @@ We follow the llms.txt standard:
|
||||
|
||||
## Agent Skills
|
||||
|
||||
Teach your coding assistant how to build with Mem0:
|
||||
Mem0 ships two kinds of skills for AI coding assistants. Both work with Claude Code, Codex, Cursor, Windsurf, OpenCode, OpenClaw, and any assistant that supports the skills standard.
|
||||
|
||||
### Reference skills — always on
|
||||
|
||||
Teach your assistant Mem0's SDK surface so it writes correct code in everyday development:
|
||||
|
||||
```bash
|
||||
npx skills add https://github.com/mem0ai/mem0 --skill mem0
|
||||
npx skills add https://github.com/mem0ai/mem0 --skill mem0-cli
|
||||
npx skills add https://github.com/mem0ai/mem0 --skill mem0-vercel-ai-sdk
|
||||
```
|
||||
|
||||
Works with Claude Code, Cursor, Windsurf, and any assistant that supports skills. Once installed, your assistant understands Mem0's full API, framework integrations, and common patterns.
|
||||
- `mem0` — Python and TypeScript SDKs (Platform + OSS), plus framework integrations (LangChain, CrewAI, OpenAI Agents, LangGraph, LlamaIndex, etc.)
|
||||
- `mem0-cli` — terminal workflows for the `mem0` CLI (both Node and Python builds)
|
||||
- `mem0-vercel-ai-sdk` — `@mem0/vercel-ai-provider` and `createMem0`
|
||||
|
||||
### Pipeline skills — run on demand
|
||||
|
||||
Let your assistant execute an end-to-end workflow in an existing repo. Invoked as slash commands:
|
||||
|
||||
```bash
|
||||
npx skills add https://github.com/mem0ai/mem0 --skill mem0-integrate
|
||||
npx skills add https://github.com/mem0ai/mem0 --skill mem0-test-integration
|
||||
```
|
||||
|
||||
- `/mem0-integrate` — wire Mem0 into an existing repository using a goal-driven, test-first pipeline. Detects the stack, asks whether to use Platform or OSS, writes failing tests first, and keeps the integration additive and feature-flagged.
|
||||
- `/mem0-test-integration` — verify what `/mem0-integrate` produced. Runs the repo's native test suite and a real end-to-end smoke flow against your API key, then produces a scorecard.
|
||||
|
||||
See the [skills index](https://github.com/mem0ai/mem0/tree/main/skills) for the full catalog.
|
||||
|
||||
## MCP Server Setup
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mem0",
|
||||
"version": "0.1.1",
|
||||
"version": "0.2.0",
|
||||
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Claude workflows using the Mem0 Platform MCP server.",
|
||||
"author": {
|
||||
"name": "Mem0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"mem0": {
|
||||
"url": "https://mcp.mem0.ai/mcp/",
|
||||
"url": "https://mcp.mem0.ai/mcp",
|
||||
"bearer_token_env_var": "MEM0_API_KEY"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mem0",
|
||||
"version": "0.1.1",
|
||||
"version": "0.2.0",
|
||||
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Codex workflows using the Mem0 Platform MCP server.",
|
||||
"author": {
|
||||
"name": "Mem0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mem0",
|
||||
"version": "0.1.1",
|
||||
"version": "0.2.0",
|
||||
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search using the Mem0 Platform MCP server.",
|
||||
"author": {
|
||||
"name": "Mem0",
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to the Mem0 plugin will be documented in this file.
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Added
|
||||
|
||||
- **Project-scoped memories:** Deterministic `project_id` from git remote (`_project.sh` / `_project.py`). Memories are now isolated per-repo via `metadata.project_id` on every `add_memory` and `search_memories` call. Same repo cloned twice → same `project_id`.
|
||||
- **Branch-aware tagging:** `metadata.branch` stamped on session-state and compact-summary memories. Enables branch-scoped recall (e.g. "what was I doing on feature/auth-rewrite?").
|
||||
- **Auto-import of project files:** SessionStart detects CLAUDE.md, AGENTS.md, .cursorrules, .windsurfrules, mem0.md — hashes them (SHA-256), imports changed files as `project_profile` memories. Idempotent across sessions.
|
||||
- **Active identity banner:** SessionStart now prints `user=X | project=Y | branch=Z | memories=N` instead of a silent bootstrap.
|
||||
- **Session-end report:** Stop hook prints `Session: wrote N memories, retrieved M. Categories touched: ...` and appends to `~/.mem0/session-log.md`.
|
||||
- **`/mem0:onboard` skill:** Post-install wizard — verifies API key, detects and imports project files, installs coding categories, prints setup summary. 30 seconds to value.
|
||||
- **`/mem0:tour` skill:** Shows all memories for the current project grouped by category. Proof-of-value demo.
|
||||
- **`/mem0:switch-project` skill:** Manual `project_id` override for monorepos and non-git directories. Persists to `~/.mem0/project_map.json`.
|
||||
- **Session stats tracker** (`session_stats.py`): Tracks memory adds/searches per session for the end-of-session report.
|
||||
|
||||
### Changed
|
||||
|
||||
- All hooks and `mem0-mcp/SKILL.md` now include `project_id` in every filter and metadata example.
|
||||
- SessionStart banner replaces the previous "## Mem0 Identity" block with a compact one-liner.
|
||||
- `on_pre_compact.py` and `capture_compact_summary.py` now include `project_id` and `branch` in stored metadata.
|
||||
|
||||
## 0.1.3
|
||||
|
||||
### Fixed
|
||||
|
||||
- **user_id resolution no longer derives from `MEM0_API_KEY`.** v0.1.2 changed the resolver to fall back to `"mem0-" + sha256(MEM0_API_KEY)[:12]` ahead of `$USER`, which silently moved every existing user to a new bucket on update. Memories written under the previous `$USER` value became unreachable from the plugin. Resolution is now back to `MEM0_USER_ID` → `$USER` → `"default"`.
|
||||
- Dropped the "regardless of which machine you're on" line from the SessionStart bootstrap, since cross-machine consolidation now requires setting `MEM0_USER_ID` explicitly.
|
||||
|
||||
### Notes for users upgrading from 0.1.2
|
||||
|
||||
- The `~/.mem0/identity.json` cache file is no longer read or written. Safe to delete.
|
||||
- If you wrote memories during the v0.1.2 window, they live under `mem0-<sha256(api_key)[:12]>`. To recover: temporarily `export MEM0_USER_ID=mem0-<hash>`, search/export, then unset.
|
||||
- Want a single bucket across machines (the original goal of #5076)? Set `MEM0_USER_ID` explicitly in your shell profile. The plugin will not auto-derive one.
|
||||
|
||||
## 0.1.2
|
||||
|
||||
### Added
|
||||
|
||||
- Deterministic `user_id` resolver (`_identity.sh` / `_identity.py`) — **reverted in 0.1.3, see above.**
|
||||
- SessionStart-compact handler (`capture_compact_summary.py`) that stores the post-compaction summary as a memory with `metadata.type=compact_summary`.
|
||||
- Coding-taxonomy setup script (`setup_coding_categories.py`) — one-shot `project.update(custom_categories=[...])` for `architecture_decisions`, `anti_patterns`, `task_learnings`, `tooling_setup`, `bug_fixes`, `coding_conventions`, `user_preferences`.
|
||||
- Opt-in hook logging via `MEM0_DEBUG=1` → `~/.mem0/hooks.log`.
|
||||
- `mem0-mcp` skill replacing the Claude-Code-specific `mem0-codex` skill.
|
||||
|
||||
### Fixed
|
||||
|
||||
- `session_id` now written to memory metadata (`on_pre_compact.py`).
|
||||
- SessionStart bootstrap exits silently when `MEM0_API_KEY` is unset.
|
||||
- `block_memory_write.sh` regex tightened to `MEMORY.md` / `.claude/memory/*` — no longer blocks `docs/memory/*.md`.
|
||||
- Removed duplicate PreCompact write path (kept agent-driven, dropped the parallel Python REST entry from `hooks.json` / `cursor-hooks.json`).
|
||||
- Hook-side captures (`session_state`, `compact_summary`) now set `expiration_date = today + 90 days`.
|
||||
|
||||
## 0.1.1
|
||||
|
||||
- Cursor plugin fully functional (`#4547`).
|
||||
- Codex plugin support and integration docs (`#4665`).
|
||||
- Codex lifecycle hooks via opt-in installer (`#4917`).
|
||||
- Removed invalid keys from Claude plugin config (`#4821`).
|
||||
|
||||
## 0.1.0
|
||||
|
||||
- Initial release: Mem0 plugin for Claude Code and Cursor (`#4518`).
|
||||
+75
-64
@@ -2,6 +2,18 @@
|
||||
|
||||
Add persistent memory to your AI workflows. Store, retrieve, and manage memories across sessions using the Mem0 Platform. Works with **Claude Code** (CLI), **Claude Cowork** (desktop app), **Cursor**, and **Codex**.
|
||||
|
||||
## Quick path for agents
|
||||
|
||||
If you're an AI agent setting up Mem0 autonomously (no human dashboard signup available), run:
|
||||
|
||||
```bash
|
||||
mem0 init --agent --json
|
||||
```
|
||||
|
||||
This mints an evaluation API key in <5 seconds — no email, no OTP, no browser. The key is written to your config and exported as `MEM0_API_KEY` for the current shell. Use it immediately against the Mem0 Platform or the MCP server. The human owner can later run `mem0 init --email <their-email>` to claim ownership: memories transfer, the same API key keeps working, and the agent isn't disrupted.
|
||||
|
||||
Humans setting up Mem0 by hand should continue with Step 1 below.
|
||||
|
||||
## Step 1: Set your API key
|
||||
|
||||
> **You must complete this step before installing the plugin.**
|
||||
@@ -49,72 +61,38 @@ This installs the full plugin including the MCP server, lifecycle hooks (automat
|
||||
|
||||
### Codex
|
||||
|
||||
**Option A — Repo marketplace** (recommended for teams):
|
||||
**Option A — Direct MCP** (fastest, MCP only):
|
||||
|
||||
Add the plugin marketplace to your repo root (already included in this repository):
|
||||
Codex reads MCP servers from `~/.codex/config.toml` as TOML. Add:
|
||||
|
||||
```
|
||||
.agents/plugins/marketplace.json
|
||||
```toml
|
||||
[mcp_servers.mem0]
|
||||
url = "https://mcp.mem0.ai/mcp"
|
||||
bearer_token_env_var = "MEM0_API_KEY"
|
||||
```
|
||||
|
||||
Then in Codex, browse the repo's plugin directory and install Mem0.
|
||||
Export `MEM0_API_KEY` in your shell and restart Codex. `codex mcp add` only supports stdio servers, so HTTP servers like Mem0's must be added via `config.toml` directly (or via the **Plugins → Connect to a custom MCP → Streamable HTTP** UI in the Codex app).
|
||||
|
||||
**Option B — Personal marketplace**:
|
||||
**Option B — Sideload the plugin** (full experience: MCP + skills + opt-in hooks):
|
||||
|
||||
Add to `~/.agents/plugins/marketplace.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "mem0-plugins",
|
||||
"interface": {
|
||||
"displayName": "Mem0 Plugins"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "mem0",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "/path/to/mem0/mem0-plugin"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "Productivity"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Option C — Manual MCP configuration**:
|
||||
|
||||
Add to your Codex MCP config:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mem0": {
|
||||
"type": "http",
|
||||
"url": "https://mcp.mem0.ai/mcp/",
|
||||
"headers": {
|
||||
"Authorization": "Token ${MEM0_API_KEY}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Options A, B, and C above install the MCP server and the Mem0 SDK skill.
|
||||
|
||||
**Optional — enable lifecycle hooks**
|
||||
|
||||
Codex only discovers hooks at `~/.codex/hooks.json` or `<repo>/.codex/hooks.json` ([docs](https://developers.openai.com/codex/hooks)) — there is no plugin-host mechanism that auto-wires hooks from an installed plugin. To opt in, run the installer once after installing the plugin:
|
||||
Clone the repo and register the bundled marketplace with one CLI call:
|
||||
|
||||
```bash
|
||||
python3 /path/to/mem0-plugin/scripts/install_codex_hooks.py
|
||||
git clone https://github.com/mem0ai/mem0.git ~/codex-plugins/mem0-source
|
||||
codex plugin marketplace add ~/codex-plugins/mem0-source
|
||||
```
|
||||
|
||||
This merges three entries into `~/.codex/hooks.json` with absolute paths pointing into the plugin directory:
|
||||
This points Codex at the repo's `.agents/plugins/marketplace.json`, which references `mem0-plugin/` as the local source. Restart Codex, run `/plugins`, and install **Mem0** from the **Mem0 Plugins** marketplace.
|
||||
|
||||
> **Don't combine with Option A.** The plugin manifest auto-registers `mem0` as an MCP server via `mem0-plugin/.codex-mcp.json` — adding a manual `[mcp_servers.mem0]` block would duplicate the registration.
|
||||
|
||||
**Optional — enable lifecycle hooks.** Codex doesn't auto-wire hooks from plugin manifests; it only reads `~/.codex/hooks.json` (or `<repo>/.codex/hooks.json`) ([docs](https://developers.openai.com/codex/hooks)). Run the bundled installer once to merge Mem0's entries:
|
||||
|
||||
```bash
|
||||
python3 ~/codex-plugins/mem0-source/mem0-plugin/scripts/install_codex_hooks.py
|
||||
```
|
||||
|
||||
This merges three entries into `~/.codex/hooks.json` with absolute paths pointing into your clone:
|
||||
|
||||
| Event | What it does |
|
||||
|-------|--------------|
|
||||
@@ -122,7 +100,7 @@ This merges three entries into `~/.codex/hooks.json` with absolute paths pointin
|
||||
| `UserPromptSubmit` | Injects relevant memories into the prompt |
|
||||
| `Stop` | Reminds the agent to persist learnings at turn end |
|
||||
|
||||
Re-running the installer is idempotent (replaces the Mem0 entries rather than duplicating) and preserves any other hooks you have. To remove: `python3 scripts/install_codex_hooks.py --uninstall`.
|
||||
Re-running the installer is idempotent (replaces the Mem0 entries rather than duplicating) and preserves any other hooks you have. To remove: `python3 .../install_codex_hooks.py --uninstall`. If you move or delete the clone directory, re-run the installer from the new location — the hooks file stores absolute paths.
|
||||
|
||||
Codex hooks also require the `codex_hooks` feature flag in `~/.codex/config.toml`:
|
||||
|
||||
@@ -133,6 +111,13 @@ codex_hooks = true
|
||||
|
||||
The installer prints a reminder if the flag isn't set. Restart Codex after editing the config.
|
||||
|
||||
**Managing the plugin:**
|
||||
|
||||
```bash
|
||||
codex plugin marketplace upgrade # pull latest plugin versions
|
||||
codex plugin marketplace remove mem0-plugins # unregister the marketplace
|
||||
```
|
||||
|
||||
### Cursor
|
||||
|
||||
> **Already have `mem0` configured as an MCP server?** Remove the existing entry from your Cursor MCP settings before installing to avoid duplicate tools.
|
||||
@@ -172,17 +157,43 @@ After installing, confirm the MCP server is connected:
|
||||
|
||||
## What's included
|
||||
|
||||
| Component | Claude Code / Cowork | Cursor (Marketplace) | Cursor (Deeplink/Manual) | Codex |
|
||||
|-----------|:--------------------:|:--------------------:|:------------------------:|:-----:|
|
||||
| MCP Server | Yes | Yes | Yes | Yes |
|
||||
| Lifecycle Hooks | Yes | Yes | No | Opt-in |
|
||||
| Mem0 SDK Skill | Yes | Yes | No | Yes |
|
||||
| Memory Protocol Skill | No | No | No | Yes |
|
||||
| Component | Claude Code / Cowork | Cursor (Marketplace) | Cursor (Deeplink/Manual) | Codex (Sideload) | Codex (Direct MCP) |
|
||||
|-----------|:--------------------:|:--------------------:|:------------------------:|:----------------:|:------------------:|
|
||||
| MCP Server | Yes | Yes | Yes | Yes | Yes |
|
||||
| Lifecycle Hooks | Yes | Yes | No | Opt-in | No |
|
||||
| Mem0 SDK Skill | Yes | Yes | No | Yes | No |
|
||||
| Memory Protocol Skill | No | No | No | Yes | No |
|
||||
|
||||
- **MCP Server** — Connects to the Mem0 remote MCP server (`mcp.mem0.ai`), providing tools to add, search, update, and delete memories. No local dependencies required.
|
||||
- **Lifecycle Hooks** — Automatic memory capture at key points. Claude Code and Cursor wire hooks up natively when the plugin is installed (session start, context compaction, task completion, session end). Codex hooks are opt-in via a one-time installer (`scripts/install_codex_hooks.py`) that writes entries into `~/.codex/hooks.json` for `SessionStart`, `UserPromptSubmit`, and `Stop`.
|
||||
- **Mem0 SDK Skill** — Guides the AI on how to integrate the Mem0 SDK (Python & TypeScript) into your applications.
|
||||
- **Memory Protocol Skill** — Codex-specific skill that instructs the agent to retrieve relevant memories at task start, store learnings on completion, and capture session state before context loss. Replaces lifecycle hooks on platforms that don't support them.
|
||||
- **Memory Protocol Skill** — Codex-specific skill that instructs the agent to retrieve relevant memories at task start, store learnings on completion, and capture session state before context loss. Complements the lifecycle hooks on Codex.
|
||||
|
||||
## Updating the plugin
|
||||
|
||||
When the plugin updates (new version pulled from the marketplace, or a fresh local install), the MCP server connection in your existing Claude Code / Cursor / Codex session is left holding a stale handle and stops responding. **Restart your client to reconnect:**
|
||||
|
||||
- **Claude Code:** run `/restart` in the prompt, or close and reopen the CLI.
|
||||
- **Cursor:** quit and relaunch.
|
||||
- **Codex:** restart the editor session.
|
||||
|
||||
Your `MEM0_API_KEY` doesn't need to be re-entered — the auth header is re-read from your environment on the new session. The plugin's MCP config uses `${MEM0_API_KEY}` interpolation at session start, not at install time, so as long as the env var is set persistently (in your shell profile or `~/.claude/settings.json` `env` block), reconnection is automatic on restart.
|
||||
|
||||
If reconnection still fails after a restart, check that `MEM0_API_KEY` is reachable in the new shell (`echo $MEM0_API_KEY`) and confirm you're using a key that starts with `m0-` (from https://app.mem0.ai/dashboard/api-keys, not a legacy token).
|
||||
|
||||
## Optional: tune categories for coding workflows
|
||||
|
||||
mem0 auto-tags every memory with one or more `categories` from a project-level list. The default list is consumer-oriented (`food`, `hobbies`, `music` …) — useful for chat assistants, less so for code. A one-shot script in this plugin replaces it with a coding-focused taxonomy:
|
||||
|
||||
```bash
|
||||
# Dry-run first -- prints current vs proposed, no changes:
|
||||
python mem0-plugin/scripts/setup_coding_categories.py
|
||||
|
||||
# Actually write:
|
||||
python mem0-plugin/scripts/setup_coding_categories.py --apply
|
||||
```
|
||||
|
||||
Requires the `mem0ai` Python SDK (`pip install mem0ai`) and `MEM0_API_KEY` set. New memories will then auto-tag against `architecture_decisions`, `anti_patterns`, `task_learnings`, `tooling_setup`, `bug_fixes`, `coding_conventions`, `user_preferences`. Re-run with a different list any time; `project.update(custom_categories=[...])` always replaces.
|
||||
|
||||
## MCP Tools
|
||||
|
||||
|
||||
@@ -18,12 +18,24 @@
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CODEX_PLUGIN_ROOT}/scripts/on_user_prompt.sh",
|
||||
"statusMessage": "Searching mem0 memories...",
|
||||
"statusMessage": "Checking memory relevance...",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "mcp__mem0__",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CODEX_PLUGIN_ROOT}/scripts/on_post_tool_use.sh",
|
||||
"timeout": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
|
||||
@@ -2,34 +2,37 @@
|
||||
"hooks": {
|
||||
"sessionStart": [
|
||||
{
|
||||
"command": "${CURSOR_PLUGIN_ROOT}/scripts/on_session_start.sh",
|
||||
"command": "${CURSOR_PLUGIN_ROOT}/scripts/on_session_start_cursor.sh",
|
||||
"matcher": "startup|resume|compact"
|
||||
}
|
||||
],
|
||||
"preToolUse": [
|
||||
{
|
||||
"command": "${CURSOR_PLUGIN_ROOT}/scripts/block_memory_write.sh",
|
||||
"command": "${CURSOR_PLUGIN_ROOT}/scripts/block_memory_write_cursor.sh",
|
||||
"matcher": "Write|Edit"
|
||||
}
|
||||
],
|
||||
"postToolUse": [
|
||||
{
|
||||
"command": "${CURSOR_PLUGIN_ROOT}/scripts/on_post_tool_use_cursor.sh",
|
||||
"matcher": "mcp__mem0__",
|
||||
"timeout": 3
|
||||
}
|
||||
],
|
||||
"preCompact": [
|
||||
{
|
||||
"command": "${CURSOR_PLUGIN_ROOT}/scripts/on_pre_compact.sh"
|
||||
},
|
||||
{
|
||||
"command": "python3 ${CURSOR_PLUGIN_ROOT}/scripts/on_pre_compact.py",
|
||||
"timeout": 30
|
||||
"command": "${CURSOR_PLUGIN_ROOT}/scripts/on_pre_compact_cursor.sh"
|
||||
}
|
||||
],
|
||||
"stop": [
|
||||
{
|
||||
"command": "${CURSOR_PLUGIN_ROOT}/scripts/on_stop.sh",
|
||||
"command": "${CURSOR_PLUGIN_ROOT}/scripts/on_stop_cursor.sh",
|
||||
"timeout": 10
|
||||
}
|
||||
],
|
||||
"beforeSubmitPrompt": [
|
||||
{
|
||||
"command": "${CURSOR_PLUGIN_ROOT}/scripts/on_user_prompt.sh",
|
||||
"command": "${CURSOR_PLUGIN_ROOT}/scripts/on_user_prompt_cursor.sh",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
|
||||
@@ -23,6 +23,18 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "mcp__mem0__",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_post_tool_use.sh",
|
||||
"timeout": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PreCompact": [
|
||||
{
|
||||
"hooks": [
|
||||
@@ -30,12 +42,6 @@
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_pre_compact.sh",
|
||||
"statusMessage": "Preparing pre-compaction summary..."
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/on_pre_compact.py",
|
||||
"statusMessage": "Saving session state to mem0...",
|
||||
"timeout": 30
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -57,7 +63,7 @@
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_user_prompt.sh",
|
||||
"statusMessage": "Searching mem0 memories...",
|
||||
"statusMessage": "Checking memory relevance...",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
"""Resolve mem0 user_id.
|
||||
|
||||
Resolution priority:
|
||||
1. MEM0_USER_ID env var (explicit override)
|
||||
2. $USER, else "default"
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def resolve_user_id() -> str:
|
||||
explicit = os.environ.get("MEM0_USER_ID", "").strip()
|
||||
if explicit:
|
||||
return explicit
|
||||
return os.environ.get("USER") or "default"
|
||||
|
||||
|
||||
try:
|
||||
from _project import resolve_branch, resolve_project_id, save_project_mapping
|
||||
except ImportError:
|
||||
def resolve_project_id() -> str:
|
||||
return os.path.basename(os.getcwd())
|
||||
|
||||
def resolve_branch() -> str:
|
||||
return "unknown"
|
||||
|
||||
def save_project_mapping(cwd: str, project_id: str) -> None:
|
||||
pass
|
||||
@@ -0,0 +1,19 @@
|
||||
# Source this file. Sets MEM0_RESOLVED_USER_ID.
|
||||
#
|
||||
# Resolution priority:
|
||||
# 1. MEM0_USER_ID env var (explicit override)
|
||||
# 2. $USER, else "default"
|
||||
|
||||
_mem0_resolve_identity() {
|
||||
if [ -n "${MEM0_USER_ID:-}" ]; then
|
||||
printf '%s' "$MEM0_USER_ID"
|
||||
return
|
||||
fi
|
||||
printf '%s' "${USER:-default}"
|
||||
}
|
||||
|
||||
MEM0_RESOLVED_USER_ID="$(_mem0_resolve_identity)"
|
||||
export MEM0_RESOLVED_USER_ID
|
||||
|
||||
# Also resolve project context
|
||||
. "$( cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd )/_project.sh"
|
||||
@@ -0,0 +1,130 @@
|
||||
"""Resolve mem0 project_id and branch.
|
||||
|
||||
Resolution priority (project_id):
|
||||
1. MEM0_PROJECT_ID env var (explicit override)
|
||||
2. ~/.mem0/project_map.json lookup by cwd
|
||||
3. Git remote slug: strip protocol/prefix, strip .git, replace / and : with -
|
||||
e.g. git@github.com:mem0ai/mem0.git -> mem0ai-mem0
|
||||
4. Fallback: basename of cwd
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
|
||||
def resolve_project_id(cwd: str | None = None) -> str:
|
||||
if cwd is None:
|
||||
cwd = os.getcwd()
|
||||
|
||||
# 1. Explicit override
|
||||
explicit = os.environ.get("MEM0_PROJECT_ID", "").strip()
|
||||
if explicit:
|
||||
return explicit
|
||||
|
||||
# 2. project_map.json lookup
|
||||
map_path = os.path.expanduser("~/.mem0/project_map.json")
|
||||
if os.path.isfile(map_path):
|
||||
try:
|
||||
with open(map_path) as f:
|
||||
project_map = json.load(f)
|
||||
mapped = project_map.get(cwd, "").strip()
|
||||
if mapped:
|
||||
return mapped
|
||||
except (OSError, json.JSONDecodeError, AttributeError):
|
||||
pass
|
||||
|
||||
# 3. Git remote slug
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "remote", "get-url", "origin"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
cwd=cwd,
|
||||
)
|
||||
remote_url = result.stdout.strip()
|
||||
if remote_url:
|
||||
slug = _remote_url_to_slug(remote_url)
|
||||
if slug:
|
||||
return slug
|
||||
except (subprocess.CalledProcessError, OSError):
|
||||
pass
|
||||
|
||||
# 4. Fallback: basename of cwd
|
||||
return os.path.basename(cwd) or "unknown"
|
||||
|
||||
|
||||
def resolve_branch(cwd: str | None = None) -> str:
|
||||
if cwd is None:
|
||||
cwd = os.getcwd()
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "branch", "--show-current"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
cwd=cwd,
|
||||
)
|
||||
branch = result.stdout.strip()
|
||||
return branch if branch else "unknown"
|
||||
except (subprocess.CalledProcessError, OSError):
|
||||
return "unknown"
|
||||
|
||||
|
||||
def save_project_mapping(cwd: str, project_id: str) -> None:
|
||||
"""Write cwd -> project_id into ~/.mem0/project_map.json."""
|
||||
mem0_dir = os.path.expanduser("~/.mem0")
|
||||
os.makedirs(mem0_dir, exist_ok=True)
|
||||
map_path = os.path.join(mem0_dir, "project_map.json")
|
||||
project_map: dict[str, str] = {}
|
||||
if os.path.isfile(map_path):
|
||||
try:
|
||||
with open(map_path) as f:
|
||||
project_map = json.load(f)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
project_map = {}
|
||||
project_map[cwd] = project_id
|
||||
with open(map_path, "w") as f:
|
||||
json.dump(project_map, f, indent=2)
|
||||
|
||||
|
||||
def _remote_url_to_slug(url: str) -> str:
|
||||
"""Convert a git remote URL to a deterministic slug.
|
||||
|
||||
Handles:
|
||||
- HTTPS: https://github.com/owner/repo.git
|
||||
- SSH: git@github.com:owner/repo.git
|
||||
- SSH: git@github.com-alias:owner/repo.git (custom host aliases)
|
||||
- ssh://: ssh://git@github.com/owner/repo.git
|
||||
- git://: git://github.com/owner/repo.git
|
||||
"""
|
||||
slug = url.strip()
|
||||
# Strip .git suffix
|
||||
if slug.endswith(".git"):
|
||||
slug = slug[:-4]
|
||||
# Strip protocol prefixes
|
||||
for prefix in ("https://", "http://", "ssh://", "git://"):
|
||||
if slug.startswith(prefix):
|
||||
slug = slug[len(prefix):]
|
||||
break
|
||||
else:
|
||||
# Handle git@ style (no protocol prefix matched)
|
||||
slug = re.sub(r"^git@", "", slug)
|
||||
# Replace the first colon (SSH host:path separator) with /
|
||||
slug = slug.replace(":", "/", 1)
|
||||
# Split on / and take last two components (owner, repo)
|
||||
parts = [p for p in slug.split("/") if p]
|
||||
if len(parts) >= 2:
|
||||
owner, repo = parts[-2], parts[-1]
|
||||
slug = f"{owner}-{repo}"
|
||||
elif parts:
|
||||
slug = parts[-1]
|
||||
else:
|
||||
return ""
|
||||
# Replace any remaining / and : with -
|
||||
slug = slug.replace("/", "-").replace(":", "-")
|
||||
return slug
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
# Source this file. Sets MEM0_PROJECT_ID and MEM0_BRANCH.
|
||||
#
|
||||
# Resolution priority (project_id):
|
||||
# 1. MEM0_PROJECT_ID env var (explicit override)
|
||||
# 2. ~/.mem0/project_map.json lookup by $PWD (requires jq)
|
||||
# 3. Git remote slug: strip protocol/prefix, strip .git, replace / and : with -
|
||||
# e.g. git@github.com:mem0ai/mem0.git -> mem0ai-mem0
|
||||
# 4. Fallback: basename of $PWD
|
||||
#
|
||||
# Branch resolution:
|
||||
# git branch --show-current, fallback "unknown"
|
||||
|
||||
_mem0_resolve_project_id() {
|
||||
# 1. Explicit override
|
||||
if [ -n "${MEM0_PROJECT_ID:-}" ]; then
|
||||
printf '%s' "$MEM0_PROJECT_ID"
|
||||
return
|
||||
fi
|
||||
|
||||
# 2. project_map.json lookup by $PWD
|
||||
_mem0_map="$HOME/.mem0/project_map.json"
|
||||
if [ -f "$_mem0_map" ] && command -v jq >/dev/null 2>&1; then
|
||||
_mem0_mapped=$(jq -r --arg cwd "$PWD" '.[$cwd] // empty' "$_mem0_map" 2>/dev/null)
|
||||
if [ -n "$_mem0_mapped" ]; then
|
||||
printf '%s' "$_mem0_mapped"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
# 3. Git remote slug
|
||||
_mem0_remote_url=$(git remote get-url origin 2>/dev/null)
|
||||
if [ -n "$_mem0_remote_url" ]; then
|
||||
_mem0_slug="$_mem0_remote_url"
|
||||
# Strip .git suffix
|
||||
_mem0_slug="${_mem0_slug%.git}"
|
||||
# Strip protocol prefixes
|
||||
_mem0_slug="${_mem0_slug#https://}"
|
||||
_mem0_slug="${_mem0_slug#http://}"
|
||||
_mem0_slug="${_mem0_slug#ssh://}"
|
||||
_mem0_slug="${_mem0_slug#git://}"
|
||||
_mem0_slug="${_mem0_slug#git@}"
|
||||
# Replace first colon (SSH host:path separator) with /
|
||||
# shellcheck disable=SC2039
|
||||
_mem0_slug="${_mem0_slug/://}"
|
||||
# Keep only the last two path components (owner/repo)
|
||||
_mem0_owner=$(printf '%s' "$_mem0_slug" | awk -F'/' '{print $(NF-1)}')
|
||||
_mem0_repo=$(printf '%s' "$_mem0_slug" | awk -F'/' '{print $NF}')
|
||||
_mem0_slug="${_mem0_owner}-${_mem0_repo}"
|
||||
# Replace any remaining / and : with -
|
||||
# shellcheck disable=SC2039
|
||||
_mem0_slug="${_mem0_slug//\//-}"
|
||||
# shellcheck disable=SC2039
|
||||
_mem0_slug="${_mem0_slug//:/-}"
|
||||
if [ -n "$_mem0_slug" ]; then
|
||||
printf '%s' "$_mem0_slug"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
# 4. Fallback: basename of $PWD
|
||||
printf '%s' "$(basename "$PWD")"
|
||||
}
|
||||
|
||||
_mem0_resolve_branch() {
|
||||
git branch --show-current 2>/dev/null || printf 'unknown'
|
||||
}
|
||||
|
||||
MEM0_PROJECT_ID="$(_mem0_resolve_project_id)"
|
||||
MEM0_BRANCH="$(_mem0_resolve_branch)"
|
||||
export MEM0_PROJECT_ID
|
||||
export MEM0_BRANCH
|
||||
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Auto-import declarative project files into mem0.
|
||||
|
||||
Runs in the background from the SessionStart hook (startup only).
|
||||
Imports CLAUDE.md, AGENTS.md, .cursorrules, .windsurfrules, mem0.md
|
||||
into mem0 as project profile memories, skipping unchanged files via
|
||||
SHA-256 hashing.
|
||||
|
||||
Input: MEM0_CWD env var (optional, defaults to os.getcwd())
|
||||
Output: stderr logs only (exit 0 always — must not block)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _identity import resolve_user_id
|
||||
from _project import resolve_project_id
|
||||
|
||||
log = logging.getLogger("mem0-auto-import")
|
||||
log.setLevel(logging.DEBUG)
|
||||
_handler = logging.StreamHandler(sys.stderr)
|
||||
_handler.setFormatter(logging.Formatter("[mem0-auto-import] %(message)s"))
|
||||
log.addHandler(_handler)
|
||||
|
||||
if os.environ.get("MEM0_DEBUG"):
|
||||
_log_dir = os.path.expanduser("~/.mem0")
|
||||
try:
|
||||
os.makedirs(_log_dir, exist_ok=True)
|
||||
_file_handler = logging.FileHandler(os.path.join(_log_dir, "hooks.log"))
|
||||
_file_handler.setFormatter(logging.Formatter("[mem0-auto-import] %(asctime)s %(message)s"))
|
||||
log.addHandler(_file_handler)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
API_URL = "https://api.mem0.ai"
|
||||
MAX_FILE_SIZE = 100_000 # skip files over 100 KB
|
||||
TARGET_FILES = ["CLAUDE.md", "AGENTS.md", ".cursorrules", ".windsurfrules", "mem0.md"]
|
||||
HASH_STORE = os.path.expanduser("~/.mem0/file_hashes.json")
|
||||
|
||||
|
||||
def sha256_file(path: str) -> str:
|
||||
"""Return the hex SHA-256 digest of a file."""
|
||||
h = hashlib.sha256()
|
||||
with open(path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(65536), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def load_hashes() -> dict[str, str]:
|
||||
"""Load the hash store from disk; return empty dict on any error."""
|
||||
if not os.path.isfile(HASH_STORE):
|
||||
return {}
|
||||
try:
|
||||
with open(HASH_STORE) as f:
|
||||
return json.load(f)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return {}
|
||||
|
||||
|
||||
def save_hashes(hashes: dict[str, str]) -> None:
|
||||
"""Persist the hash store to disk."""
|
||||
mem0_dir = os.path.expanduser("~/.mem0")
|
||||
os.makedirs(mem0_dir, exist_ok=True)
|
||||
try:
|
||||
with open(HASH_STORE, "w") as f:
|
||||
json.dump(hashes, f, indent=2)
|
||||
except OSError as e:
|
||||
log.warning("Could not save hash store: %s", e)
|
||||
|
||||
|
||||
def post_memory(api_key: str, content: str, user_id: str, filename: str, project_id: str) -> bool:
|
||||
"""POST a project profile memory to the Mem0 REST API."""
|
||||
body = {
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"## Project Profile: {filename}\n\nProject: {project_id}\n\n{content}",
|
||||
}
|
||||
],
|
||||
"user_id": user_id,
|
||||
"metadata": {
|
||||
"type": "project_profile",
|
||||
"file": filename,
|
||||
"project_id": project_id,
|
||||
"source": "auto-import",
|
||||
},
|
||||
"infer": False,
|
||||
}
|
||||
|
||||
data = json.dumps(body).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
f"{API_URL}/v1/memories/",
|
||||
data=data,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Token {api_key}",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
if resp.status in (200, 201):
|
||||
log.info("Imported %s (project=%s)", filename, project_id)
|
||||
return True
|
||||
log.warning("API returned status %d for %s", resp.status, filename)
|
||||
return False
|
||||
except urllib.error.URLError as e:
|
||||
log.warning("API call failed for %s: %s", filename, e)
|
||||
return False
|
||||
|
||||
|
||||
def main() -> None:
|
||||
api_key = os.environ.get("MEM0_API_KEY", "")
|
||||
if not api_key:
|
||||
log.debug("MEM0_API_KEY not set, skipping auto-import")
|
||||
return
|
||||
|
||||
cwd = os.environ.get("MEM0_CWD", "").strip() or os.getcwd()
|
||||
user_id = resolve_user_id()
|
||||
project_id = resolve_project_id(cwd)
|
||||
|
||||
log.debug("Auto-import started: cwd=%s project=%s user=%s", cwd, project_id, user_id)
|
||||
|
||||
hashes = load_hashes()
|
||||
updated = False
|
||||
|
||||
for filename in TARGET_FILES:
|
||||
filepath = os.path.join(cwd, filename)
|
||||
|
||||
if not os.path.isfile(filepath):
|
||||
log.debug("Not found, skipping: %s", filename)
|
||||
continue
|
||||
|
||||
try:
|
||||
file_size = os.path.getsize(filepath)
|
||||
except OSError:
|
||||
log.debug("Cannot stat %s, skipping", filename)
|
||||
continue
|
||||
|
||||
if file_size > MAX_FILE_SIZE:
|
||||
log.debug("Skipping %s: size %d exceeds %d bytes", filename, file_size, MAX_FILE_SIZE)
|
||||
continue
|
||||
|
||||
try:
|
||||
current_hash = sha256_file(filepath)
|
||||
except OSError as e:
|
||||
log.debug("Cannot hash %s: %s", filename, e)
|
||||
continue
|
||||
|
||||
hash_key = f"{project_id}:{filename}"
|
||||
if hashes.get(hash_key) == current_hash:
|
||||
log.debug("Unchanged, skipping: %s", filename)
|
||||
continue
|
||||
|
||||
try:
|
||||
with open(filepath, encoding="utf-8", errors="replace") as f:
|
||||
content = f.read()
|
||||
except OSError as e:
|
||||
log.debug("Cannot read %s: %s", filename, e)
|
||||
continue
|
||||
|
||||
if post_memory(api_key, content, user_id, filename, project_id):
|
||||
hashes[hash_key] = current_hash
|
||||
updated = True
|
||||
# on API failure we don't update the hash — retry next session
|
||||
|
||||
if updated:
|
||||
save_hashes(hashes)
|
||||
else:
|
||||
log.debug("No files imported this run")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except Exception as e:
|
||||
log.error("Unexpected error: %s", e)
|
||||
sys.exit(0)
|
||||
@@ -13,6 +13,10 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ -n "${MEM0_DEBUG:-}" ]; then
|
||||
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
|
||||
fi
|
||||
|
||||
INPUT=$(cat)
|
||||
|
||||
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // ""' 2>/dev/null || echo "")
|
||||
@@ -22,7 +26,7 @@ if [ -z "$FILE_PATH" ]; then
|
||||
fi
|
||||
|
||||
case "$FILE_PATH" in
|
||||
*/MEMORY.md|*/memory/*.md|*/.claude/*/memory/*)
|
||||
*/MEMORY.md|*/.claude/memory/*)
|
||||
echo "BLOCKED: Do not write to $FILE_PATH. Use the mem0 MCP \`add_memory\` tool instead to persist memories. This project uses mem0 for all memory storage." >&2
|
||||
exit 2
|
||||
;;
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hook: preToolUse (Cursor) — blocks writes to MEMORY.md
|
||||
#
|
||||
# Cursor variant of block_memory_write.sh. Returns JSON:
|
||||
# {"permission":"deny","agent_message":"..."} on block,
|
||||
# {"permission":"allow"} on pass.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
if [ -n "${MEM0_DEBUG:-}" ]; then
|
||||
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
|
||||
fi
|
||||
|
||||
INPUT=$(cat)
|
||||
|
||||
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // ""' 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$FILE_PATH" ]; then
|
||||
jq -cn '{permission:"allow"}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
case "$FILE_PATH" in
|
||||
*/MEMORY.md|*/.claude/memory/*|*/.cursor/memory/*)
|
||||
jq -cn --arg msg "Do not write to $FILE_PATH. Use the mem0 MCP add_memory tool instead to persist memories." \
|
||||
'{permission:"deny", agent_message:$msg}'
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
jq -cn '{permission:"allow"}'
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Capture the post-compaction summary into mem0.
|
||||
|
||||
PreCompact hooks fire BEFORE the summary is generated, so they can't
|
||||
store the actual compact-summary text. This script runs at
|
||||
SessionStart with source=compact, reads the transcript, finds the
|
||||
most recent entry flagged isCompactSummary=true, and stores it as a
|
||||
memory tagged metadata.type=compact_summary.
|
||||
|
||||
Input: JSON on stdin with transcript_path, session_id, source
|
||||
Output: stderr logs only (exit 0 always -- must not block)
|
||||
|
||||
Spawned in the background by on_session_start.sh; the user-facing
|
||||
bootstrap text continues without waiting on the network.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from datetime import date, timedelta
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _identity import resolve_user_id
|
||||
from _project import resolve_branch, resolve_project_id
|
||||
|
||||
log = logging.getLogger("mem0-compact-summary")
|
||||
log.setLevel(logging.DEBUG)
|
||||
_handler = logging.StreamHandler(sys.stderr)
|
||||
_handler.setFormatter(logging.Formatter("[mem0-compact-summary] %(message)s"))
|
||||
log.addHandler(_handler)
|
||||
|
||||
if os.environ.get("MEM0_DEBUG"):
|
||||
_log_dir = os.path.expanduser("~/.mem0")
|
||||
try:
|
||||
os.makedirs(_log_dir, exist_ok=True)
|
||||
_file_handler = logging.FileHandler(os.path.join(_log_dir, "hooks.log"))
|
||||
_file_handler.setFormatter(logging.Formatter("[mem0-compact-summary] %(asctime)s %(message)s"))
|
||||
log.addHandler(_file_handler)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
API_URL = "https://api.mem0.ai"
|
||||
MAX_TAIL_LINES = 2000
|
||||
MAX_SUMMARY_CHARS = 50000
|
||||
# Compact summaries describe a single session's state -- stale after a quarter.
|
||||
COMPACT_SUMMARY_EXPIRY_DAYS = 90
|
||||
|
||||
|
||||
def tail_lines(filepath: str, n: int) -> list[str]:
|
||||
try:
|
||||
with open(filepath, "rb") as f:
|
||||
f.seek(0, 2)
|
||||
file_size = f.tell()
|
||||
if file_size == 0:
|
||||
return []
|
||||
chunk_size = min(file_size, n * 4096)
|
||||
f.seek(max(0, file_size - chunk_size))
|
||||
data = f.read().decode("utf-8", errors="replace")
|
||||
return data.splitlines()[-n:]
|
||||
except OSError:
|
||||
return []
|
||||
|
||||
|
||||
def find_compact_summary(lines: list[str]) -> str:
|
||||
"""Walk transcript backwards, return text content of the most recent
|
||||
entry flagged isCompactSummary=true. Empty string if none found."""
|
||||
for line in reversed(lines):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
entry = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if not entry.get("isCompactSummary"):
|
||||
continue
|
||||
|
||||
message = entry.get("message", {})
|
||||
content = message.get("content", [])
|
||||
if isinstance(content, str):
|
||||
return content[:MAX_SUMMARY_CHARS]
|
||||
if isinstance(content, list):
|
||||
parts = []
|
||||
for block in content:
|
||||
if isinstance(block, str):
|
||||
parts.append(block)
|
||||
elif isinstance(block, dict) and block.get("type") == "text":
|
||||
parts.append(block.get("text", ""))
|
||||
return "\n".join(parts).strip()[:MAX_SUMMARY_CHARS]
|
||||
return ""
|
||||
|
||||
|
||||
def store_summary(api_key: str, summary: str, user_id: str, session_id: str, project_id: str = "", branch: str = "") -> bool:
|
||||
expires = (date.today() + timedelta(days=COMPACT_SUMMARY_EXPIRY_DAYS)).isoformat()
|
||||
body = {
|
||||
"messages": [{"role": "user", "content": summary}],
|
||||
"user_id": user_id,
|
||||
"metadata": {
|
||||
"type": "compact_summary",
|
||||
"source": "session-start-compact",
|
||||
"session_id": session_id,
|
||||
"project_id": project_id,
|
||||
"branch": branch,
|
||||
},
|
||||
"infer": False,
|
||||
"expiration_date": expires,
|
||||
}
|
||||
|
||||
data = json.dumps(body).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
f"{API_URL}/v1/memories/",
|
||||
data=data,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Token {api_key}",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
if resp.status in (200, 201):
|
||||
log.info("Compact summary stored")
|
||||
return True
|
||||
log.warning("API returned status %d", resp.status)
|
||||
return False
|
||||
except urllib.error.URLError as e:
|
||||
log.warning("API call failed: %s", e)
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
api_key = os.environ.get("MEM0_API_KEY", "")
|
||||
if not api_key:
|
||||
log.debug("MEM0_API_KEY not set, skipping capture")
|
||||
return
|
||||
|
||||
try:
|
||||
hook_input = json.loads(sys.stdin.read())
|
||||
except (json.JSONDecodeError, OSError):
|
||||
log.debug("No valid JSON on stdin")
|
||||
return
|
||||
|
||||
transcript_path = hook_input.get("transcript_path", "")
|
||||
if not transcript_path:
|
||||
log.debug("No transcript_path provided")
|
||||
return
|
||||
|
||||
session_id = hook_input.get("session_id", "")
|
||||
user_id = resolve_user_id()
|
||||
project_id = resolve_project_id()
|
||||
branch = resolve_branch()
|
||||
|
||||
lines = tail_lines(transcript_path, MAX_TAIL_LINES)
|
||||
if not lines:
|
||||
log.debug("Transcript empty or unreadable: %s", transcript_path)
|
||||
return
|
||||
|
||||
summary = find_compact_summary(lines)
|
||||
if not summary:
|
||||
log.debug("No isCompactSummary entry found")
|
||||
return
|
||||
|
||||
log.info("Capturing compact summary (%d chars)", len(summary))
|
||||
store_summary(api_key, summary, user_id, session_id, project_id, branch)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except Exception as e:
|
||||
log.error("Unexpected error: %s", e)
|
||||
sys.exit(0)
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hook: PostToolUse — track mem0 MCP tool usage for session stats
|
||||
#
|
||||
# Fires after any tool call. We only care about mem0 MCP tools:
|
||||
# mcp__mem0__add_memory → record an add
|
||||
# mcp__mem0__search_memories → record a search
|
||||
#
|
||||
# Input: JSON on stdin with tool_name, tool_input, tool_result
|
||||
# Output: none (exit 0, non-blocking)
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
INPUT=$(cat)
|
||||
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null || echo "")
|
||||
|
||||
case "$TOOL_NAME" in
|
||||
mcp__mem0__add_memory)
|
||||
CATEGORY=$(echo "$INPUT" | jq -r '.tool_input.metadata.type // .tool_input.metadata.category // ""' 2>/dev/null || echo "")
|
||||
python3 "$SCRIPT_DIR/session_stats.py" add "$CATEGORY" 2>/dev/null || true
|
||||
;;
|
||||
mcp__mem0__search_memories|mcp__mem0__get_memories)
|
||||
python3 "$SCRIPT_DIR/session_stats.py" search 2>/dev/null || true
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hook: postToolUse (Cursor) — track mem0 MCP tool usage for session stats
|
||||
#
|
||||
# Wraps on_post_tool_use.sh. Cursor expects JSON output but PostToolUse
|
||||
# has no meaningful return value, so we output {} after tracking.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
# Run the shared tracker (output is ignored)
|
||||
"$SCRIPT_DIR/on_post_tool_use.sh" 2>/dev/null || true
|
||||
|
||||
echo '{}'
|
||||
exit 0
|
||||
@@ -18,8 +18,13 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from datetime import date, timedelta
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _identity import resolve_user_id
|
||||
from _project import resolve_branch, resolve_project_id
|
||||
|
||||
log = logging.getLogger("mem0-capture")
|
||||
log.setLevel(logging.DEBUG)
|
||||
@@ -27,11 +32,25 @@ _handler = logging.StreamHandler(sys.stderr)
|
||||
_handler.setFormatter(logging.Formatter("[mem0-capture] %(message)s"))
|
||||
log.addHandler(_handler)
|
||||
|
||||
if os.environ.get("MEM0_DEBUG"):
|
||||
_log_dir = os.path.expanduser("~/.mem0")
|
||||
try:
|
||||
os.makedirs(_log_dir, exist_ok=True)
|
||||
_file_handler = logging.FileHandler(os.path.join(_log_dir, "hooks.log"))
|
||||
_file_handler.setFormatter(logging.Formatter("[mem0-capture] %(asctime)s %(message)s"))
|
||||
log.addHandler(_file_handler)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
API_URL = "https://api.mem0.ai"
|
||||
MAX_TAIL_LINES = 500
|
||||
MAX_USER_MESSAGES = 30
|
||||
MAX_BASH_COMMANDS = 20
|
||||
MAX_ASSISTANT_TEXT = 10000
|
||||
# session_state captures churn fast (active codebase, files in flight). Past
|
||||
# ~3 months they're stale noise. Durable facts (decisions, conventions) are
|
||||
# stored separately by the agent without an expiration_date.
|
||||
SESSION_STATE_EXPIRY_DAYS = 90
|
||||
|
||||
|
||||
def tail_lines(filepath: str, n: int) -> list[str]:
|
||||
@@ -149,8 +168,9 @@ def build_content(state: dict, source: str) -> str:
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def store_memory(api_key: str, content: str, user_id: str, source: str) -> bool:
|
||||
def store_memory(api_key: str, content: str, user_id: str, source: str, session_id: str = "", project_id: str = "", branch: str = "") -> bool:
|
||||
"""Store session state as a memory via the Mem0 REST API."""
|
||||
expires = (date.today() + timedelta(days=SESSION_STATE_EXPIRY_DAYS)).isoformat()
|
||||
body = {
|
||||
"messages": [
|
||||
{"role": "user", "content": content}
|
||||
@@ -159,7 +179,11 @@ def store_memory(api_key: str, content: str, user_id: str, source: str) -> bool:
|
||||
"metadata": {
|
||||
"type": "session_state",
|
||||
"source": source,
|
||||
"session_id": session_id,
|
||||
"project_id": project_id,
|
||||
"branch": branch,
|
||||
},
|
||||
"expiration_date": expires,
|
||||
}
|
||||
|
||||
data = json.dumps(body).encode("utf-8")
|
||||
@@ -207,7 +231,10 @@ def main():
|
||||
log.debug("No transcript_path provided")
|
||||
return
|
||||
|
||||
user_id = os.environ.get("MEM0_USER_ID", os.environ.get("USER", "default"))
|
||||
session_id = hook_input.get("session_id", "")
|
||||
user_id = resolve_user_id()
|
||||
project_id = resolve_project_id()
|
||||
branch = resolve_branch()
|
||||
|
||||
lines = tail_lines(transcript_path, MAX_TAIL_LINES)
|
||||
if not lines:
|
||||
@@ -228,7 +255,7 @@ def main():
|
||||
len(state["bash_commands"]),
|
||||
)
|
||||
|
||||
store_memory(api_key, content, user_id, source)
|
||||
store_memory(api_key, content, user_id, source, session_id, project_id, branch)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -5,12 +5,16 @@
|
||||
# the full context before it gets compressed.
|
||||
#
|
||||
# Output: Text instructions injected into Claude's context.
|
||||
# Claude still has the full conversation and can write an accurate summary.
|
||||
# A companion Python script (on_pre_compact.py) also runs to capture
|
||||
# transcript state directly via the Mem0 REST API as a safety net.
|
||||
# Claude still has the full conversation and can write an accurate summary,
|
||||
# which it stores via add_memory(infer=False) so the platform preserves
|
||||
# the structure verbatim instead of running a second extraction pass.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ -n "${MEM0_DEBUG:-}" ]; then
|
||||
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
|
||||
fi
|
||||
|
||||
cat <<'EOF'
|
||||
## CRITICAL: Pre-Compaction Session Summary
|
||||
|
||||
@@ -18,7 +22,9 @@ Context compaction is about to happen. You are about to lose most of your conver
|
||||
|
||||
### Step 1: Store session summary
|
||||
|
||||
Call `add_memory` with a thorough summary covering ALL of the following:
|
||||
Call `add_memory` with `infer=False` and a thorough summary covering ALL of the following.
|
||||
|
||||
`infer=False` is critical here: you've already done the extraction work yourself using full context. Without it, the platform runs a second LLM pass that loses your structure and pulls fragmented facts. With it, your summary is preserved verbatim.
|
||||
|
||||
```
|
||||
## Session Summary (Pre-Compaction)
|
||||
@@ -44,11 +50,19 @@ Call `add_memory` with a thorough summary covering ALL of the following:
|
||||
the post-compaction agent continue without asking redundant questions]
|
||||
```
|
||||
|
||||
Include metadata: `{"type": "session_state", "source": "pre-compaction"}`
|
||||
Tool call shape:
|
||||
```
|
||||
add_memory(
|
||||
messages=[{"role":"user","content":"<the summary above>"}],
|
||||
user_id="<the active user_id from the SessionStart bootstrap>",
|
||||
metadata={"type":"session_state","source":"pre-compaction","project_id":"<the active project_id>","branch":"<the active branch>"},
|
||||
infer=False,
|
||||
)
|
||||
```
|
||||
|
||||
### Step 2: Store any unstored learnings
|
||||
|
||||
If there are learnings from this session that you haven't stored yet, store them as separate memories:
|
||||
If there are learnings from this session that you haven't stored yet, store them as separate memories with `infer=False` (same reasoning -- you've already extracted the fact, don't re-extract):
|
||||
- Failed approaches -> metadata `{"type": "anti_pattern"}`
|
||||
- Successful strategies -> metadata `{"type": "task_learning"}`
|
||||
- Architecture decisions -> metadata `{"type": "decision"}`
|
||||
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hook: preCompact (Cursor)
|
||||
#
|
||||
# Wraps on_pre_compact.sh and converts plain-text output to Cursor's
|
||||
# expected JSON format: {"user_message":"<text>"}
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
TEXT=$("$SCRIPT_DIR/on_pre_compact.sh" 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$TEXT" ]; then
|
||||
echo '{}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
jq -cn --arg msg "$TEXT" '{user_message:$msg}'
|
||||
exit 0
|
||||
@@ -11,9 +11,73 @@
|
||||
# even if jq is missing or stdin is malformed.
|
||||
set -uo pipefail
|
||||
|
||||
if [ -n "${MEM0_DEBUG:-}" ]; then
|
||||
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
|
||||
fi
|
||||
|
||||
# Skip the bootstrap entirely if no API key is configured -- the agent
|
||||
# would otherwise be told to call mem0 MCP tools that will all fail.
|
||||
if [ -z "${MEM0_API_KEY:-}" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
# shellcheck source=_identity.sh
|
||||
. "$SCRIPT_DIR/_identity.sh"
|
||||
|
||||
# Initialize session stats tracker
|
||||
python3 "$SCRIPT_DIR/session_stats.py" init 2>/dev/null || true
|
||||
|
||||
INPUT=$(cat)
|
||||
SOURCE=$(echo "$INPUT" | jq -r '.source // "startup"' 2>/dev/null || echo "startup")
|
||||
|
||||
# Fetch project-scoped memory count (best-effort, don't block on failure, 5s timeout)
|
||||
MEM0_COUNT="?"
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
MEM0_COUNT=$(python3 -c "
|
||||
import json, os, urllib.request, urllib.error
|
||||
api_key = os.environ.get('MEM0_API_KEY', '')
|
||||
user_id = os.environ.get('MEM0_RESOLVED_USER_ID', 'default')
|
||||
project_id = os.environ.get('MEM0_PROJECT_ID', '')
|
||||
body = json.dumps({
|
||||
'query': 'project context',
|
||||
'user_id': user_id,
|
||||
'filters': {'AND': [{'user_id': user_id}, {'metadata': {'project_id': project_id}}]},
|
||||
'limit': 100,
|
||||
}).encode()
|
||||
req = urllib.request.Request(
|
||||
'https://api.mem0.ai/v2/memories/search/',
|
||||
data=body,
|
||||
headers={'Authorization': f'Token {api_key}', 'Content-Type': 'application/json'},
|
||||
method='POST',
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=5) as r:
|
||||
results = json.loads(r.read())
|
||||
if isinstance(results, list):
|
||||
n = len(results)
|
||||
print(f'{n}+' if n >= 100 else str(n))
|
||||
else:
|
||||
print('?')
|
||||
except Exception:
|
||||
print('?')
|
||||
" 2>/dev/null || echo "?")
|
||||
fi
|
||||
|
||||
# Identity line is emitted before every bootstrap variant so the agent
|
||||
# uses the same user_id the hooks resolved. Without this, the agent's
|
||||
# search_memories/add_memory MCP calls may bind to a different bucket
|
||||
# than what the hooks write to.
|
||||
echo "## Mem0 Active"
|
||||
echo ""
|
||||
echo "\`user=$MEM0_RESOLVED_USER_ID | project=$MEM0_PROJECT_ID | branch=$MEM0_BRANCH | memories=$MEM0_COUNT\`"
|
||||
echo ""
|
||||
echo "Always include \`user_id\` + \`metadata.project_id\` in every \`search_memories\` filter and \`add_memory\` call:"
|
||||
echo "- user_id: \`$MEM0_RESOLVED_USER_ID\`"
|
||||
echo "- project_id: \`$MEM0_PROJECT_ID\`"
|
||||
echo "- branch: \`$MEM0_BRANCH\` (include in session-state / compact-summary metadata)"
|
||||
echo ""
|
||||
|
||||
if [ "$SOURCE" = "startup" ]; then
|
||||
cat <<'EOF'
|
||||
## Mem0 Session Bootstrap
|
||||
@@ -27,6 +91,10 @@ You have access to persistent memory via the mem0 MCP tools. Before doing anythi
|
||||
IMPORTANT: Do NOT skip this step. Always bootstrap context first.
|
||||
EOF
|
||||
|
||||
# Auto-import declarative project files in background
|
||||
MEM0_CWD="$(echo "$INPUT" | jq -r '.cwd // "."' 2>/dev/null || echo ".")" \
|
||||
python3 "$SCRIPT_DIR/auto_import.py" 2>/dev/null &
|
||||
|
||||
elif [ "$SOURCE" = "resume" ]; then
|
||||
cat <<'EOF'
|
||||
## Mem0 Session Resumed
|
||||
@@ -40,14 +108,22 @@ Continue where you left off.
|
||||
EOF
|
||||
|
||||
elif [ "$SOURCE" = "compact" ]; then
|
||||
# Capture the just-generated compact summary in the background.
|
||||
# PreCompact fires too early to see this entry; SessionStart-compact
|
||||
# is the first place isCompactSummary=true is in the transcript.
|
||||
echo "$INPUT" | python3 "$SCRIPT_DIR/capture_compact_summary.py" 2>/dev/null &
|
||||
|
||||
cat <<'EOF'
|
||||
## Mem0 Post-Compaction Recovery
|
||||
|
||||
Context was just compacted. You may have lost important session context.
|
||||
Context was just compacted. The Claude Code-generated compact summary
|
||||
is being captured to mem0 in the background as `metadata.type=compact_summary`.
|
||||
|
||||
1. Call `search_memories` with queries related to what you were working on to reload relevant knowledge.
|
||||
2. Check for any session state memories that were saved before compaction.
|
||||
3. Continue working based on the recovered context.
|
||||
1. Call `search_memories` to reload context, layering up to three angles:
|
||||
- `metadata.type=session_state` -- the rich pre-compaction summary you wrote
|
||||
- `metadata.type=compact_summary` -- the platform-generated condensed summary just now
|
||||
- `metadata.type=decision` / `anti_pattern` -- specific facts you stored during the session
|
||||
2. Continue working from the recovered context.
|
||||
EOF
|
||||
fi
|
||||
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hook: sessionStart (Cursor)
|
||||
#
|
||||
# Wraps on_session_start.sh and converts plain-text output to Cursor's
|
||||
# expected JSON format: {"additional_context":"<text>"}
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
TEXT=$("$SCRIPT_DIR/on_session_start.sh" 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$TEXT" ]; then
|
||||
echo '{}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
jq -cn --arg ctx "$TEXT" '{additional_context:$ctx}'
|
||||
exit 0
|
||||
@@ -12,6 +12,10 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ -n "${MEM0_DEBUG:-}" ]; then
|
||||
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
INPUT=$(cat)
|
||||
@@ -21,6 +25,22 @@ if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Print session-end report
|
||||
REPORT=$(python3 "$SCRIPT_DIR/session_stats.py" report 2>/dev/null || echo "")
|
||||
if [ -n "$REPORT" ]; then
|
||||
echo ""
|
||||
echo "---"
|
||||
echo "**mem0 $REPORT**"
|
||||
echo "---"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Append to persistent session log (guarded — on_stop.sh uses set -euo pipefail)
|
||||
if [ -n "$REPORT" ]; then
|
||||
mkdir -p "$HOME/.mem0" 2>/dev/null || true
|
||||
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | $REPORT" >> "$HOME/.mem0/session-log.md" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
cat <<'EOF'
|
||||
Before finishing, check if there are important learnings from this interaction that should be persisted using the mem0 `add_memory` tool:
|
||||
|
||||
@@ -33,6 +53,8 @@ Before finishing, check if there are important learnings from this interaction t
|
||||
Memories can be as detailed as needed — include full context, reasoning, code snippets, file paths, and examples. Longer, searchable memories are more valuable than vague one-liners.
|
||||
|
||||
If nothing notable happened in this interaction, it's fine to skip. Only store genuinely useful learnings.
|
||||
|
||||
Always include `"project_id"` in the metadata of any memory you store.
|
||||
EOF
|
||||
|
||||
# Capture transcript state in the background via Mem0 REST API
|
||||
|
||||
@@ -17,6 +17,12 @@
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
if [ -n "${MEM0_DEBUG:-}" ]; then
|
||||
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
INPUT=$(cat)
|
||||
STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false' 2>/dev/null || echo "false")
|
||||
|
||||
@@ -25,20 +31,35 @@ if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
REASON=$(cat <<'EOF'
|
||||
Before finishing, check if there are important learnings from this interaction that should be persisted using the mem0 `add_memory` tool:
|
||||
# Session-end report (best-effort, must not break JSON output)
|
||||
REPORT=$(python3 "$SCRIPT_DIR/session_stats.py" report 2>/dev/null || echo "")
|
||||
REPORT_BLOCK=""
|
||||
if [ -n "$REPORT" ]; then
|
||||
REPORT_BLOCK="---\nmem0 $REPORT\n---\n\n"
|
||||
mkdir -p "$HOME/.mem0" 2>/dev/null || true
|
||||
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | $REPORT" >> "$HOME/.mem0/session-log.md" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
1. Were any significant decisions made? -> Store with metadata `{"type": "decision"}`
|
||||
2. Were any new patterns or strategies discovered? -> Store with metadata `{"type": "task_learning"}`
|
||||
3. Did any approach fail? -> Store with metadata `{"type": "anti_pattern"}`
|
||||
4. Did you learn anything about the user's preferences? -> Store with metadata `{"type": "user_preference"}`
|
||||
5. Were there environment/setup discoveries? -> Store with metadata `{"type": "environmental"}`
|
||||
REASON=$(cat <<EOF
|
||||
${REPORT_BLOCK}Before finishing, check if there are important learnings from this interaction that should be persisted using the mem0 \`add_memory\` tool:
|
||||
|
||||
1. Were any significant decisions made? -> Store with metadata \`{"type": "decision"}\`
|
||||
2. Were any new patterns or strategies discovered? -> Store with metadata \`{"type": "task_learning"}\`
|
||||
3. Did any approach fail? -> Store with metadata \`{"type": "anti_pattern"}\`
|
||||
4. Did you learn anything about the user's preferences? -> Store with metadata \`{"type": "user_preference"}\`
|
||||
5. Were there environment/setup discoveries? -> Store with metadata \`{"type": "environmental"}\`
|
||||
|
||||
Memories can be as detailed as needed — include full context, reasoning, code snippets, file paths, and examples. Longer, searchable memories are more valuable than vague one-liners.
|
||||
|
||||
Always include \`"project_id"\` in the metadata of any memory you store.
|
||||
|
||||
If nothing notable happened in this interaction, it's fine to skip. Only store genuinely useful learnings.
|
||||
EOF
|
||||
)
|
||||
|
||||
jq -cn --arg reason "$REASON" '{decision:"block", reason:$reason}'
|
||||
|
||||
# Capture transcript state in the background via Mem0 REST API
|
||||
echo "$INPUT" | python3 "$SCRIPT_DIR/on_pre_compact.py" --source=session-end 2>/dev/null &
|
||||
|
||||
exit 0
|
||||
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hook: Stop (Cursor)
|
||||
#
|
||||
# Fires when Cursor agent completes a turn. Wraps the same logic as
|
||||
# on_stop.sh but outputs JSON (Cursor expects {"followup_message":"..."}).
|
||||
#
|
||||
# Input: JSON on stdin with status, loop_count, conversation_id, etc.
|
||||
# Output: JSON on stdout: {"followup_message":"<reminder text>"}
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
if [ -n "${MEM0_DEBUG:-}" ]; then
|
||||
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
INPUT=$(cat)
|
||||
|
||||
# Session-end report (best-effort)
|
||||
REPORT=$(python3 "$SCRIPT_DIR/session_stats.py" report 2>/dev/null || echo "")
|
||||
REPORT_BLOCK=""
|
||||
if [ -n "$REPORT" ]; then
|
||||
REPORT_BLOCK="---\nmem0 $REPORT\n---\n\n"
|
||||
mkdir -p "$HOME/.mem0" 2>/dev/null || true
|
||||
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | $REPORT" >> "$HOME/.mem0/session-log.md" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
MESSAGE=$(cat <<EOF
|
||||
${REPORT_BLOCK}Before finishing, check if there are important learnings from this interaction that should be persisted using the mem0 \`add_memory\` tool:
|
||||
|
||||
1. Were any significant decisions made? -> Store with metadata \`{"type": "decision"}\`
|
||||
2. Were any new patterns or strategies discovered? -> Store with metadata \`{"type": "task_learning"}\`
|
||||
3. Did any approach fail? -> Store with metadata \`{"type": "anti_pattern"}\`
|
||||
4. Did you learn anything about the user's preferences? -> Store with metadata \`{"type": "user_preference"}\`
|
||||
5. Were there environment/setup discoveries? -> Store with metadata \`{"type": "environmental"}\`
|
||||
|
||||
Always include \`"project_id"\` in the metadata of any memory you store.
|
||||
|
||||
If nothing notable happened, it's fine to skip. Only store genuinely useful learnings.
|
||||
EOF
|
||||
)
|
||||
|
||||
jq -cn --arg msg "$MESSAGE" '{followup_message:$msg}'
|
||||
|
||||
# Capture transcript state in the background via Mem0 REST API
|
||||
echo "$INPUT" | python3 "$SCRIPT_DIR/on_pre_compact.py" --source=session-end 2>/dev/null &
|
||||
|
||||
exit 0
|
||||
@@ -9,6 +9,13 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ -n "${MEM0_DEBUG:-}" ]; then
|
||||
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
. "$SCRIPT_DIR/_identity.sh" || true
|
||||
|
||||
INPUT=$(cat)
|
||||
TASK_SUBJECT=$(echo "$INPUT" | jq -r '.task_subject // "unknown task"' 2>/dev/null || echo "unknown task")
|
||||
|
||||
@@ -24,6 +31,7 @@ Extract key learnings from this completed task and store them using the mem0 \`a
|
||||
|
||||
Memories can be as detailed as needed — include full context, reasoning, code snippets, and examples.
|
||||
Only store genuinely useful learnings — skip if the task was trivial.
|
||||
Include \`"project_id": "$MEM0_PROJECT_ID"\` in metadata for all memories.
|
||||
EOF
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -1,61 +1,72 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hook: UserPromptSubmit
|
||||
#
|
||||
# Fires on every user message. Searches mem0 for relevant memories
|
||||
# and injects them into Claude's context before processing.
|
||||
# Fires on every user message. Instead of pre-searching mem0 with the
|
||||
# raw prompt, this injects a decision rubric telling the agent when
|
||||
# and how to search itself. The agent has more context than this
|
||||
# script does -- let it decide.
|
||||
#
|
||||
# Input: JSON on stdin with prompt, session_id, cwd, transcript_path
|
||||
# Output: Matching memories as context text (exit 0)
|
||||
#
|
||||
# Skips search for very short prompts (< 20 chars) and when
|
||||
# MEM0_API_KEY is not set. Uses a 3s timeout to minimize latency.
|
||||
# Input: JSON on stdin (prompt, session_id, cwd, transcript_path)
|
||||
# Output: Decision rubric injected into Claude's context (exit 0)
|
||||
|
||||
# Intentionally omit -e so the script always exits 0 even if
|
||||
# curl or jq fail — must never block the user's prompt.
|
||||
# Intentionally omit -e so the script always exits 0 even if jq fails --
|
||||
# must never block the user's prompt.
|
||||
set -uo pipefail
|
||||
|
||||
if [ -n "${MEM0_DEBUG:-}" ]; then
|
||||
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
|
||||
fi
|
||||
|
||||
INPUT=$(cat)
|
||||
PROMPT=$(echo "$INPUT" | jq -r '.prompt // ""' 2>/dev/null || echo "")
|
||||
|
||||
# Skip trivial prompts — not worth a network call
|
||||
# Acknowledgements and short replies don't warrant memory context
|
||||
if [ ${#PROMPT} -lt 20 ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
API_KEY="${MEM0_API_KEY:-}"
|
||||
if [ -z "$API_KEY" ]; then
|
||||
# No API key means the agent can't search anyway
|
||||
if [ -z "${MEM0_API_KEY:-}" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
USER_ID="${MEM0_USER_ID:-${USER:-default}}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
# shellcheck source=_identity.sh
|
||||
. "$SCRIPT_DIR/_identity.sh"
|
||||
USER_ID="$MEM0_RESOLVED_USER_ID"
|
||||
|
||||
# Build request body safely via jq to avoid injection
|
||||
BODY=$(jq -n --arg query "$PROMPT" --arg user_id "$USER_ID" \
|
||||
'{query: $query, filters: {user_id: $user_id}, top_k: 5}')
|
||||
cat <<EOF
|
||||
## Memory check
|
||||
|
||||
# Search mem0 for memories relevant to this prompt
|
||||
RESPONSE=$(curl -s --max-time 3 \
|
||||
-X POST "https://api.mem0.ai/v2/memories/search/" \
|
||||
-H "Authorization: Token $API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$BODY" \
|
||||
2>/dev/null || echo "")
|
||||
Before responding, decide whether persistent memory context from mem0 would
|
||||
improve your answer. The agent -- not this hook -- owns this decision.
|
||||
|
||||
if [ -z "$RESPONSE" ]; then
|
||||
exit 0
|
||||
fi
|
||||
**Search WHEN** the user:
|
||||
- references past work, decisions, or things "we" built
|
||||
- asks "how should we...", "best way to...", or any decision-style question
|
||||
- hits an error, bug, or asks for debugging help
|
||||
- requests work that touches their stack, tools, conventions, or preferences
|
||||
- starts a non-trivial task in a known project
|
||||
|
||||
# Extract memories from response (API returns a flat array)
|
||||
MEMORIES=$(echo "$RESPONSE" | jq -r '
|
||||
if type == "array" then . else .results // [] end |
|
||||
if length == 0 then empty else
|
||||
"## Relevant memories from mem0\n\n" +
|
||||
(map(select(.memory != null) | "- " + .memory) | join("\n"))
|
||||
end
|
||||
' 2>/dev/null || echo "")
|
||||
**Skip WHEN:**
|
||||
- the prompt is an acknowledgement or continuation
|
||||
- the user is *stating* new info -- that's a write trigger (\`add_memory\`), not a search
|
||||
- it's a pure syntax / factual question answerable from general knowledge
|
||||
- you already searched this scope earlier in the turn
|
||||
|
||||
if [ -n "$MEMORIES" ]; then
|
||||
echo "$MEMORIES"
|
||||
fi
|
||||
**If searching, do it well:**
|
||||
- Run **2-4 parallel** \`search_memories\` calls with different angles, not one
|
||||
query that echoes the user's prompt.
|
||||
- Phrase queries as **nouns** ("auth module decisions"), not full sentences.
|
||||
- Filter shape: the root must be a logical operator (\`AND\` / \`OR\` / \`NOT\`)
|
||||
with an array, and metadata uses a **nested** object (not dotted keys).
|
||||
Combine \`user_id\` with one \`metadata.type\` clause per call:
|
||||
- \`{"AND": [{"user_id": "$USER_ID"}, {"metadata": {"project_id": "$MEM0_PROJECT_ID"}}, {"metadata": {"type": "decision"}}]}\` -- design / architecture
|
||||
- \`{"AND": [{"user_id": "$USER_ID"}, {"metadata": {"project_id": "$MEM0_PROJECT_ID"}}, {"metadata": {"type": "anti_pattern"}}]}\` -- debugging, error handling
|
||||
- \`{"AND": [{"user_id": "$USER_ID"}, {"metadata": {"project_id": "$MEM0_PROJECT_ID"}}, {"metadata": {"type": "user_preference"}}]}\` -- tooling, stack, style
|
||||
- \`{"AND": [{"user_id": "$USER_ID"}, {"metadata": {"project_id": "$MEM0_PROJECT_ID"}}, {"metadata": {"type": "convention"}}]}\` -- established patterns
|
||||
- Or scope with just \`{"AND": [{"user_id": "$USER_ID"}, {"metadata": {"project_id": "$MEM0_PROJECT_ID"}}]}\` when no metadata filter fits.
|
||||
- Empty results are normal -- proceed without context.
|
||||
EOF
|
||||
|
||||
exit 0
|
||||
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hook: beforeSubmitPrompt (Cursor)
|
||||
#
|
||||
# Wraps on_user_prompt.sh and converts plain-text output to Cursor's
|
||||
# expected JSON format: {"continue":true,"user_message":"<text>"}
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
TEXT=$("$SCRIPT_DIR/on_user_prompt.sh" 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$TEXT" ]; then
|
||||
jq -cn '{continue:true}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
jq -cn --arg msg "$TEXT" '{continue:true, user_message:$msg}'
|
||||
exit 0
|
||||
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Session stats tracker for mem0 plugin.
|
||||
|
||||
Tracks memory adds/searches per session.
|
||||
Uses /tmp/mem0_session_stats_$USER.json (single file per user, reset on init).
|
||||
|
||||
Usage:
|
||||
python session_stats.py init # reset for new session
|
||||
python session_stats.py add <category> # record a memory write
|
||||
python session_stats.py search # record a search
|
||||
python session_stats.py report # print summary, clean up temp file
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
STATS_FILE = f"/tmp/mem0_session_stats_{os.environ.get('USER', 'default')}.json"
|
||||
|
||||
|
||||
def _load() -> dict:
|
||||
if os.path.isfile(STATS_FILE):
|
||||
try:
|
||||
with open(STATS_FILE) as f:
|
||||
return json.load(f)
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
return {"adds": 0, "searches": 0, "categories": [], "started": datetime.now().isoformat()}
|
||||
|
||||
|
||||
def _save(stats: dict) -> None:
|
||||
with open(STATS_FILE, "w") as f:
|
||||
json.dump(stats, f)
|
||||
|
||||
|
||||
def init() -> None:
|
||||
_save({"adds": 0, "searches": 0, "categories": [], "started": datetime.now().isoformat()})
|
||||
|
||||
|
||||
def record_add(category: str = "") -> None:
|
||||
stats = _load()
|
||||
stats["adds"] = stats.get("adds", 0) + 1
|
||||
if category and category not in stats.get("categories", []):
|
||||
stats.setdefault("categories", []).append(category)
|
||||
_save(stats)
|
||||
|
||||
|
||||
def record_search() -> None:
|
||||
stats = _load()
|
||||
stats["searches"] = stats.get("searches", 0) + 1
|
||||
_save(stats)
|
||||
|
||||
|
||||
def report() -> str:
|
||||
stats = _load()
|
||||
adds = stats.get("adds", 0)
|
||||
searches = stats.get("searches", 0)
|
||||
categories = stats.get("categories", [])
|
||||
|
||||
# Clean up temp file after reading
|
||||
try:
|
||||
os.unlink(STATS_FILE)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
if adds == 0 and searches == 0:
|
||||
return ""
|
||||
|
||||
parts = []
|
||||
parts.append(f"Session: wrote {adds} memories, retrieved {searches}")
|
||||
if categories:
|
||||
parts.append(f"Categories touched: {', '.join(categories)}")
|
||||
|
||||
return ". ".join(parts) + "."
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: session_stats.py [init|add|search|report]", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
cmd = sys.argv[1]
|
||||
if cmd == "init":
|
||||
init()
|
||||
elif cmd == "add":
|
||||
category = sys.argv[2] if len(sys.argv) > 2 else ""
|
||||
record_add(category)
|
||||
elif cmd == "search":
|
||||
record_search()
|
||||
elif cmd == "report":
|
||||
result = report()
|
||||
if result:
|
||||
print(result)
|
||||
else:
|
||||
print("Session: no memory operations.")
|
||||
else:
|
||||
print(f"Unknown command: {cmd}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Replace mem0's default category taxonomy with one tuned for coding workflows.
|
||||
|
||||
mem0 auto-tags every memory with one or more `categories`. By default the list
|
||||
is consumer-oriented (food, hobbies, music, ...), which is meaningless for code.
|
||||
This script replaces the project's category list with a coding-focused one.
|
||||
|
||||
The change is project-level (per the platform docs, per-request overrides are
|
||||
not supported on the managed API). Run once per project; future memories will
|
||||
be tagged using the new list automatically.
|
||||
|
||||
Usage:
|
||||
python setup_coding_categories.py # dry-run: show current vs proposed, no changes
|
||||
python setup_coding_categories.py --apply # actually call project.update()
|
||||
|
||||
Requires the mem0ai Python SDK and MEM0_API_KEY to be set.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
CODING_CATEGORIES = [
|
||||
{
|
||||
"architecture_decisions": (
|
||||
"Design choices, system structure, technology selection, trade-offs evaluated, "
|
||||
"and architectural patterns adopted in the project."
|
||||
)
|
||||
},
|
||||
{
|
||||
"anti_patterns": (
|
||||
"Approaches that failed, debugging dead-ends, common mistakes to avoid, "
|
||||
"and lessons learned from things that didn't work."
|
||||
)
|
||||
},
|
||||
{
|
||||
"task_learnings": (
|
||||
"Strategies and approaches that succeeded for specific tasks, including tooling "
|
||||
"tricks, workflow shortcuts, and effective problem-solving patterns."
|
||||
)
|
||||
},
|
||||
{
|
||||
"tooling_setup": (
|
||||
"Development environment, build tools, dependencies, package managers, deploy "
|
||||
"pipelines, and configuration steps for the project."
|
||||
)
|
||||
},
|
||||
{
|
||||
"bug_fixes": (
|
||||
"Specific bug fixes with root cause analysis, the fix applied, and how the bug "
|
||||
"was diagnosed -- useful for recognising similar issues later."
|
||||
)
|
||||
},
|
||||
{
|
||||
"coding_conventions": (
|
||||
"Code style, naming patterns, file organisation, error-handling conventions, "
|
||||
"and team agreements about how code is written in this project."
|
||||
)
|
||||
},
|
||||
{
|
||||
"user_preferences": (
|
||||
"User's stated preferences for tools, libraries, languages, formatting, "
|
||||
"and ways of working."
|
||||
)
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def _print_categories(label: str, cats):
|
||||
print(f"=== {label} ===")
|
||||
if cats:
|
||||
print(json.dumps(cats, indent=2))
|
||||
else:
|
||||
print("(none / using mem0 defaults)")
|
||||
print()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument(
|
||||
"--apply",
|
||||
action="store_true",
|
||||
help="Actually call project.update(). Without this flag, runs in dry-run mode.",
|
||||
)
|
||||
args = ap.parse_args()
|
||||
|
||||
if not os.environ.get("MEM0_API_KEY"):
|
||||
print("ERROR: MEM0_API_KEY is not set. Export it and try again.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
try:
|
||||
from mem0 import MemoryClient
|
||||
except ImportError:
|
||||
print(
|
||||
"ERROR: the mem0ai Python SDK is not installed.\n"
|
||||
"Install with: pip install mem0ai\n"
|
||||
"Then re-run this script.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
try:
|
||||
client = MemoryClient()
|
||||
except Exception as e:
|
||||
print(
|
||||
f"ERROR initialising MemoryClient: {e}\n"
|
||||
"Most commonly this is an invalid MEM0_API_KEY -- check the key at "
|
||||
"https://app.mem0.ai/dashboard/api-keys",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
try:
|
||||
current = client.project.get(fields=["custom_categories"])
|
||||
current_cats = current.get("custom_categories") if isinstance(current, dict) else None
|
||||
except Exception as e:
|
||||
print(f"ERROR fetching current categories: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
_print_categories("Current project categories", current_cats)
|
||||
_print_categories("Proposed coding categories", CODING_CATEGORIES)
|
||||
|
||||
if not args.apply:
|
||||
print("Dry-run only -- no changes made. Re-run with --apply to write.")
|
||||
return 0
|
||||
|
||||
print("Applying coding categories...")
|
||||
try:
|
||||
response = client.project.update(custom_categories=CODING_CATEGORIES)
|
||||
except Exception as e:
|
||||
print(f"ERROR applying update: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print("Done.", response if response else "")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user