Compare commits

..
Author SHA1 Message Date
Saket Aryan f026caab69 ci: drop build_embedchain — embedchain is deprecated
Revert the embedchain matrix restoration. The suite has been broken since
the embedchain Makefile switched from hatch to poetry (install_all calls
`poetry install --all-extras` but the job only installs hatch). Since
embedchain is deprecated, remove the CI block entirely; branch protection
will drop the build_embedchain* contexts in a follow-up admin change.
2026-05-21 19:10:35 +05:30
Saket Aryan f5f98e2be7 ci: restore build_embedchain as always-run job with step-level guards
Branch protection requires matrix-suffixed contexts (build_embedchain (3.9)
… (3.12)). Re-add the embedchain job using the same pattern as build_mem0
so all matrix entries always report a status: the job runs unconditionally
and each step is gated on embedchain_changed, exiting cleanly when the
directory is untouched and running the full suite otherwise.
2026-05-21 19:07:07 +05:30
kartik-mem0 a439ad18a9 fix(ci): remove deprecated embedchain CI job
Embedchain is deprecated — remove build_embedchain job, path filter,
and output from ci.yml to stop blocking PRs with a required check
that never reports status.
2026-05-21 15:11:09 +05:30
kartik-mem0 629f0ab06d fix(ci): ensure required checks report status on every PR
Remove paths filters from ci.yml triggers so build_mem0 and
build_embedchain always run. Uses check_changes + step-level
conditions to skip expensive steps when no relevant files changed,
resolving "Waiting for status to be reported" on docs-only PRs.
2026-05-20 23:29:02 +05:30
KartikandGitHub 606ede7c0a feat(mem0-plugin): onboarding, project scoping, identity banner (#5207) 2026-05-20 22:30:07 +05:30
Mragank ShekharandGitHub edd1b3e2f2 feat(cli): add mem0 whoami + mem0 agent-rush subcommands (#5199) 2026-05-20 19:05:12 +05:30
74d043731b docs(llms.txt): lead with signup flow and CLI install (#5159)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 16:25:24 -07:00
Mragank ShekharandGitHub 843ab82905 chore: trigger Mintlify redeploy for #5152 docs (#5185) 2026-05-18 16:22:48 +05:30
50 changed files with 2023 additions and 102 deletions
+15 -58
View File
@@ -3,18 +3,7 @@ name: ci
on:
push:
branches: [main]
paths:
- 'mem0/**'
- 'tests/**'
- 'embedchain/**'
- '.github/workflows/**'
- 'pyproject.toml'
pull_request:
paths:
- 'mem0/**'
- 'tests/**'
- 'embedchain/**'
- 'pyproject.toml'
jobs:
changelog_check:
@@ -60,9 +49,8 @@ jobs:
runs-on: ubuntu-latest
outputs:
mem0_changed: ${{ steps.filter.outputs.mem0 }}
embedchain_changed: ${{ steps.filter.outputs.embedchain }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v2
id: filter
with:
@@ -70,25 +58,28 @@ jobs:
mem0:
- 'mem0/**'
- 'tests/**'
- '.github/workflows/**'
- '.github/workflows/ci.yml'
- 'pyproject.toml'
embedchain:
- 'embedchain/**'
build_mem0:
needs: check_changes
if: needs.check_changes.outputs.mem0_changed == 'true'
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v3
- name: Skip — no relevant changes
if: needs.check_changes.outputs.mem0_changed != 'true'
run: echo "No changes in mem0/, tests/, pyproject.toml, or ci.yml — skipping"
- uses: actions/checkout@v4
if: needs.check_changes.outputs.mem0_changed == 'true'
- name: Set up Python ${{ matrix.python-version }}
if: needs.check_changes.outputs.mem0_changed == 'true'
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Clean up disk space
if: needs.check_changes.outputs.mem0_changed == 'true'
run: |
df -h
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
@@ -96,61 +87,27 @@ jobs:
sudo docker builder prune -a
df -h
- name: Install Hatch
if: needs.check_changes.outputs.mem0_changed == 'true'
run: pip install hatch
- name: Load cached venv
if: needs.check_changes.outputs.mem0_changed == 'true'
id: cached-hatch-dependencies
uses: actions/cache@v3
with:
path: .venv
key: venv-mem0-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }}
- name: Install GEOS Libraries
if: needs.check_changes.outputs.mem0_changed == 'true'
run: sudo apt-get update && sudo apt-get install -y libgeos-dev
- name: Install dependencies
if: needs.check_changes.outputs.mem0_changed == 'true' && steps.cached-hatch-dependencies.outputs.cache-hit != 'true'
run: |
pip install --upgrade pip
pip install -e ".[test,graph,vector_stores,llms,extras]"
pip install ruff
if: steps.cached-hatch-dependencies.outputs.cache-hit != 'true'
- name: Run Linting
if: needs.check_changes.outputs.mem0_changed == 'true'
run: make lint
- name: Run tests and generate coverage report
if: needs.check_changes.outputs.mem0_changed == 'true'
run: make test
build_embedchain:
needs: check_changes
if: needs.check_changes.outputs.embedchain_changed == 'true'
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install Hatch
run: pip install hatch
- name: Load cached venv
id: cached-hatch-dependencies
uses: actions/cache@v3
with:
path: .venv
key: venv-embedchain-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }}
- name: Install dependencies
run: cd embedchain && make install_all
if: steps.cached-hatch-dependencies.outputs.cache-hit != 'true'
- name: Run Formatting
run: |
mkdir -p embedchain/.ruff_cache && chmod -R 777 embedchain/.ruff_cache
cd embedchain && hatch run format
- name: Lint with ruff
run: cd embedchain && make lint
- name: Run tests and generate coverage report
run: cd embedchain && make coverage
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
with:
file: coverage.xml
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+36
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@mem0/cli",
"version": "0.2.6",
"version": "0.2.7",
"description": "The official CLI for mem0 — the memory layer for AI agents",
"type": "module",
"bin": {
+147
View File
@@ -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)}`);
});
}
+18
View File
@@ -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");
}
+15
View File
@@ -40,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 {
@@ -69,6 +76,9 @@ export function createDefaultConfig(): Mem0Config {
telemetry: {
anonymousId: "",
},
agentRush: {
acknowledgedAt: "",
},
};
}
@@ -103,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
@@ -143,6 +155,9 @@ export function saveConfig(config: Mem0Config): void {
telemetry: {
anonymous_id: config.telemetry.anonymousId,
},
agent_rush: {
acknowledged_at: config.agentRush.acknowledgedAt,
},
};
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2));
+42
View File
@@ -262,6 +262,48 @@ program
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
+36
View File
@@ -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/`.
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "mem0-cli"
version = "0.2.6"
version = "0.2.7"
description = "The official CLI for mem0 — the memory layer for AI agents"
readme = "README.md"
license = "Apache-2.0"
+59
View File
@@ -914,6 +914,65 @@ def 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)
@@ -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,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")
+14
View File
@@ -51,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] = {
@@ -105,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:
@@ -158,6 +169,9 @@ def save_config(config: Mem0Config) -> None:
"telemetry": {
"anonymous_id": config.telemetry.anonymous_id,
},
"agent_rush": {
"acknowledged_at": config.agent_rush.acknowledged_at,
},
}
with open(CONFIG_FILE, "w") as f:
+18 -6
View File
@@ -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.
@@ -11,12 +29,6 @@
- Live MCP server: https://mcp.mem0.ai (see `platform/mem0-mcp`).
- Source repo: https://github.com/mem0ai/mem0
### Don't have a Mem0 API key?
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 — no email, no dashboard, no OTP required. 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).
## Install
- Python SDK: `pip install mem0ai`
+86
View File
@@ -271,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:
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mem0",
"version": "0.1.3",
"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 -1
View File
@@ -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 -1
View File
@@ -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",
+20
View File
@@ -2,6 +2,26 @@
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
+12
View File
@@ -24,6 +24,18 @@
]
}
],
"PostToolUse": [
{
"matcher": "mcp__mem0__",
"hooks": [
{
"type": "command",
"command": "${CODEX_PLUGIN_ROOT}/scripts/on_post_tool_use.sh",
"timeout": 3
}
]
}
],
"Stop": [
{
"hooks": [
+12 -5
View File
@@ -2,30 +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": "${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
}
]
+12
View File
@@ -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": [
+13
View File
@@ -15,3 +15,16 @@ def resolve_user_id() -> str:
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
+3
View File
@@ -14,3 +14,6 @@ _mem0_resolve_identity() {
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"
+130
View File
@@ -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
+71
View File
@@ -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
+188
View File
@@ -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)
+33
View File
@@ -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
@@ -26,6 +26,7 @@ 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)
@@ -94,7 +95,7 @@ def find_compact_summary(lines: list[str]) -> str:
return ""
def store_summary(api_key: str, summary: str, user_id: str, session_id: str) -> bool:
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}],
@@ -103,6 +104,8 @@ def store_summary(api_key: str, summary: str, user_id: str, session_id: str) ->
"type": "compact_summary",
"source": "session-start-compact",
"session_id": session_id,
"project_id": project_id,
"branch": branch,
},
"infer": False,
"expiration_date": expires,
@@ -149,6 +152,8 @@ def main():
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:
@@ -161,7 +166,7 @@ def main():
return
log.info("Capturing compact summary (%d chars)", len(summary))
store_summary(api_key, summary, user_id, session_id)
store_summary(api_key, summary, user_id, session_id, project_id, branch)
if __name__ == "__main__":
+28
View File
@@ -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
View File
@@ -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
+7 -2
View File
@@ -24,6 +24,7 @@ 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)
@@ -167,7 +168,7 @@ 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, session_id: 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 = {
@@ -179,6 +180,8 @@ def store_memory(api_key: str, content: str, user_id: str, source: str, session_
"type": "session_state",
"source": source,
"session_id": session_id,
"project_id": project_id,
"branch": branch,
},
"expiration_date": expires,
}
@@ -230,6 +233,8 @@ def main():
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:
@@ -250,7 +255,7 @@ def main():
len(state["bash_commands"]),
)
store_memory(api_key, content, user_id, source, session_id)
store_memory(api_key, content, user_id, source, session_id, project_id, branch)
if __name__ == "__main__":
+1 -1
View File
@@ -55,7 +55,7 @@ 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"},
metadata={"type":"session_state","source":"pre-compaction","project_id":"<the active project_id>","branch":"<the active branch>"},
infer=False,
)
```
+19
View File
@@ -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
+46 -3
View File
@@ -25,18 +25,57 @@ 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 Identity"
echo "## Mem0 Active"
echo ""
echo "Active user_id: \`$MEM0_RESOLVED_USER_ID\`"
echo "\`user=$MEM0_RESOLVED_USER_ID | project=$MEM0_PROJECT_ID | branch=$MEM0_BRANCH | memories=$MEM0_COUNT\`"
echo ""
echo "Always include \`{\"user_id\": \"$MEM0_RESOLVED_USER_ID\"}\` (wrapped in an \`AND\` clause) in every \`search_memories\` filter and as \`user_id\` on every \`add_memory\` call. This keeps the agent's MCP calls aligned with the bucket the hooks write to."
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
@@ -52,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
+19
View File
@@ -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
+18
View File
@@ -25,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:
@@ -37,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
+24 -7
View File
@@ -21,6 +21,8 @@ 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")
@@ -29,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
+49
View File
@@ -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
+4
View File
@@ -13,6 +13,9 @@ 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")
@@ -28,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
+5 -5
View File
@@ -61,11 +61,11 @@ improve your answer. The agent -- not this hook -- owns this decision.
- 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": {"type": "decision"}}]}\` -- design / architecture
- \`{"AND": [{"user_id": "$USER_ID"}, {"metadata": {"type": "anti_pattern"}}]}\` -- debugging, error handling
- \`{"AND": [{"user_id": "$USER_ID"}, {"metadata": {"type": "user_preference"}}]}\` -- tooling, stack, style
- \`{"AND": [{"user_id": "$USER_ID"}, {"metadata": {"type": "convention"}}]}\` -- established patterns
- Or scope with just \`{"AND": [{"user_id": "$USER_ID"}]}\` when no metadata filter fits.
- \`{"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
+19
View File
@@ -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
+106
View File
@@ -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())
+25 -8
View File
@@ -16,6 +16,23 @@ You have access to persistent memory via the mem0 MCP tools. Follow this protoco
Decide whether persistent memory context would improve your response, then act accordingly. Don't search by default — search deliberately.
## Project scoping
Every memory operation MUST be scoped to the current project:
- **On `add_memory`:** Always include `metadata.project_id` (the active project_id from SessionStart).
- **On `search_memories`:** Always include `{"metadata": {"project_id": "<your_project_id>"}}` in the AND filter.
- **Session-state memories:** Also include `metadata.branch` (the active branch from SessionStart).
Full filter template:
```python
filters={"AND": [
{"user_id": "<your_user_id>"},
{"metadata": {"project_id": "<your_project_id>"}},
{"metadata": {"type": "decision"}}
]}
```
### Decide: search or skip?
**Search WHEN** the user:
@@ -59,9 +76,9 @@ Combine `user_id` with one metadata clause per call:
| `{"metadata": {"type": "user_preference"}}` | tooling, stack, style — always include for code work |
| `{"metadata": {"type": "convention"}}` | established patterns in this project |
Full filter (replace `<your_user_id>` with the active user_id from your runtime):
Full filter (replace `<your_user_id>` and `<your_project_id>` with the active values from SessionStart):
```python
filters={"AND": [{"user_id": "<your_user_id>"}, {"metadata": {"type": "decision"}}]}
filters={"AND": [{"user_id": "<your_user_id>"}, {"metadata": {"project_id": "<your_project_id>"}}, {"metadata": {"type": "decision"}}]}
```
### Worked example
@@ -74,16 +91,16 @@ search_memories(query="Refactor the auth module to use JWT")
# Hits whatever shares words. Misses prior decisions and preferences.
```
Do (parallel — substitute the active `user_id` for `<your_user_id>`):
Do (parallel — substitute the active `user_id` and `project_id` for the placeholders):
```python
search_memories(query="auth module decisions",
filters={"AND": [{"user_id": "<your_user_id>"}, {"metadata": {"type": "decision"}}]})
filters={"AND": [{"user_id": "<your_user_id>"}, {"metadata": {"project_id": "<your_project_id>"}}, {"metadata": {"type": "decision"}}]})
search_memories(query="JWT",
filters={"AND": [{"user_id": "<your_user_id>"}]})
filters={"AND": [{"user_id": "<your_user_id>"}, {"metadata": {"project_id": "<your_project_id>"}}]})
search_memories(query="auth refactor failures",
filters={"AND": [{"user_id": "<your_user_id>"}, {"metadata": {"type": "anti_pattern"}}]})
filters={"AND": [{"user_id": "<your_user_id>"}, {"metadata": {"project_id": "<your_project_id>"}}, {"metadata": {"type": "anti_pattern"}}]})
search_memories(query="auth",
filters={"AND": [{"user_id": "<your_user_id>"}, {"metadata": {"type": "user_preference"}}]})
filters={"AND": [{"user_id": "<your_user_id>"}, {"metadata": {"project_id": "<your_project_id>"}}, {"metadata": {"type": "user_preference"}}]})
```
## After completing significant work
@@ -116,7 +133,7 @@ When the user is asking about *current* state ("where were we", "what's the acti
```python
# Last 90 days only
{"AND": [{"user_id": "<id>"}, {"metadata": {"type": "session_state"}}, {"created_at": {"gte": "<90 days ago, YYYY-MM-DD>"}}]}
{"AND": [{"user_id": "<id>"}, {"metadata": {"project_id": "<your_project_id>"}}, {"metadata": {"type": "session_state"}}, {"created_at": {"gte": "<90 days ago, YYYY-MM-DD>"}}]}
```
Skip the recency filter when the user is asking about durable facts ("what conventions does this project use", "have we hit this bug before") — those are timeless and recency would hide them.
+89
View File
@@ -0,0 +1,89 @@
---
name: mem0-onboard
description: >
Post-install onboarding wizard for the mem0 plugin.
Detects CLAUDE.md, AGENTS.md, .cursorrules, .windsurfrules, mem0.md
and offers to import them. Installs coding categories. Shows active identity.
TRIGGER: user runs /mem0:onboard, or mentions "setup mem0", "configure mem0 plugin".
---
# Mem0 Onboarding Wizard
Run this wizard to set up the mem0 plugin for the current project. Complete in ~30 seconds.
## Step 1: Verify API key
Check if `MEM0_API_KEY` is set in the current environment:
```bash
echo "${MEM0_API_KEY:+SET}" || echo "NOT_SET"
```
- If **NOT set**:
1. Ask the user: "No MEM0_API_KEY found. Do you have one, or need to create one?"
2. If they need one, provide two options:
- **Browser**: Go to https://app.mem0.ai/dashboard/api-keys and copy the key
- **CLI**: Run `pip install mem0-cli && mem0 init --agent --json` to mint a key without email
3. Once they have the key, tell them to run: `export MEM0_API_KEY="m0-..."` in their terminal, then restart this Claude Code session (the env var must be set before Claude Code starts).
4. **STOP here.** Do not proceed until the key is confirmed set.
- If **SET**: Proceed to Step 2.
## Step 2: Show identity
Report the active identity to the user:
- Call `search_memories` with `query="project setup"`, `user_id=<active_user_id>`, `filters={"AND": [{"user_id": "<active_user_id>"}, {"metadata": {"project_id": "<active_project_id>"}}]}`, `limit=1` to verify connectivity.
- Print: `Connected. user=<user_id>, project=<project_id>, branch=<branch>`
- If the search fails, troubleshoot the API key.
## Step 3: Detect and import project files
Check for these files in the project root:
1. `CLAUDE.md`
2. `AGENTS.md`
3. `.cursorrules`
4. `.windsurfrules`
5. `mem0.md`
For each file found, ask the user: "Found `<filename>` (<size> bytes). Import into mem0? [Y/n]"
If user says yes (or default):
- Read the file content
- Call `add_memory` with:
- `messages=[{"role": "user", "content": "## Project Profile: <filename>\n\nProject: <project_id>\n\n<file_content>"}]`
- `user_id=<active_user_id>`
- `metadata={"type": "project_profile", "file": "<filename>", "project_id": "<active_project_id>", "source": "onboard"}`
- `infer=False`
## Step 4: Install coding categories
Ask: "Install coding categories optimized for development workflows? [Y/n]"
If yes, run the script directly (no external dependencies required — uses stdlib only).
The script lives at `scripts/setup_coding_categories.py` relative to the plugin root. Use the appropriate plugin root variable for the current platform:
- Claude Code: `${CLAUDE_PLUGIN_ROOT}`
- Codex: `${CODEX_PLUGIN_ROOT}`
- Cursor: `${CURSOR_PLUGIN_ROOT}`
```bash
python3 "<PLUGIN_ROOT>/scripts/setup_coding_categories.py" --apply
```
If the script reports an error, show the error message and suggest checking the API key.
## Step 5: Summary
Print a summary:
```
Onboarding complete.
user_id: <user_id>
project_id: <project_id>
branch: <branch>
imported: <N> files
categories: <installed or skipped>
Memory is now active for this project. Start working — mem0 will
automatically search relevant context and capture learnings.
Run /mem0:tour to see what mem0 already knows about this project.
```
@@ -0,0 +1,50 @@
---
name: mem0-switch-project
description: >
Manually override project_id for the current directory.
Useful for monorepos, nested git dirs, or non-git directories.
TRIGGER: user runs /mem0:switch-project <name>, or asks "switch mem0 project",
"change project scope", "override project_id".
---
# Mem0 Switch Project
Override the automatic project_id detection for the current directory.
## Usage
The user provides a project name as an argument: `/mem0:switch-project <project-name>`
## Execution
1. If no project name was given, ask: "What project_id should this directory use?"
2. Write the mapping to `~/.mem0/project_map.json` using the Bash tool:
```bash
python3 -c "
import json, os
map_file = os.path.expanduser('~/.mem0/project_map.json')
mapping = {}
if os.path.isfile(map_file):
with open(map_file) as f:
mapping = json.load(f)
mapping[os.getcwd()] = '<PROJECT_NAME>'
os.makedirs(os.path.dirname(map_file), exist_ok=True)
with open(map_file, 'w') as f:
json.dump(mapping, f, indent=2)
print(f'Mapped {os.getcwd()} -> <PROJECT_NAME>')
"
```
(Replace `<PROJECT_NAME>` with the user's chosen project name.)
3. Verify by searching for existing memories:
- Call `search_memories` with `query="project"`, `filters={"AND": [{"user_id": "<id>"}, {"metadata": {"project_id": "<PROJECT_NAME>"}}]}`, `limit=1`
4. Print:
```
Switched to project <PROJECT_NAME>.
<N> memories found for this project.
Note: This override persists across sessions for this directory.
```
+46
View File
@@ -0,0 +1,46 @@
---
name: mem0-tour
description: >
Show what mem0 knows about the current project. Dumps top memories
grouped by category. Power-user-friendly proof of value.
TRIGGER: user runs /mem0:tour, or asks "what do you know about this project",
"show me my memories", "what has mem0 stored".
---
# Mem0 Project Tour
Show the user what mem0 has stored for the current project.
## Execution
1. Run the following `search_memories` calls in parallel (all with the active `user_id` and `metadata.project_id`):
- `query="architecture decisions"`, `filters={"AND": [{"user_id": "<id>"}, {"metadata": {"project_id": "<pid>"}}, {"metadata": {"type": "decision"}}]}`, `limit=5`
- `query="anti patterns failures"`, `filters={"AND": [{"user_id": "<id>"}, {"metadata": {"project_id": "<pid>"}}, {"metadata": {"type": "anti_pattern"}}]}`, `limit=5`
- `query="task learnings strategies"`, `filters={"AND": [{"user_id": "<id>"}, {"metadata": {"project_id": "<pid>"}}, {"metadata": {"type": "task_learning"}}]}`, `limit=5`
- `query="coding conventions"`, `filters={"AND": [{"user_id": "<id>"}, {"metadata": {"project_id": "<pid>"}}, {"metadata": {"type": "convention"}}]}`, `limit=5`
- `query="user preferences"`, `filters={"AND": [{"user_id": "<id>"}, {"metadata": {"project_id": "<pid>"}}, {"metadata": {"type": "user_preference"}}]}`, `limit=5`
- `query="project profile"`, `filters={"AND": [{"user_id": "<id>"}, {"metadata": {"project_id": "<pid>"}}, {"metadata": {"type": "project_profile"}}]}`, `limit=5`
- `query="tooling setup environment"`, `filters={"AND": [{"user_id": "<id>"}, {"metadata": {"project_id": "<pid>"}}, {"metadata": {"type": "environmental"}}]}`, `limit=5`
2. Group results by category. For each category with results, print:
```
## <category_name> (<count> memories)
- <memory_content_truncated_to_100_chars> (score: <similarity_score>)
- ...
```
3. For categories with zero results, print: `<category_name>: (empty)`
4. Print totals at the end:
```
---
Total: <N> memories across <M> categories for project <project_id>
```
5. If ALL categories are empty, print:
```
No memories stored yet for project <project_id>.
Run /mem0:onboard to import project files, or start working — mem0 captures learnings automatically.
```
+54
View File
@@ -0,0 +1,54 @@
"""Shared fixtures for mem0-plugin tests."""
from __future__ import annotations
import os
import subprocess
import sys
import pytest
SCRIPTS_DIR = os.path.join(os.path.dirname(__file__), "..", "scripts")
@pytest.fixture(autouse=True)
def _scripts_on_path():
"""Ensure scripts/ is on sys.path so we can import _project, session_stats, etc."""
abs_scripts = os.path.abspath(SCRIPTS_DIR)
if abs_scripts not in sys.path:
sys.path.insert(0, abs_scripts)
yield
if abs_scripts in sys.path:
sys.path.remove(abs_scripts)
@pytest.fixture()
def tmp_git_repo(tmp_path):
"""Create a temp dir with a git repo and HTTPS remote."""
subprocess.run(["git", "init"], cwd=tmp_path, capture_output=True, check=True)
subprocess.run(
["git", "remote", "add", "origin", "https://github.com/mem0ai/mem0.git"],
cwd=tmp_path,
capture_output=True,
check=True,
)
return tmp_path
@pytest.fixture()
def tmp_git_repo_ssh(tmp_path):
"""Create a temp dir with a git repo and SSH remote."""
subprocess.run(["git", "init"], cwd=tmp_path, capture_output=True, check=True)
subprocess.run(
["git", "remote", "add", "origin", "git@github.com:acme/cool-project.git"],
cwd=tmp_path,
capture_output=True,
check=True,
)
return tmp_path
@pytest.fixture()
def tmp_no_git(tmp_path):
"""Temp dir with no git repo."""
return tmp_path
+107
View File
@@ -0,0 +1,107 @@
"""Tests for _project.py — project_id + branch resolver."""
from __future__ import annotations
import json
import os
import subprocess
def test_resolve_project_id_from_https_remote(tmp_git_repo):
from _project import resolve_project_id
pid = resolve_project_id(str(tmp_git_repo))
assert pid == "mem0ai-mem0"
def test_resolve_project_id_from_ssh_remote(tmp_git_repo_ssh):
from _project import resolve_project_id
pid = resolve_project_id(str(tmp_git_repo_ssh))
assert pid == "acme-cool-project"
def test_resolve_project_id_fallback_basename(tmp_no_git):
from _project import resolve_project_id
pid = resolve_project_id(str(tmp_no_git))
assert pid == os.path.basename(str(tmp_no_git))
def test_resolve_project_id_from_env(tmp_no_git, monkeypatch):
from _project import resolve_project_id
monkeypatch.setenv("MEM0_PROJECT_ID", "my-override")
pid = resolve_project_id(str(tmp_no_git))
assert pid == "my-override"
def test_resolve_project_id_from_project_map(tmp_no_git):
from _project import resolve_project_id, save_project_mapping
save_project_mapping(str(tmp_no_git), "custom-project")
pid = resolve_project_id(str(tmp_no_git))
assert pid == "custom-project"
def test_save_project_mapping_creates_file(tmp_no_git):
from _project import save_project_mapping
save_project_mapping(str(tmp_no_git), "test-proj")
map_path = os.path.expanduser("~/.mem0/project_map.json")
assert os.path.isfile(map_path)
with open(map_path) as f:
data = json.load(f)
assert data[str(tmp_no_git)] == "test-proj"
def test_resolve_branch_in_git_repo(tmp_git_repo):
from _project import resolve_branch
subprocess.run(
["git", "checkout", "-b", "feat/test-branch"],
cwd=tmp_git_repo,
capture_output=True,
check=True,
)
branch = resolve_branch(str(tmp_git_repo))
assert branch == "feat/test-branch"
def test_resolve_branch_no_git(tmp_no_git):
from _project import resolve_branch
branch = resolve_branch(str(tmp_no_git))
assert branch == "unknown"
def test_remote_url_to_slug_various_formats():
from _project import _remote_url_to_slug
assert _remote_url_to_slug("https://github.com/mem0ai/mem0.git") == "mem0ai-mem0"
assert _remote_url_to_slug("git@github.com:mem0ai/mem0.git") == "mem0ai-mem0"
assert _remote_url_to_slug("ssh://git@github.com/acme/app.git") == "acme-app"
assert _remote_url_to_slug("https://gitlab.com/org/sub/repo.git") == "sub-repo"
assert _remote_url_to_slug("git@bitbucket.org:team/project.git") == "team-project"
def test_remote_url_to_slug_no_git_suffix():
from _project import _remote_url_to_slug
assert _remote_url_to_slug("https://github.com/foo/bar") == "foo-bar"
def test_resolve_project_id_priority_order(tmp_git_repo, monkeypatch):
"""Env var > project_map > git remote > basename."""
from _project import resolve_project_id, save_project_mapping
# Git remote gives "mem0ai-mem0"
assert resolve_project_id(str(tmp_git_repo)) == "mem0ai-mem0"
# project_map overrides git remote
save_project_mapping(str(tmp_git_repo), "from-map")
assert resolve_project_id(str(tmp_git_repo)) == "from-map"
# Env var overrides everything
monkeypatch.setenv("MEM0_PROJECT_ID", "from-env")
assert resolve_project_id(str(tmp_git_repo)) == "from-env"
+143
View File
@@ -0,0 +1,143 @@
"""Tests for session_stats.py — session-level memory operation tracker."""
from __future__ import annotations
import json
import os
import subprocess
import sys
import pytest
SCRIPTS_DIR = os.path.join(os.path.dirname(__file__), "..", "scripts")
@pytest.fixture(autouse=True)
def _isolate_stats_file(tmp_path, monkeypatch):
"""Point STATS_FILE to a temp location so tests don't interfere."""
stats_file = str(tmp_path / "test_stats.json")
monkeypatch.setattr("session_stats.STATS_FILE", stats_file)
yield stats_file
def test_init_creates_file(_isolate_stats_file):
import session_stats
session_stats.init()
assert os.path.isfile(_isolate_stats_file)
with open(_isolate_stats_file) as f:
data = json.load(f)
assert data["adds"] == 0
assert data["searches"] == 0
assert data["categories"] == []
def test_record_add_increments(_isolate_stats_file):
import session_stats
session_stats.init()
session_stats.record_add("bug_fixes")
session_stats.record_add("bug_fixes")
session_stats.record_add("decisions")
with open(_isolate_stats_file) as f:
data = json.load(f)
assert data["adds"] == 3
assert set(data["categories"]) == {"bug_fixes", "decisions"}
def test_record_search_increments(_isolate_stats_file):
import session_stats
session_stats.init()
session_stats.record_search()
session_stats.record_search()
with open(_isolate_stats_file) as f:
data = json.load(f)
assert data["searches"] == 2
def test_report_returns_summary(_isolate_stats_file):
import session_stats
session_stats.init()
session_stats.record_add("architecture_decisions")
session_stats.record_add("task_learnings")
session_stats.record_search()
session_stats.record_search()
session_stats.record_search()
result = session_stats.report()
assert "wrote 2 memories" in result
assert "retrieved 3" in result
assert "architecture_decisions" in result
assert "task_learnings" in result
def test_report_empty_session(_isolate_stats_file):
import session_stats
session_stats.init()
result = session_stats.report()
assert result == ""
def test_report_cleans_up_file(_isolate_stats_file):
import session_stats
session_stats.init()
session_stats.record_add()
session_stats.report()
assert not os.path.isfile(_isolate_stats_file)
def test_record_add_no_category(_isolate_stats_file):
import session_stats
session_stats.init()
session_stats.record_add("")
session_stats.record_add()
with open(_isolate_stats_file) as f:
data = json.load(f)
assert data["adds"] == 2
assert data["categories"] == []
def test_duplicate_categories_not_added(_isolate_stats_file):
import session_stats
session_stats.init()
session_stats.record_add("bug_fixes")
session_stats.record_add("bug_fixes")
session_stats.record_add("bug_fixes")
with open(_isolate_stats_file) as f:
data = json.load(f)
assert data["adds"] == 3
assert data["categories"] == ["bug_fixes"]
def test_cli_init(tmp_path):
"""Test CLI invocation: session_stats.py init."""
env = {**os.environ, "USER": "test"}
result = subprocess.run(
[sys.executable, os.path.join(SCRIPTS_DIR, "session_stats.py"), "init"],
capture_output=True,
text=True,
env=env,
)
assert result.returncode == 0
def test_cli_report_no_data(tmp_path):
"""Test CLI invocation: report with no prior init prints fallback."""
env = {**os.environ, "USER": f"test_{os.getpid()}"}
result = subprocess.run(
[sys.executable, os.path.join(SCRIPTS_DIR, "session_stats.py"), "report"],
capture_output=True,
text=True,
env=env,
)
assert result.returncode == 0