Compare commits

..

2 Commits

Author SHA1 Message Date
kartik-mem0 9a03a59279 Merge branch 'main' into feat/plugin-self-hosted-support 2026-08-14 17:13:36 +05:30
kartik-mem0 16c2cfe50e feat(mem0-plugin): support self-hosted Mem0 server via MEM0_BASE_URL
Adds an optional MEM0_BASE_URL override (env var, falling back to
settings.json's base_url, defaulting to the hosted Platform unchanged)
so the plugin's REST-backed hooks can talk to a self-hosted server/
deployment instead of api.mem0.ai.

A new scripts/_api.py centralizes the hosted-vs-self-hosted protocol
differences (unversioned paths, X-API-Key auth, agent_id instead of
app_id) that every hook previously hardcoded, and all 9 urllib call
sites now route through it.

MCP tools have no self-hosted backend (server/ implements no MCP), so
they continue to talk to the hosted Platform regardless of
MEM0_BASE_URL; this is documented rather than silently left unclear.
Coding-category customization also has no self-hosted SDK equivalent
and now skips cleanly with a log message instead of failing.

Closes MEM-5647
2026-08-13 18:29:24 +05:30
139 changed files with 2138 additions and 7232 deletions
+1 -1
View File
@@ -12,7 +12,7 @@
"name": "mem0",
"source": "./integrations/mem0-plugin",
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Claude workflows.",
"version": "0.2.15"
"version": "0.2.14"
}
]
}
+1 -1
View File
@@ -12,7 +12,7 @@
"name": "mem0",
"source": "./integrations/mem0-plugin",
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search.",
"version": "0.2.15"
"version": "0.2.14"
}
]
}
+5 -58
View File
@@ -21,17 +21,12 @@ Package workflows keep their own push-to-main and manual triggers. Their `pull_r
| Mem0 Plugin | `mem0-plugin-checks.yml` | Push to main (`integrations/mem0-plugin/`, excluding `.opencode-plugin/`), manual | pytest + hook exec bits + JSON manifest validation on Python 3.10, 3.11, 3.12 |
| OpenCode Plugin | `opencode-plugin-checks.yml` | Push to main (`.opencode-plugin/`), manual | Bun: tsc + build + dist artifact check |
| Pi Agent Plugin | `pi-agent-plugin-checks.yml` | Push to main (`integrations/pi-agent-plugin/`), manual | tsc + vitest + tsup on Node 20, 22 |
| DeepSeek Harness Plugin | `dsh-mem0-checks.yml` | Push to main (`integrations/dsh-mem0/`), manual | tsc + vitest + tsup on Node 20, 22 |
| n8n Node | `n8n-nodes-mem0-checks.yml` | Push to main (`integrations/n8n-nodes-mem0/`), manual | ESLint + tsc build on Node 20 |
| Zapier App | `zapier-mem0-checks.yml` | Push to main (`integrations/zapier-mem0/`), manual | tsc + `zapier validate` + offline unit tests on Node 22 |
| strands-mem0 | `strands-mem0-checks.yml` | Push to main (`integrations/strands-mem0/`), manual | Ruff + mypy + pytest + hatch build on Python 3.10, 3.11, 3.12 |
| docs llms.txt | `docs-llms-txt-check.yml` | Manual | `docs/llms.txt` coverage |
| GitHub Scripts | inline in `ci-gate.yml` | none | `node` over every `.github/scripts/*.test.js` |
Adding a package CI workflow: give it `workflow_call` plus `push` / `workflow_dispatch` as needed but **no `pull_request` trigger**, then register it in `ci-gate.yml` with a path filter under the `changes` job, a call job, and an entry in the gate job's `needs` list.
`GitHub Scripts` is the one row that is a plain job inside `ci-gate.yml` rather than a called workflow, because a reusable workflow wrapping two `node` invocations would be more file than test. It runs on the `github_scripts` filter, which covers `.github/scripts/**` plus every file those tests read: `pr-gate.yml`, `vouch-check-pr.yml`, `issue-labeler.yml`, and `VOUCHED.td`. Add a new `.github/scripts/*.test.js` and it is picked up with no wiring; make a test read a new file and that file belongs in the filter.
## Branch protection on `main`
A repository ruleset named `Main Branch Rule`, id `11813754`. It enforces squash-only merges, linear history, no deletion, no force-push, and one approving review. Two status checks belong in its `required_status_checks` rule:
@@ -60,9 +55,7 @@ Requiring `CI Gate` also means fork PRs from first-time contributors cannot merg
| OpenClaw | `openclaw-cd.yml` | `openclaw-v*` | npm (`@mem0/openclaw-mem0`) |
| OpenCode Plugin | `opencode-plugin-cd.yml` | `opencode-v*` | npm (`@mem0/opencode-plugin`) |
| Pi Agent Plugin | `pi-agent-plugin-cd.yml` | `pi-agent-v*` | npm (`@mem0/pi-agent-plugin`) |
| DeepSeek Harness Plugin | `dsh-mem0-cd.yml` | `dsh-mem0-v*` | npm (`@mem0/dsh-mem0`) |
| n8n Node | `n8n-nodes-mem0-cd.yml` | `n8n-nodes-mem0-v*` | npm (`@mem0/n8n-nodes-mem0`) |
| strands-mem0 | `strands-mem0-cd.yml` | `strands-mem0-v*` | PyPI (`strands-mem0`) |
- Package CD workflows are `workflow_dispatch`-only, with `tag` and `prerelease` inputs. They check out and build the given tag.
- All publishing uses **OIDC trusted publishing**. No tokens, no secrets.
@@ -76,9 +69,9 @@ Requiring `CI Gate` also means fork PRs from first-time contributors cannot merg
| Workflow | File | Purpose |
|----------|------|---------|
| PR Gate | `pr-gate.yml` | Closes PRs that do not link an issue labeled `accepted`, and reopens them when that label arrives. Exempts members, bots, drafts, and docs-only changes. Never checks out PR code. |
| Vouch (check PR) | `vouch-check-pr.yml` | Closes PRs from authors denounced in `VOUCHED.td`. Comments once on PRs from authors merely absent from it, and blocks nothing in that case. |
| Vouch (manage list) | `vouch-manage-by-issue.yml` | Maintainers edit the trust list by commenting `!vouch @user`, `!denounce @user`, or `!unvouch @user` on any issue. Opens a PR against `VOUCHED.td` through a GitHub App token, for a maintainer to merge. |
| PR Gate | `pr-gate.yml` | Closes PRs that do not link an issue labeled `accepted`, with a reopen path. Exempts members, bots, drafts, and docs-only changes. Never checks out PR code. |
| Vouch (check PR) | `vouch-check-pr.yml` | Comments on PRs from authors absent from `VOUCHED.td`. Comment-only mode (`auto-close: false`). |
| Vouch (manage list) | `vouch-manage-by-issue.yml` | Maintainers edit the trust list by commenting `!vouch @user`, `!denounce @user`, or `!unvouch @user` on any issue. Commits back to `VOUCHED.td` through a GitHub App token. |
| Issue Labeler | `issue-labeler.yml` | Labels issues from the `component` field in the issue forms |
| PR Labeler | `pr-labeler.yml` | Path-based labels, plus propagating labels from linked issues |
| Stale Bot | `stale.yml` | Marks stale issues and PRs |
@@ -86,53 +79,7 @@ Requiring `CI Gate` also means fork PRs from first-time contributors cannot merg
`pr-gate.yml` and `vouch-check-pr.yml` use `pull_request_target`, which is required to label and close fork PRs. Neither checks out PR code and neither has a `run:` step, so there is no pwn-request or script-injection surface. Keep it that way: any future `run:` step in these files must never interpolate `github.event.*` text.
Both workflows exempt maintainers twice, and the second guard is the one that holds. `author_association` is rendered for the viewer, and a webhook payload has no privileged viewer: `MEMBER` needs the author's org membership to be **public**, `COLLABORATOR` needs a **direct** repository invite. An org member with private membership whose `maintain` comes through a team matches neither and arrives as `CONTRIBUTOR`, which is how PR #6948 was closed by its own author's gate. So the guard also skips any PR whose head branch lives in this repository (`head.repo.full_name == github.repository`). Pushing a branch here already requires write access and outside contributors always arrive from a fork, so that test means the same thing without depending on who is looking. Keep both: the `author_association` arm still covers members who work from their own fork.
`pr-gate.yml` carries two jobs whose `if:` conditions are deliberately disjoint. `gate` closes, and only ever runs on `opened`, `reopened`, and `ready_for_review`. `reopen` reopens, and only ever runs on `edited` or on `issues: labeled` with the `accepted` label. Nothing can both close and reopen on the same event, which is the property to preserve when editing either guard.
That split exists because the two halves of a gated PR's recovery arrive in either order. A maintainer usually labels the issue `accepted` at triage, before the author has linked it; sometimes the link lands first and the label follows. So `reopen` handles both directions. From `issues: labeled` it walks `closedByPullRequestsReferences` back to the pull requests that link the issue. From `edited` it takes the edited pull request directly. Both paths then apply the same four tests: the author is not denounced in `VOUCHED.td`, the PR is `CLOSED`, it links an issue labeled `accepted`, and it carries the `<!-- pr-gate -->` marker comment. Without the label path, a maintainer's label is inert. Without the `edited` path, an author who links the issue after it was labeled is stuck, since no other event fires.
The denounce test is what keeps the two gates from cancelling each other out. A denounced author whose PR also lacked an accepted issue was closed by both workflows, so it carries the `<!-- pr-gate -->` marker, and labeling the linked issue would otherwise reopen it. Vouch cannot undo that: reopening runs through `GITHUB_TOKEN`, which raises no events, so `vouch-check-pr.yml` never fires a second time. Reading the list here is the only place the check can live. It fails open like vouch does, warning and treating nobody as denounced if the file cannot be read, and it is the one piece of vouch semantics duplicated outside `vouch-check-pr.yml`, because `pr-gate.yml` never checks out the repository and so cannot import a shared parser. `.github/scripts/vouch-decision.test.js` covers the parsing and asserts `pr-gate.yml` still filters the list the same way.
`edited` must never reach the `gate` job. It fires on any title or description change, so when `gate` listened for it the gate re-judged pull requests that had been open for days and closed them the moment their author touched the description, which is what closed #6948. Rescuing on `edited` is safe for the same reason closing on it was not: the job can only move a PR from closed to open.
Reopening runs through `GITHUB_TOKEN`, which by design raises no further workflow events, so `gate` cannot bounce a freshly reopened PR straight back out.
The concurrency group is keyed on `github.event.action` as well as `github.event_name` and the number, and both keys carry weight. Without the event name, a maintainer applying `bug` right after `accepted` cancels the reopen mid-flight, since `cancel-in-progress` is on for `pull_request_target` and both label events would land in the same group. Without the action, `opened` and `edited` share a group on the same pull request, and an author who ticks a template checkbox in the seconds after opening cancels the run that was about to gate them: `gate` skips `edited` and `reopen` skips an open pull request, so the cancelled run is never replaced and the pull request stays ungated forever, since `opened` fires exactly once. Rapid successive edits still cancel each other, which is the dedup that was wanted.
The `edited` arm of `reopen` requires `github.event.pull_request.state == 'closed'`, so ordinary description edits on open pull requests do not start a runner.
Two known gaps, both mild. A PR that the gate closed, that someone reopened, and that a maintainer then closed deliberately still carries the marker, so labeling its issue reopens it again; a maintainer closes it once more. And an author who strips `Closes #<number>` out after passing keeps an open PR, which a reviewer sees anyway.
`GATE_EFFECTIVE_FROM` in `pr-gate.yml` is a `created_at` cutoff. `reopened` and `ready_for_review` still fire on PRs opened long before the gate existed, so without the cutoff part of the open backlog would be closed by a rule that did not exist when those PRs were filed. Set it to the actual merge date in UTC.
The gate's docs-only exemption covers `docs/` plus a named allowlist of four root files: `README.md`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, and `SECURITY.md`. It is an allowlist rather than a rule about top-level markdown because the repository root also holds `AGENTS.md`, `CLAUDE.md`, and `LLM.md`, which are the instructions coding agents read before touching this codebase. Those are functional files that happen to be written in prose, and rewriting them is a change to behaviour, so they stay gated. Markdown nested anywhere else stays gated for the same reason: `skills/**/*.md` and everything under `.github/` are functional too. Adding a genuinely prose root file means adding it to `rootDocs` in `pr-gate.yml`.
`.github/scripts/pr-gate-docs-exemption.test.js` covers that predicate. It pulls the `rootDocs` and `isDocs` lines out of `pr-gate.yml` and evaluates them, so it exercises the shipped rule rather than a copy that could drift from it, and it pins `AGENTS.md`, `CLAUDE.md`, and `LLM.md` on the gated side along with `skills/**/*.md`, nested `.github/` files, and the empty file list. It only accepts those two declarations in a literal one-line form, so keep `rootDocs` a `Set` of quoted names and `isDocs` a single arrow expression.
The two contribution gates answer different questions and neither covers for the other. `pr-gate.yml` judges the change, and the `accepted` label is how a maintainer says yes to it. `vouch-check-pr.yml` judges the author, and `VOUCHED.td` is how a maintainer says no to one. A vouched author with no accepted issue is still closed by the gate; a denounced author with an accepted issue is still closed by vouch. Read either one as a backstop for the other and both get weakened.
Vouch enforces on the denounce axis only, through `require-vouch: false` with `auto-close: true`. That pair is not the obvious reading of either input, so the decision table from v1.5.0 (`vouch/github.nu` at pinned SHA `d66fa29`) is worth stating outright:
| Author | `status` | Effect |
|---|---|---|
| ends in `[bot]` | `skipped` | nothing |
| collaborator with write or admin | `vouched` | nothing |
| listed in `VOUCHED.td` | `vouched` | nothing |
| listed as `-handle` | `closed` | action comments and closes |
| absent from the file | `allowed` | workflow comments, nothing closed |
`require-vouch: true` would close every first-time contributor, which is the opposite of what a trust list is for: the funnel has to stay open or nobody ever earns a vouch. `auto-close: false` is the setting that looked safe and did nothing at all, since in v1.5.0 both the unvouched and the denounced branch return before posting anything, leaving only a line in the run log. That is why `!denounce` was decorative until this pair landed.
Only the `allowed` arm is ours: a `github-script` step posts the soft comment, keyed on a `<!-- vouch-check -->` marker so a reopen does not comment twice. The `closed` arm belongs to the action, message and all. Keeping the two arms disjoint is what stops a denounced author getting two comments, so if that step is ever re-keyed off `allowed`, check the overlap first.
`.github/scripts/vouch-decision.test.js` holds that table as a `decide()` function and asserts the workflow's `require-vouch`, `auto-close`, and comment-step gating still produce it, comment counts included. Be clear about what that does and does not prove. `decide()` is a **hand transcription** of `gh-check-pr`, read from `vouch/github.nu` at the pinned SHA; the test cannot run the action, so it cannot notice the action changing underneath it. Left alone it would agree with itself forever, which makes bumping the pinned SHA the one edit it would otherwise sail through. So it also asserts `vouch-check-pr.yml` still pins `PINNED_VOUCH_SHA`, and a bump fails it on purpose: re-read `gh-check-pr` at the new revision, correct `decide()` and the table above, then move the constant. CI runs it through the `GitHub Scripts` job on any change to the scripts or the files they read.
Failure is open by design. If the action cannot read `VOUCHED.td` it falls back to an empty list, every author reads as absent, and nobody is closed by an API hiccup.
`vouch-manage-by-issue.yml` runs with `merge-immediately: "false"`. The `Main Branch Rule` ruleset requires one approving review and has no bypass actors, so the action's immediate `PUT /pulls/{n}/merge` would return 405 and leave `VOUCHED.td` unchanged on `main`. The bot opens the PR, a maintainer merges it. Setting `pull-request: "false"` is not an alternative: the same ruleset blocks direct pushes.
That workflow also needs `VOUCH_APP_ID` and `VOUCH_APP_PRIVATE_KEY` repository secrets. Without them it fails at the token step before doing anything. `vouch-check-pr.yml` needs neither.
`GATE_EFFECTIVE_FROM` in `pr-gate.yml` is a `created_at` cutoff. `edited`, `reopened`, and `ready_for_review` fire on PRs opened long before the gate existed, so without the cutoff the whole open backlog would be closed by a rule that did not exist when those PRs were filed. Set it to the actual merge date in UTC.
## Issue forms and templates
@@ -152,4 +99,4 @@ Current field ids:
`VOUCHED.td` is one GitHub username per line, `#` for comments. Seeded from every author with at least one merged PR in this repository, then filtered: accounts at or below a 16% merge rate across five or more attempts were dropped, since landing one change out of many is the signature of automated submission rather than contribution.
Vouch's only built-in exemptions are accounts ending in `[bot]` and repo collaborators with `write` or `admin`. **Organization membership alone is not one of them.** So `vouch-check-pr.yml` carries a job-level `if:` that skips the check for `OWNER`, `MEMBER`, and `COLLABORATOR` authors, the same exemption `pr-gate.yml` already applies. Org members are still listed in the file as a fallback, but the workflow guard is what actually holds.
Vouch's only built-in exemptions are accounts ending in `[bot]` and repo collaborators with `write` or `admin`. **Organization membership alone is not one of them.** So `vouch-check-pr.yml` carries a job-level `if:` that skips the check for `OWNER`, `MEMBER`, and `COLLABORATOR` authors, the same exemption `pr-gate.yml` already applies. `author_association` is `MEMBER` for every org member regardless of repository permission, so no member can be flagged even if their `VOUCHED.td` entry is missing, misspelled, or miscased. Org members are still listed in the file as a fallback, but the workflow guard is what actually holds.
+6 -20
View File
@@ -1,29 +1,15 @@
# The list of vouched (or denounced) users for this repository.
#
# A denounced user (prefixed with a minus) is blocked outright: their pull
# requests are closed on sight, whatever they link. Being absent from this file
# blocks nothing. An unvouched author gets one comment saying so and their pull
# request is reviewed like anyone else's, because a first contribution has to
# start somewhere. Vouching is how that comment stops.
#
# This list is about who, and it is the only thing that judges who. Whether a
# change is wanted is a separate question, answered by the accepted label and
# enforced by pr-gate.yml. Neither gate substitutes for the other: a vouched
# author still needs an accepted issue, and a denounced author is turned away
# even holding one.
# Only vouched users can open pull requests here. A denounced user (prefixed
# with a minus) is blocked outright.
#
# Vouch automatically allows two kinds of account without consulting this file:
# accounts ending in [bot], and repo collaborators with write or admin
# permission. Org membership on its own is NOT one of them, so
# vouch-check-pr.yml skips the check for OWNER, MEMBER, and COLLABORATOR
# authors, and for any branch pushed to this repository.
#
# Keep every mem0ai member listed below anyway. The author_association arm of
# that guard is weaker than it looks: MEMBER needs the member's org membership
# to be public and COLLABORATOR needs a direct repo invite, so a member with
# private membership and a team-derived role reads as CONTRIBUTOR. Working from
# a branch here covers them, working from their own fork leaves this file as
# the only thing that does. A missing or miscased entry is a real gap.
# vouch-check-pr.yml skips the check entirely for OWNER, MEMBER, and
# COLLABORATOR authors. mem0ai org members are listed below as well, but that
# workflow guard is what actually protects them: a missing, misspelled, or
# miscased entry here can never cause a member to be flagged.
#
# Syntax:
# - One handle per line (without @), sorted alphabetically.
@@ -1,60 +0,0 @@
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const gate = fs.readFileSync(path.join(__dirname, '..', 'workflows', 'pr-gate.yml'), 'utf8');
const rootDocsLine = gate.match(/^\s*(const rootDocs = new Set\(\['[\w.-]+'(?:, '[\w.-]+')*\]\);)\s*$/m);
const isDocsLine = gate.match(/^\s*(const isDocs = \(\w+\) => [\w.'"()[\]\/, |&!=><+-]+;)\s*$/m);
assert.ok(
rootDocsLine,
'pr-gate.yml no longer declares rootDocs as a single-line Set of quoted filenames. ' +
'This test evaluates that line to exercise the shipped predicate rather than a copy of it, ' +
'and only accepts a literal shape, so widen the pattern deliberately or keep the declaration literal.',
);
assert.ok(
isDocsLine,
'pr-gate.yml no longer declares isDocs as a single-line arrow expression. ' +
'This test evaluates that line to exercise the shipped predicate rather than a copy of it, ' +
'and refuses anything with a statement body, so keep it an expression.',
);
const isDocs = new Function(`${rootDocsLine[1]}\n${isDocsLine[1]}\nreturn isDocs;`)();
const exempt = (files) => files.length > 0 && files.every(isDocs);
const cases = [
[['docs/a.mdx'], true],
[['docs/platform/quickstart.mdx'], true],
[['README.md'], true],
[['CONTRIBUTING.md'], true],
[['CODE_OF_CONDUCT.md'], true],
[['SECURITY.md'], true],
[['README.md', 'CONTRIBUTING.md', 'docs/x.mdx'], true],
[['AGENTS.md'], false],
[['CLAUDE.md'], false],
[['LLM.md'], false],
[['README.md', 'AGENTS.md'], false],
[['README.md', 'mem0/memory/main.py'], false],
[['skills/mem0/SKILL.md'], false],
[['.github/AGENTS.md'], false],
[['.github/workflows/ci.yml'], false],
[['docs-site/index.md'], false],
[[], false],
];
let failures = 0;
for (const [files, expected] of cases) {
const actual = exempt(files);
const label = files.length ? files.join(', ') : '(no files)';
if (actual === expected) {
console.log(`ok ${label} -> ${actual ? 'exempt' : 'gated'}`);
} else {
failures += 1;
console.log(`FAIL ${label} -> ${actual ? 'exempt' : 'gated'}, expected ${expected ? 'exempt' : 'gated'}`);
}
}
console.log(failures === 0 ? '\nPASS' : `\nFAIL (${failures} cases)`);
process.exit(failures === 0 ? 0 : 1);
-122
View File
@@ -1,122 +0,0 @@
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const workflowPath = path.join(__dirname, '..', 'workflows', 'vouch-check-pr.yml');
const workflow = fs.readFileSync(workflowPath, 'utf8');
const PINNED_VOUCH_SHA = 'd66fa29a64600490892131ad87597c30c91fcac4';
assert.ok(
workflow.includes(`mitchellh/vouch/action/check-pr@${PINNED_VOUCH_SHA}`),
`decide() below is a hand transcription of gh-check-pr from vouch/github.nu at ${PINNED_VOUCH_SHA} (v1.5.0). ` +
'It reads the action, it does not run it, so on its own it agrees with itself whatever the action does. ' +
'vouch-check-pr.yml now pins a different revision: re-read gh-check-pr there, update decide() and the ' +
'decision table in .github/AGENTS.md to match it, then set PINNED_VOUCH_SHA to the new SHA.',
);
const actionDefaults = { 'require-vouch': true, 'auto-close': false };
const booleanInput = (name) => {
const match = workflow.match(new RegExp(`^\\s+${name}:\\s*"?(true|false)"?\\s*$`, 'm'));
return match ? match[1] === 'true' : actionDefaults[name];
};
const requireVouch = booleanInput('require-vouch');
const autoClose = booleanInput('auto-close');
const commentedStatus = (() => {
const match = workflow.match(/steps\.vouch\.outputs\.status == '(\w+)'/);
assert.ok(match, 'the follow-up comment step is not keyed on a vouch status');
return match[1];
})();
const decide = (author) => {
if (author === 'bot') return { status: 'skipped', closed: false, actionComments: false };
if (author === 'collaborator' || author === 'vouched') {
return { status: 'vouched', closed: false, actionComments: false };
}
if (author === 'denounced') {
if (!autoClose) return { status: 'closed', closed: false, actionComments: false };
return { status: 'closed', closed: true, actionComments: true };
}
if (!requireVouch) return { status: 'allowed', closed: false, actionComments: false };
if (!autoClose) return { status: 'closed', closed: false, actionComments: false };
return { status: 'closed', closed: true, actionComments: true };
};
const outcome = (author) => {
const result = decide(author);
return { ...result, workflowComments: result.status === commentedStatus };
};
const cases = [
{ author: 'bot', closed: false, comments: 0 },
{ author: 'collaborator', closed: false, comments: 0 },
{ author: 'vouched', closed: false, comments: 0 },
{ author: 'unvouched', closed: false, comments: 1 },
{ author: 'denounced', closed: true, comments: 1 },
];
let failures = 0;
for (const expected of cases) {
const actual = outcome(expected.author);
const comments = Number(actual.actionComments) + Number(actual.workflowComments);
try {
assert.strictEqual(actual.closed, expected.closed, `${expected.author}: closed`);
assert.strictEqual(comments, expected.comments, `${expected.author}: comment count`);
console.log(`ok ${expected.author} -> ${actual.status}, closed=${actual.closed}, comments=${comments}`);
} catch (error) {
failures += 1;
console.log(`FAIL ${expected.author} -> ${actual.status}, closed=${actual.closed}, comments=${comments}`);
console.log(` ${error.message}: expected ${JSON.stringify(expected)}`);
}
}
console.log(`\nvouch@${PINNED_VOUCH_SHA.slice(0, 7)} require-vouch=${requireVouch} auto-close=${autoClose} comment-on=${commentedStatus}`);
const parseDenounced = (contents) => new Set(contents
.split('\n')
.map((line) => line.trim())
.filter((line) => line.startsWith('-'))
.map((line) => line.slice(1).split(/\s+/)[0].split(':').pop().toLowerCase())
.filter(Boolean));
const gate = fs.readFileSync(path.join(__dirname, '..', 'workflows', 'pr-gate.yml'), 'utf8');
assert.ok(
gate.includes(".filter((line) => line.startsWith('-'))"),
'pr-gate.yml no longer parses the denounce list the way this test does',
);
const vouched = fs.readFileSync(path.join(__dirname, '..', 'VOUCHED.td'), 'utf8');
const denouncedNow = parseDenounced(vouched);
const sample = parseDenounced([
'# -notacomment is a comment line',
'-SpamBot seeded 2026-08-12',
'-github:OtherSpammer',
'realcontributor',
'',
].join('\n'));
let parseFailures = 0;
for (const [label, actual, expected] of [
['denounce entry, with note', sample.has('spambot'), true],
['denounce entry, platform prefixed', sample.has('otherspammer'), true],
['comment line is not an entry', sample.has('notacomment'), false],
['vouched entry is not denounced', sample.has('realcontributor'), false],
['live file parses without throwing', denouncedNow instanceof Set, true],
]) {
try {
assert.strictEqual(actual, expected, label);
console.log(`ok ${label}`);
} catch (error) {
parseFailures += 1;
console.log(`FAIL ${label}: ${error.message}`);
}
}
console.log(`denounced in VOUCHED.td: ${denouncedNow.size}`);
const total = failures + parseFailures;
console.log(total === 0 ? 'PASS' : `FAIL (${total} assertions)`);
process.exit(total === 0 ? 0 : 1);
-53
View File
@@ -41,12 +41,9 @@ jobs:
mem0_plugin: ${{ steps.filter.outputs.mem0_plugin }}
opencode_plugin: ${{ steps.filter.outputs.opencode_plugin }}
pi_agent_plugin: ${{ steps.filter.outputs.pi_agent_plugin }}
dsh_mem0: ${{ steps.filter.outputs.dsh_mem0 }}
n8n_nodes_mem0: ${{ steps.filter.outputs.n8n_nodes_mem0 }}
zapier_mem0: ${{ steps.filter.outputs.zapier_mem0 }}
strands_mem0: ${{ steps.filter.outputs.strands_mem0 }}
docs_llms_txt: ${{ steps.filter.outputs.docs_llms_txt }}
github_scripts: ${{ steps.filter.outputs.github_scripts }}
steps:
- uses: dorny/paths-filter@v3
id: filter
@@ -90,10 +87,6 @@ jobs:
- 'integrations/pi-agent-plugin/**'
- '.github/workflows/pi-agent-plugin-checks.yml'
- '.github/workflows/ci-gate.yml'
dsh_mem0:
- 'integrations/dsh-mem0/**'
- '.github/workflows/dsh-mem0-checks.yml'
- '.github/workflows/ci-gate.yml'
n8n_nodes_mem0:
- 'integrations/n8n-nodes-mem0/**'
- '.github/workflows/n8n-nodes-mem0-checks.yml'
@@ -101,10 +94,6 @@ jobs:
- 'integrations/zapier-mem0/**'
- '.github/workflows/zapier-mem0-checks.yml'
- '.github/workflows/ci-gate.yml'
strands_mem0:
- 'integrations/strands-mem0/**'
- '.github/workflows/strands-mem0-checks.yml'
- '.github/workflows/ci-gate.yml'
docs_llms_txt:
- 'docs/**/*.mdx'
- 'docs/llms.txt'
@@ -112,13 +101,6 @@ jobs:
- 'scripts/llms-txt-ignore.txt'
- '.github/workflows/docs-llms-txt-check.yml'
- '.github/workflows/ci-gate.yml'
github_scripts:
- '.github/scripts/**'
- '.github/VOUCHED.td'
- '.github/workflows/pr-gate.yml'
- '.github/workflows/vouch-check-pr.yml'
- '.github/workflows/issue-labeler.yml'
- '.github/workflows/ci-gate.yml'
python-sdk:
name: Python SDK
@@ -176,13 +158,6 @@ jobs:
uses: ./.github/workflows/pi-agent-plugin-checks.yml
secrets: inherit
dsh-mem0:
name: DeepSeek Harness Plugin
needs: changes
if: needs.changes.outputs.dsh_mem0 == 'true'
uses: ./.github/workflows/dsh-mem0-checks.yml
secrets: inherit
n8n-nodes-mem0:
name: n8n Node
needs: changes
@@ -195,13 +170,6 @@ jobs:
uses: ./.github/workflows/zapier-mem0-checks.yml
secrets: inherit
strands-mem0:
name: strands-mem0
needs: changes
if: needs.changes.outputs.strands_mem0 == 'true'
uses: ./.github/workflows/strands-mem0-checks.yml
secrets: inherit
docs-llms-txt:
name: docs llms.txt
needs: changes
@@ -209,24 +177,6 @@ jobs:
uses: ./.github/workflows/docs-llms-txt-check.yml
secrets: inherit
github-scripts:
name: GitHub Scripts
needs: changes
if: needs.changes.outputs.github_scripts == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Run .github/scripts tests
run: |
for test in .github/scripts/*.test.js; do
echo "::group::$test"
node "$test"
echo "::endgroup::"
done
gate:
name: CI Gate
needs:
@@ -239,12 +189,9 @@ jobs:
- mem0-plugin
- opencode-plugin
- pi-agent-plugin
- dsh-mem0
- n8n-nodes-mem0
- zapier-mem0
- strands-mem0
- docs-llms-txt
- github-scripts
if: always()
runs-on: ubuntu-latest
steps:
-60
View File
@@ -1,60 +0,0 @@
name: Publish @mem0/dsh-mem0 📦 to npm
# Dispatched by release.yml (Release Router) when a release tagged
# dsh-mem0-v* is published. Can also be dispatched manually to re-publish
# a tag.
on:
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build and publish (e.g. dsh-mem0-v0.1.1)'
required: true
type: string
prerelease:
description: 'Publish under the version preid dist-tag instead of latest'
required: false
type: boolean
default: false
jobs:
build-n-publish:
name: Build and publish @mem0/dsh-mem0 📦 to npm
if: startsWith(inputs.tag, 'dsh-mem0-v')
runs-on: ubuntu-latest
permissions:
id-token: write
defaults:
run:
working-directory: integrations/dsh-mem0
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
cache-dependency-path: integrations/dsh-mem0/pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
- name: Publish to npm
run: |
if [ "${{ inputs.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npx npm@latest publish --provenance --access public --tag "$PREID"
else
npx npm@latest publish --provenance --access public
fi
-90
View File
@@ -1,90 +0,0 @@
name: dsh-mem0 checks
# On PRs this is invoked by ci-gate.yml (the single required check);
# push-to-main and manual runs remain standalone.
on:
workflow_dispatch:
push:
branches: [main]
paths:
- 'integrations/dsh-mem0/**'
- '.github/workflows/dsh-mem0-checks.yml'
workflow_call:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: integrations/dsh-mem0/pnpm-lock.yaml
- name: Install dependencies
run: cd integrations/dsh-mem0 && pnpm install --frozen-lockfile
- name: Type check
run: cd integrations/dsh-mem0 && pnpm exec tsc --noEmit
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22]
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
cache-dependency-path: integrations/dsh-mem0/pnpm-lock.yaml
- name: Install dependencies
run: cd integrations/dsh-mem0 && pnpm install --frozen-lockfile
- name: Run tests
run: cd integrations/dsh-mem0 && pnpm exec vitest run
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: integrations/dsh-mem0/pnpm-lock.yaml
- name: Install dependencies
run: cd integrations/dsh-mem0 && pnpm install --frozen-lockfile
- name: Build
run: cd integrations/dsh-mem0 && pnpm build
- name: Verify dist output exists
run: |
test -f integrations/dsh-mem0/dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
test -f integrations/dsh-mem0/dist/index.d.ts || (echo "Build output missing: dist/index.d.ts" && exit 1)
+7 -118
View File
@@ -2,13 +2,11 @@ name: PR Gate
on:
pull_request_target:
types: [opened, reopened, ready_for_review, edited]
issues:
types: [labeled]
types: [opened, reopened, edited, ready_for_review]
concurrency:
group: pr-gate-${{ github.event_name }}-${{ github.event.action }}-${{ github.event.pull_request.number || github.event.issue.number }}
cancel-in-progress: ${{ github.event_name == 'pull_request_target' }}
group: pr-gate-${{ github.event.pull_request.number }}
cancel-in-progress: true
env:
GATE_EFFECTIVE_FROM: '2026-08-12T00:00:00Z'
@@ -21,11 +19,8 @@ permissions:
jobs:
gate:
if: >-
github.event_name == 'pull_request_target' &&
github.event.action != 'edited' &&
github.event.pull_request.draft == false &&
github.event.pull_request.user.type != 'Bot' &&
github.event.pull_request.head.repo.full_name != github.repository &&
!contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association)
runs-on: ubuntu-latest
steps:
@@ -52,9 +47,7 @@ jobs:
const files = await github.paginate(github.rest.pulls.listFiles, {
owner, repo, pull_number: pr.number, per_page: 100,
});
const rootDocs = new Set(['README.md', 'CONTRIBUTING.md', 'CODE_OF_CONDUCT.md', 'SECURITY.md']);
const isDocs = (filename) => filename.startsWith('docs/') || rootDocs.has(filename);
if (files.length > 0 && files.every((file) => isDocs(file.filename))) {
if (files.length > 0 && files.every((file) => file.filename.startsWith('docs/'))) {
core.info('Docs-only PR, gate skipped');
return;
}
@@ -82,7 +75,6 @@ jobs:
}
const body = [
'<!-- pr-gate -->',
'Thanks for taking the time to open this.',
'',
'We only review pull requests that fix an issue we have already agreed to take on, so this one is closed for now.',
@@ -92,9 +84,10 @@ jobs:
'',
'1. Make sure an issue describes the problem, with the version you are on, a runnable reproduction, and the real output or traceback you saw.',
'2. Link it from this pull request description with `Closes #<number>`.',
'3. Ask a maintainer to label that issue `accepted`. This pull request reopens by itself when they do.',
'3. Ask a maintainer to label that issue `accepted`.',
'4. Reopen this pull request. The check runs again and it stays open.',
'',
'Issue already labeled `accepted`? Just add `Closes #<number>` to the description. That reopens this too.',
'Already linked an accepted issue? Edit the description to include `Closes #<number>` and reopen. The check reruns automatically.',
'',
'Documentation-only changes skip this gate entirely.',
'',
@@ -108,107 +101,3 @@ jobs:
owner, repo, pull_number: pr.number, state: 'closed',
});
core.info(`Closed #${pr.number}: no accepted issue linked`);
reopen:
if: >-
(github.event_name == 'issues' && github.event.label.name == 'accepted') ||
(github.event.action == 'edited' && github.event.pull_request.state == 'closed')
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
const { owner, repo } = context.repo;
const marker = '<!-- pr-gate -->';
const denounced = await (async () => {
try {
const { data } = await github.rest.repos.getContent({
owner, repo, path: '.github/VOUCHED.td',
ref: context.payload.repository.default_branch,
});
return new Set(Buffer.from(data.content, 'base64').toString('utf8')
.split('\n')
.map((line) => line.trim())
.filter((line) => line.startsWith('-'))
.map((line) => line.slice(1).split(/\s+/)[0].split(':').pop().toLowerCase())
.filter(Boolean));
} catch (error) {
core.warning(`Could not read VOUCHED.td, treating nobody as denounced: ${error.message}`);
return new Set();
}
})();
const isReopenable = async (number) => {
const { repository } = await github.graphql(
`query ($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
state
author { login }
closingIssuesReferences(first: 20) {
nodes { labels(first: 50) { nodes { name } } }
}
}
}
}`,
{ owner, repo, number },
);
const pullRequest = repository.pullRequest;
if (denounced.has(pullRequest.author?.login?.toLowerCase())) {
core.info(`#${number} is from a denounced author. Vouch outranks this gate.`);
return false;
}
return pullRequest.state === 'CLOSED' &&
pullRequest.closingIssuesReferences.nodes.some((issue) =>
issue.labels.nodes.some((label) => label.name === 'accepted'));
};
let candidates;
if (context.eventName === 'issues') {
const { repository } = await github.graphql(
`query ($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
issue(number: $number) {
closedByPullRequestsReferences(first: 20, includeClosedPrs: true) {
nodes { number }
}
}
}
}`,
{ owner, repo, number: context.payload.issue.number },
);
candidates = repository.issue.closedByPullRequestsReferences.nodes.map((pr) => pr.number);
} else {
candidates = [context.payload.pull_request.number];
}
for (const number of candidates) {
if (!(await isReopenable(number))) {
core.info(`#${number} is not a closed pull request linking an accepted issue. Skipped.`);
continue;
}
const comments = await github.paginate(github.rest.issues.listComments, {
owner, repo, issue_number: number, per_page: 100,
});
if (!comments.some((comment) => comment.body?.startsWith(marker))) {
core.info(`#${number} was not closed by this gate. Left alone.`);
continue;
}
try {
await github.rest.pulls.update({
owner, repo, pull_number: number, state: 'open',
});
} catch (error) {
core.warning(`Could not reopen #${number}: ${error.message}`);
continue;
}
await github.rest.issues.createComment({
owner, repo, issue_number: number,
body: 'An `accepted` issue is linked now, so this is open again and ready for review.',
});
core.info(`Reopened #${number}`);
}
-2
View File
@@ -45,9 +45,7 @@ jobs:
openclaw-v*) workflow="openclaw-cd.yml" ;;
opencode-v*) workflow="opencode-plugin-cd.yml" ;;
pi-agent-v*) workflow="pi-agent-plugin-cd.yml" ;;
dsh-mem0-v*) workflow="dsh-mem0-cd.yml" ;;
n8n-nodes-mem0-v*) workflow="n8n-nodes-mem0-cd.yml" ;;
strands-mem0-v*) workflow="strands-mem0-cd.yml" ;;
v*) workflow="cd.yml" ;;
*)
echo "::error::Release tag '$TAG' does not match any known package prefix — nothing will be published. See the tag prefix table in AGENTS.md."
-50
View File
@@ -1,50 +0,0 @@
name: Publish strands-mem0 🐍 distribution 📦 to PyPI
# Dispatched by release.yml (Release Router) when a release tagged
# strands-mem0-v* is published. Can also be dispatched manually to re-publish
# a tag. Publishing uses PyPI Trusted Publishing (OIDC), so no API token is
# stored; the `strands-mem0` PyPI project must have a trusted publisher
# configured for mem0ai/mem0 + this workflow.
on:
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build and publish (e.g. strands-mem0-v0.1.0)'
required: true
type: string
prerelease:
description: 'Unused for PyPI (pre-releases are expressed in the version itself); accepted for router uniformity'
required: false
type: boolean
default: false
jobs:
build-n-publish:
name: Build and publish strands-mem0 📦 to PyPI
if: startsWith(inputs.tag, 'strands-mem0-v')
runs-on: ubuntu-latest
permissions:
id-token: write
defaults:
run:
working-directory: integrations/strands-mem0/python
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install Hatch
run: pip install hatch
- name: Build a binary wheel and a source tarball
run: hatch build --clean
- name: Publish distribution 📦 to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: integrations/strands-mem0/python/dist/
-82
View File
@@ -1,82 +0,0 @@
name: strands-mem0 CI
# On PRs this is invoked by ci-gate.yml (the single required check);
# push-to-main and manual runs remain standalone.
on:
workflow_dispatch:
push:
branches: [main]
paths:
- 'integrations/strands-mem0/**'
- '.github/workflows/strands-mem0-checks.yml'
workflow_call:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dev dependencies
working-directory: integrations/strands-mem0/python
run: pip install -e ".[dev]"
- name: Lint with ruff
working-directory: integrations/strands-mem0/python
run: ruff check .
- name: Check formatting
working-directory: integrations/strands-mem0/python
run: ruff format --check .
- name: Type-check with mypy
working-directory: integrations/strands-mem0/python
run: mypy src
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dev dependencies
working-directory: integrations/strands-mem0/python
run: pip install -e ".[dev]"
- name: Run tests
working-directory: integrations/strands-mem0/python
run: pytest
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install Hatch
run: pip install hatch
- name: Build
working-directory: integrations/strands-mem0/python
run: hatch build --clean
- name: Verify dist output
run: |
ls integrations/strands-mem0/python/dist/*.whl || (echo "Wheel file missing" && exit 1)
ls integrations/strands-mem0/python/dist/*.tar.gz || (echo "Source dist missing" && exit 1)
+1 -33
View File
@@ -16,44 +16,12 @@ jobs:
check:
if: >-
github.event.pull_request.user.type != 'Bot' &&
github.event.pull_request.head.repo.full_name != github.repository &&
!contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association)
runs-on: ubuntu-latest
steps:
- uses: mitchellh/vouch/action/check-pr@d66fa29a64600490892131ad87597c30c91fcac4 # v1.5.0
id: vouch
with:
pr-number: ${{ github.event.pull_request.number }}
require-vouch: false
auto-close: true
auto-close: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: steps.vouch.outputs.status == 'allowed'
uses: actions/github-script@v7
with:
script: |
const { owner, repo } = context.repo;
const pr = context.payload.pull_request;
const marker = '<!-- vouch-check -->';
const comments = await github.paginate(github.rest.issues.listComments, {
owner, repo, issue_number: pr.number, per_page: 100,
});
if (comments.some((comment) => comment.body?.startsWith(marker))) {
core.info('Vouch comment already posted, skipped.');
return;
}
const body = [
marker,
`Hi @${context.payload.pull_request.user.login}, thanks for opening this pull request.`,
'',
"This is just a soft check: you are not yet in this repo's vouched contributor list (`.github/VOUCHED.td`). Nothing is blocked and there is nothing you need to do.",
'',
`A maintainer can vouch for you by commenting \`!vouch @${context.payload.pull_request.user.login}\` on any issue.`,
].join('\n');
await github.rest.issues.createComment({
owner, repo, issue_number: pr.number, body,
});
+1 -1
View File
@@ -37,6 +37,6 @@ jobs:
denounce-keyword: "!denounce"
unvouch-keyword: "!unvouch"
pull-request: "true"
merge-immediately: "false"
merge-immediately: "true"
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
-4
View File
@@ -191,7 +191,3 @@ qdrant_storage/
testing.ipynb
.weave/
# TypeScript incremental build info and local, uncommitted e2e scripts (used by the integrations, e.g. integrations/dsh-mem0)
*.tsbuildinfo
*.local.mjs
+1 -21
View File
@@ -10,7 +10,6 @@ This is a polyglot monorepo and **every package sets its own rules**. Read the `
## Do NOT
- Open a pull request without a signed CLA. It will not be reviewed. See [The CLA is not optional](#the-cla-is-not-optional).
- Open a pull request that does not link an issue carrying the `accepted` label. A bot closes it within a minute. See [Two gates decide whether your pull request stays open](#two-gates-decide-whether-your-pull-request-stays-open).
- Modify anything in `.github/workflows/` without explicit maintainer approval. Publishing credentials are pinned to workflow filenames.
- Commit `.env` files, API keys, or credentials.
- Skip pre-commit hooks.
@@ -122,24 +121,6 @@ Full guide: [`CONTRIBUTING.md`](CONTRIBUTING.md). Conduct: [`CODE_OF_CONDUCT.md`
6. Open the PR against `main` and fill in [the template](.github/PULL_REQUEST_TEMPLATE.md). Do not paraphrase it; GitHub prefills it.
7. **Sign the CLA.**
### Two gates decide whether your pull request stays open
Two workflows run on every pull request from a fork. They judge different things and neither covers for the other, so a pull request has to get past both.
**The [PR Gate](.github/workflows/pr-gate.yml) judges the change.** It closes any pull request that does not link an issue carrying the `accepted` label. Closed is a queue decision, not a verdict: when a maintainer applies the label the pull request reopens by itself. Drafts, documentation-only changes, and branches pushed to this repository rather than a fork are all exempt.
**The [vouch check](.github/workflows/vouch-check-pr.yml) judges the account.** It reads [`.github/VOUCHED.td`](.github/VOUCHED.td), which has three possible answers about any given person:
| The list says | Meaning | Effect on the pull request |
|---|---|---|
| `-handle` | a maintainer ran `!denounce` after the code of conduct process | closed, even with an accepted issue |
| nothing at all | everybody who has not contributed here before | **none.** One comment saying nothing is blocked. |
| `handle` | a maintainer ran `!vouch` | none, and the comment stops appearing |
Being vouched grants nothing. It is a "we have seen this person before" flag that mutes the newcomer comment, not permission to skip the accepted-issue rule. Being absent from the list costs nothing.
If you are an agent opening a pull request on someone's behalf, the practical consequence is one rule: **get the linked issue labelled `accepted` before you open the pull request, or expect the pull request to be closed and to reopen later.** Do not work around either gate, do not reopen a gated pull request by hand, and do not re-file the same change under a new pull request when one is closed.
### The CLA is not optional
**A pull request from a contributor who has not signed the Contributor License Agreement is not accepted, not reviewed, and not merged.** This is not a formality applied at merge time. An unsigned pull request does not enter the review queue at all: maintainers do not read the diff, do not leave feedback, and do not discuss the approach. It sits until the CLA is signed, and it is closed if it goes stale.
@@ -170,6 +151,5 @@ Beyond the CLA and the accepted-issue gate, the [Contribution Conduct](CODE_OF_C
| Documentation contributions | `docs/contributing/documentation.mdx` |
| PR template | `.github/PULL_REQUEST_TEMPLATE.md` |
| Issue forms | `.github/ISSUE_TEMPLATE/` |
| Contribution gates | [Two gates decide whether your pull request stays open](#two-gates-decide-whether-your-pull-request-stays-open) |
| Trust list (vouch) | [`.github/VOUCHED.td`](.github/VOUCHED.td) |
| Trust list (vouch) | `.github/VOUCHED.td` |
| CI/CD, gates, rulesets | [`.github/AGENTS.md`](.github/AGENTS.md) |
+3 -31
View File
@@ -40,24 +40,9 @@ agree the change is one we want.
Pull requests that don't link an accepted issue are closed automatically by the
[PR Gate](./.github/workflows/pr-gate.yml). **Closed does not mean rejected.** It
means the change isn't in the queue yet. Once a maintainer labels the issue the
pull request reopens itself, and you don't have to do anything. Documentation-only
changes skip the gate entirely.
A second check looks at who opened the pull request rather than what it changes.
If you are not yet in this repo's contributor list
([`.github/VOUCHED.td`](./.github/VOUCHED.td)) you get one comment saying so.
**Nothing is blocked and there is nothing you need to do.** A maintainer can add
you by commenting `!vouch @you` on any issue, which only stops that comment from
appearing again. Being on the list is not permission to skip the accepted-issue
rule, and being absent from it costs you nothing.
The list has a negative side too. A maintainer can `!denounce` an account that
has been through the
[code of conduct](./CODE_OF_CONDUCT.md#contribution-conduct) enforcement process,
and pull requests from that account are closed whether or not they link an
accepted issue. This is rare, it is never where anyone starts, and it is
reversible.
means the change isn't in the queue yet. Once a maintainer labels the issue,
reopen the pull request and it stays open. Documentation-only changes skip the
gate entirely.
Security fixes are the one exception, and they don't go through public pull
requests at all. Follow the [Security Policy](./SECURITY.md) instead, which uses
@@ -100,19 +85,6 @@ sign. Signing takes less than a minute and only needs to be done once. Pull
requests from contributors who have not signed the CLA will be blocked from
merging.
## First Contribution Fast Path
Fixing a typo or a small docs issue? You don't need the full workflow below.
1. **Pick something small.** Look for issues labeled `documentation` or `good first issue`, or a typo/broken link you noticed while reading the docs.
2. **Branch from `main`** with a name that says what you're fixing, e.g. `docs/fix-quickstart-typo` or `fix/broken-crewai-link`.
3. **Make the change, then run only what applies:**
- Docs-only change (`docs/**`): preview with `make docs`. If you added or removed an `.mdx` page, run `python scripts/check-llms-txt-coverage.py --write` so `docs/llms.txt` stays in sync.
- Code change: run the linter and tests for the package you touched, see [Development Workflow](#development-workflow) below.
4. **Open a PR** against `main` with `Closes #<issue-number>` and a one-line description of what you fixed.
For anything larger than a docs fix or a small bug, follow the full workflow below.
## Repository Layout
The two most common contribution targets are the SDKs:
+1 -1
View File
@@ -11,7 +11,7 @@ install:
hatch env create
install_all:
pip install ruff==0.16.0 groq together boto3 'litellm>=1.83.7,<1.98.0' ollama chromadb weaviate weaviate-client sentence_transformers vertexai \
pip install ruff==0.16.0 groq together boto3 litellm ollama chromadb weaviate weaviate-client sentence_transformers vertexai \
google-generativeai elasticsearch opensearch-py vecs "pinecone<7.0.0" pinecone-text faiss-cpu langchain-community \
upstash-vector azure-search-documents langchain-memgraph langchain-neo4j langchain-aws rank-bm25 pymochow pymongo psycopg kuzu databricks-sdk valkey
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@mem0/cli",
"version": "0.2.13",
"version": "0.2.12",
"description": "The official CLI for mem0 — the memory layer for AI agents",
"type": "module",
"bin": {
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "mem0-cli"
version = "0.2.12"
version = "0.2.11"
description = "The official CLI for mem0 — the memory layer for AI agents"
readme = "README.md"
license = "Apache-2.0"
+1 -1
View File
@@ -1,3 +1,3 @@
"""mem0 CLI — the command-line interface for the mem0 memory layer."""
__version__ = "0.2.12"
__version__ = "0.2.11"
-49
View File
@@ -4,55 +4,6 @@ description: "Major product launches, headline features, and milestones for Mem0
mode: "wide"
---
<Update label="2026-08-24" description="DeepSeek Harness plugin">
**DeepSeek Harness: Mem0 as a Native Cordis Plugin**
The DeepSeek Harness agent forgets everything between sessions. [`@mem0/dsh-mem0`](https://www.npmjs.com/package/@mem0/dsh-mem0) gives it two Mem0-backed tools, so recall and writes persist across runs against the same memory bank you already use from Claude Code, Codex, and every other connected agent.
- **Two agent-callable tools:** `search_memory` recalls facts relevant to a query, `add_memory` stores a fact for future sessions. Both accept per-call `userId` / `agentId` / `runId` scope overrides.
- **Native Cordis lifecycle:** The plugin declares `inject = ['tools']` so it waits for the harness tool registry, then registers through `ctx.tools.register()`. Unmounting the plugin removes the tools automatically.
- **Managed backend, not a memory file:** Server-side extraction, semantic dedup, and conflict resolution, rather than a Markdown file the agent has to maintain itself.
- **Config:** `userId` is required, `apiKey` defaults to `$MEM0_API_KEY`, and `host` optionally targets a dedicated Mem0 Platform base URL.
See [DeepSeek Harness](/integrations/dsh-mem0) for setup and [SDK & Tools](/changelog/sdk) for PR links.
<Note>
Developer preview. Auto-capture and auto-recall, where memory reaches the context with no explicit tool call, are planned but not yet built.
</Note>
</Update>
<Update label="2026-08-24" description="Strands Agents integration">
**Strands Agents: Mem0 as a Native MemoryStore**
[`strands-mem0`](https://pypi.org/project/strands-mem0/) plugs Mem0 into AWS's [Strands Agents](https://strandsagents.com/) SDK as a native `MemoryStore`, so recall and writes happen inside the agent loop rather than as tool calls the model has to remember to make.
- **Automatic recall:** The `MemoryManager` drives the store on every turn, searching Mem0 and injecting the results into the prompt with no tool call required.
- **Server-side extraction:** Because the store implements `add_messages`, enabling extraction routes raw conversation turns straight to Mem0's extraction pipeline, skipping the extra client-side model call needed to distill facts first.
- **Hosted or self-hosted:** An API key targets the hosted Mem0 Platform; a config dict targets self-hosted Mem0 OSS.
- **Entity scoping:** Accepts `user_id`, `agent_id`, `run_id`, and `app_id`, with at least one required and invalid combinations rejected at construction rather than on the first write.
See [Strands Agents](/integrations/strands) for setup and [SDK & Tools](/changelog/sdk) for PR links.
</Update>
<Update label="2026-08-13" description="Kimi Code plugin">
**Kimi Code: Mem0 Joins the Editor Plugin Family**
The shared Mem0 editor plugin now covers Kimi Code alongside Claude Code, Cursor, Codex, and Antigravity, running on the same scripts, skills, and memory bank, so context written in one editor is available in the others.
- **Hosted MCP server:** Registers `https://mcp.mem0.ai/mcp/`, authenticated with `MEM0_API_KEY` as a bearer token.
- **Automatic capture and recall:** SessionStart loads context through the `context-loader` skill, and hooks on prompt submit, file reads, Bash output, stop, and pre-compact capture and inject memory without an explicit tool call.
- **Guardrails:** Direct `Write` / `Edit` / `MultiEdit` to memory files is blocked, and metadata defaults are enforced on every Mem0 MCP tool call.
- **Kimi hook adapter:** A shim normalizes Kimi Code's hook contract to the shape the shared scripts already expect, resolving the real project directory and translating the differing prompt, tool-output, and MCP tool-name fields.
See [SDK & Tools](/changelog/sdk) for version details and PR links.
</Update>
<Update label="2026-07-30" description="n8n and Zapier integrations">
**Workflow Automation: Mem0 Memory in n8n and Zapier**
-161
View File
@@ -7,19 +7,6 @@ mode: "wide"
<Tabs>
<Tab title="Python">
<Update label="2026-08-24" description="v2.0.19">
**Bug Fixes:**
- **Embeddings:** `HuggingFaceEmbedding` now falls back to the `HUGGINGFACE_API_KEY` env var, then a placeholder key, when `huggingface_base_url` is set and no `api_key` is configured. The OpenAI-compatible client used to talk to TEI endpoints raises at construction when no key resolves at all, so a TEI deployment that doesn't require a real key previously failed to initialize ([#6947](https://github.com/mem0ai/mem0/pull/6947))
- **Core:** `remove_code_blocks()` now accepts list-shaped content (a sequence of `{"text": ...}` blocks, as some agent frameworks pass) by joining each block's text before stripping code fences, instead of raising `AttributeError` from calling `.strip()` on a list ([#6947](https://github.com/mem0ai/mem0/pull/6947))
- **Core:** `create_procedural_memory()` (`Memory` and `AsyncMemory`) now raises a clear `ValueError` when the LLM returns no content for the summary, instead of continuing with empty content that surfaced as a confusing error further down the call ([#6947](https://github.com/mem0ai/mem0/pull/6947))
- **Proxy:** `mem0.proxy` no longer auto-installs `litellm` via a `pip install` subprocess when the import fails; it now raises `ImportError` with instructions to install it yourself. The auto-install could hang or fail silently in restricted environments and ran an unreviewed install on the caller's behalf ([#6947](https://github.com/mem0ai/mem0/pull/6947))
- **Client:** `get_all()` (sync and async) now sends `page` and `page_size` as independent query params instead of requiring both to be set before either was sent. Passing only `page_size` without `page` previously had it silently dropped, so results came back at the server's default page size ([#6900](https://github.com/mem0ai/mem0/pull/6900))
- **LLMs:** Add `provider_override` to `AWSBedrockConfig`, an explicit provider name (for example `"anthropic"`) for when `model` is an application inference profile ARN whose opaque ID has no provider substring for `extract_provider()` to detect. Without it, those ARNs raised `ValueError: Unable to determine provider` ([#6899](https://github.com/mem0ai/mem0/pull/6899))
- **LLMs:** `VllmConfig` now falls back to the `VLLM_BASE_URL` env var when `vllm_base_url` isn't passed explicitly. The default was filled in before the env var was ever checked, so `VLLM_BASE_URL` was silently ignored ([#6897](https://github.com/mem0ai/mem0/pull/6897))
</Update>
<Update label="2026-08-11" description="v2.0.18">
**Bug Fixes:**
@@ -1219,19 +1206,6 @@ See the [OSS v2 to v3 migration guide](https://docs.mem0.ai/migration/oss-v2-to-
<Tab title="TypeScript">
<Update label="2026-08-24" description="v3.1.7">
**Bug Fixes:**
- **Vector Stores:** Redis and Valkey `search()` / `get()` / `list()` now preserve `agent_id`, `run_id`, and `user_id` as snake_case in the returned payload. The shared payload formatter camelCased every key including those three identity fields, so entity ids came back as `agentId` / `runId` / `userId`, inconsistent with every other vector store ([#6902](https://github.com/mem0ai/mem0/pull/6902))
- **Memory (OSS):** Embedding-cache lookups now use `Object.prototype.hasOwnProperty.call()` instead of the `in` operator or a falsy `||` check. Memory text matching an inherited `Object.prototype` property name (`constructor`, `toString`, and similar) previously short-circuited the lookup and resolved to that inherited value instead of computing a real embedding, silently corrupting the stored vector ([#6903](https://github.com/mem0ai/mem0/pull/6903))
- **Client:** `getAll()` now sends `page` and `pageSize` as independent query params instead of requiring both to be set before either was sent. Passing only `pageSize` without `page` previously had it silently dropped, so results came back at the server's default page size ([#6900](https://github.com/mem0ai/mem0/pull/6900))
- **LLMs:** Add `providerOverride` to the Bedrock `LLMConfig`, an explicit provider name for when `model` is an application inference profile ARN whose opaque ID has no provider substring for `extractProvider()` to detect. Without it, those ARNs threw before the provider-specific settings could be initialized ([#6899](https://github.com/mem0ai/mem0/pull/6899))
**Security:**
- **Dependencies:** Resolved 17 additional high and critical severity dependency vulnerabilities across 5 pnpm workspaces (`mem0-ts`, `vercel-ai-sdk`, `n8n-nodes-mem0`, `zapier-mem0`, `server/dashboard`) via `pnpm.overrides` and a `tar` patch ([#7032](https://github.com/mem0ai/mem0/pull/7032))
</Update>
<Update label="2026-08-11" description="v3.1.6">
**New Features:**
@@ -1849,17 +1823,6 @@ See the [TypeScript SDK migration guide](https://docs.mem0.ai/migration/ts-v2-to
<Tab title="CLI">
<Update label="2026-08-24" description="Python v0.2.12 / Node v0.2.13">
**New Features:**
- **`version`:** New `mem0 version` subcommand, alongside the existing `--version` flag, so scripts and agent harnesses can read the CLI version as a regular subcommand instead of a root-level flag (Python and Node [#6907](https://github.com/mem0ai/mem0/pull/6907))
- **`add`:** New `--agent-custom-instructions` flag, threaded through to `agent_custom_instructions` on the `/v3/memories/add/` payload: a second extraction instruction set that applies only to agent-scoped memories, matching the SDKs' `agentCustomInstructions` / `agent_custom_instructions` support (Python and Node [#6910](https://github.com/mem0ai/mem0/pull/6910))
**Documentation:**
- **`search --filter`:** The `--filter` help text and `docs/platform/cli.mdx` now spell out the JSON shape (`{"AND": [...]}` / `{"OR": [...]}`) with a concrete example instead of just calling it "an advanced filter expression," and a matching example command was added to both the CLI help text and the docs page (Python and Node [#6907](https://github.com/mem0ai/mem0/pull/6907))
</Update>
<Update label="2026-08-04" description="Python v0.2.11 / Node v0.2.12">
**New Features:**
@@ -2045,22 +2008,6 @@ A full-featured command-line interface for Mem0, available in both Python and No
<Tabs>
<Tab title="Mem0 Plugin">
<Update label="2026-08-24" description="mem0-plugin v0.2.15">
**Fixes:**
- **Search:** A failed search request now prints `[mem0] search request failed: <error>` to stderr before returning no results. `search_memories()` swallowed every exception and returned `[]`, so an expired API key, a network failure, or a 500 from the backend was indistinguishable from a genuine "nothing stored yet" and the agent carried on with no context and no warning. Shared by Claude Code, Cursor, Codex, Antigravity, and Kimi ([#6898](https://github.com/mem0ai/mem0/pull/6898))
- **Cursor:** `mcpServers` in `.cursor-plugin/plugin.json` now points at `./.cursor-mcp.json` instead of `.cursor-mcp.json`. The un-prefixed path resolved inconsistently depending on Cursor's working directory when it loaded the plugin ([#6948](https://github.com/mem0ai/mem0/pull/6948))
- **Codex:** `install_codex_hooks.py` now prints all six registered events (`PreToolUse, SessionStart, UserPromptSubmit, PostToolUse, Stop, PreCompact`) after installing, instead of a stale four-event list left over from an earlier version of the installer. The README's hook table is corrected to match, documenting the three `PreToolUse` handlers and two `PostToolUse` handlers that were previously undocumented ([#6948](https://github.com/mem0ai/mem0/pull/6948))
**Documentation:**
- New [Claude.ai](/integrations/claude-ai) integration page ([#6948](https://github.com/mem0ai/mem0/pull/6948))
<Note>
The Claude Code, Cursor, and Codex per-editor manifests (`.claude-plugin/plugin.json`, `.cursor-plugin/plugin.json`, `.codex-plugin/plugin.json`) had drifted to `0.2.13` while the `.claude-plugin/marketplace.json` and `.cursor-plugin/marketplace.json` listings had already moved to `0.2.14`, so installs were pinned one release behind what the marketplace advertised. This release realigns every manifest and marketplace listing to `0.2.15`.
</Note>
</Update>
<Update label="2026-08-04" description="mem0-plugin v0.2.14">
**Fixes:**
@@ -2413,13 +2360,6 @@ Initial release of the Mem0 plugin for Claude Code and Cursor, followed by Codex
<Tab title="Antigravity">
<Update label="2026-08-24" description="Antigravity plugin v0.1.7">
**Fixes:**
- **Hooks:** The `mem0-ensure-deps` and `mem0-session-start` hook commands in `hooks.json` no longer redirect stderr to `/dev/null`. Both commands still end in `|| true` so a failure can't block startup, but a broken dependency install or session bootstrap now shows up in the Antigravity hook log instead of failing invisibly ([#6948](https://github.com/mem0ai/mem0/pull/6948))
</Update>
<Update label="2026-08-04" description="Antigravity plugin v0.1.6">
**Fixes:**
@@ -2487,31 +2427,8 @@ Existing memories written by the previous versions are not rewritten. If your me
</Tab>
<Tab title="Kimi">
<Update label="2026-08-24" description="kimi-plugin v0.1.0">
**Initial release** of the Mem0 plugin for Kimi Code, sharing its scripts, skills, and marketplace listing with the Claude Code / Cursor / Codex / Antigravity plugin family ([#6919](https://github.com/mem0ai/mem0/pull/6919))
**New Features:**
- **MCP server:** Registers the hosted Mem0 MCP server at `https://mcp.mem0.ai/mcp/`, authenticated via the `MEM0_API_KEY` env var as a bearer token.
- **Lifecycle hooks:** Wires SessionStart (loads context through the `context-loader` skill), UserPromptSubmit, three PreToolUse hooks (blocks direct `Write`/`Edit`/`MultiEdit` to memory files, enforces metadata defaults on Mem0 MCP tool calls, and injects context on file reads), two PostToolUse hooks (post-tool tracking and Bash-output scanning), Stop, and PreCompact.
- **Hook adapter:** `kimi_hook_shim.sh` normalizes Kimi Code's hook contract to what the shared hook scripts expect: it resolves the real project directory from the payload's `cwd` (Kimi forces the plugin root as the working directory), converts the array-shaped `prompt` field and the `tool_output` / `tool_input.path` field names to the Claude-style shapes the scripts already handle, and translates the plugin-scoped MCP tool name prefix.
- **Shared policy skill:** Bundles the `/mem0:policy` skill for managing the `## Instructions` and `## Agent Instructions` sections of `mem0.md`.
</Update>
</Tab>
<Tab title="OpenClaw">
<Update label="2026-08-24" description="openclaw-mem0 v1.0.16">
**Security:**
- **Dependencies:** Tightened the `undici` pnpm override from `<6.27.0 → >=6.27.0 <8.0.0` to `<7.29.0 → >=7.29.0 <8.0.0`, closing a newer CVE range the previous floor didn't cover, as part of a wider dependency patch sweep across the pnpm workspaces ([#6847](https://github.com/mem0ai/mem0/pull/6847))
</Update>
<Update label="2026-08-01" description="openclaw-mem0 v1.0.15">
**Improvements:**
@@ -2775,13 +2692,6 @@ Existing memories written by the previous versions are not rewritten. If your me
<Tab title="Pi Agent">
<Update label="2026-08-24" description="Pi Agent plugin v0.1.5">
**Security:**
- **Dependencies:** Tightened the `undici` pnpm override from `<6.27.0 → >=6.27.0 <8.0.0` / `>=8.0.0 <8.5.0 → >=8.5.0` to `<7.29.0 → >=7.29.0 <8.0.0` / `>=8.0.0 <8.9.0 → >=8.9.0 <9.0.0`, closing a newer CVE range the previous floors didn't cover, as part of a wider dependency patch sweep across the pnpm workspaces ([#6847](https://github.com/mem0ai/mem0/pull/6847))
</Update>
<Update label="2026-08-01" description="Pi Agent plugin v0.1.4">
**Security:**
@@ -2839,57 +2749,8 @@ Existing memories written by the previous versions are not rewritten. If your me
</Tab>
<Tab title="Strands">
<Update label="2026-08-24" description="strands-mem0 v0.1.0">
**Initial release** of [`strands-mem0`](https://pypi.org/project/strands-mem0/), a native `MemoryStore` that plugs Mem0 into the [Strands Agents](https://strandsagents.com/) `MemoryManager` ([#7021](https://github.com/mem0ai/mem0/pull/7021))
**New Features:**
- **Automatic recall and injection:** `Mem0MemoryStore.search()` runs every turn through the `MemoryManager`, so relevant memories are searched and prepended to the prompt with no explicit tool call required.
- **Server-side extraction:** `add_messages()` renders raw conversation turns to text and hands them to Mem0's own extraction pipeline (`infer=True`), so enabling extraction skips an extra client-side model call to distill facts first.
- **Verbatim writes:** `add()` stores a single fact exactly as given (`infer=False`), the sink used by the `add_memory` tool or a client-side extractor.
- **Entity scoping:** Accepts `user_id`, `agent_id`, `run_id`, and `app_id`; at least one is required, and mixing the platform-only `app_id` with a self-hosted `config` raises at construction instead of failing on the first write.
- **Hosted or self-hosted:** Defaults to the hosted Mem0 Platform via `api_key` (or `$MEM0_API_KEY`), or pass a `config` dict for a self-hosted Mem0 OSS backend.
- **Non-blocking construction:** The underlying Mem0 client is built lazily on first use inside `asyncio.to_thread`, so API-key validation and OSS embedder/vector-store setup never block the event loop.
<Note>
`Mem0MemoryStore` is the automatic-recall store for the `MemoryManager`. For a model-called tool instead, use the [`mem0_memory`](https://github.com/strands-agents/tools) tool from `strands-agents-tools`; both share the same Mem0 backend and namespace. See [Strands Agents](/integrations/strands) for setup.
</Note>
</Update>
</Tab>
<Tab title="DeepSeek Harness">
<Update label="2026-08-24" description="dsh-mem0 v0.1.0">
**Initial release** of [`dsh-mem0`](https://www.npmjs.com/package/@mem0/dsh-mem0), a native DeepSeek Harness (Cordis) plugin that registers Mem0 as two agent-callable tools ([#7027](https://github.com/mem0ai/mem0/pull/7027))
**New Features:**
- **`search_memory`:** Recalls facts relevant to a query, with an optional `limit` (default 10) and per-call `userId` / `agentId` / `runId` scope overrides.
- **`add_memory`:** Stores a fact for future sessions, tagged `source: "DEEPSEEK_HARNESS"` for backend attribution; extraction runs asynchronously server-side, so a stored fact may take a moment to become searchable.
- **Cordis lifecycle:** `apply(ctx, config)` declares `inject = ['tools']`, so the plugin waits for the harness tool registry to exist, and both tools are registered via `ctx.tools.register()` so they auto-unregister when the plugin unmounts.
- **Config:** `userId` is required; `apiKey` defaults to `$MEM0_API_KEY`; `host` optionally points at a dedicated Mem0 Platform base URL (not a switch to self-hosted Mem0 OSS).
<Note>
Developer preview: auto-capture and auto-recall (memory injected into context automatically, without an explicit tool call) are planned but not yet built. The backend's `KNOWN_EVENT_SOURCES` allowlist also needs `"DEEPSEEK_HARNESS"` added before usage surfaces by name in telemetry rather than bucketing into "OTHERS". See [DeepSeek Harness](/integrations/dsh-mem0) for setup.
</Note>
</Update>
</Tab>
<Tab title="Vercel AI SDK">
<Update label="2026-08-24" description="Vercel AI SDK v3.0.2">
**Security:**
- **Dependencies:** Tightened the `js-yaml` pnpm overrides from `<3.15.0 → >=3.15.0 <4.0.0` / `>=4.0.0 <4.3.0 → >=4.3.0 <5.0.0` to `<3.15.1 → >=3.15.1 <4.0.0` / `>=4.0.0 <4.3.1 → >=4.3.1 <5.0.0`, closing a newer CVE range the previous floors didn't cover, as part of a wider dependency patch sweep across the pnpm workspaces ([#7032](https://github.com/mem0ai/mem0/pull/7032))
</Update>
<Update label="2026-08-01" description="Vercel AI SDK v3.0.1">
**Security:**
@@ -2982,13 +2843,6 @@ Existing memories written by the previous versions are not rewritten. If your me
<Tab title="n8n">
<Update label="2026-08-24" description="n8n-nodes-mem0 v0.1.4">
**Security:**
- **Dependencies:** Add `js-yaml` pnpm overrides (`<3.15.1 → >=3.15.1 <4.0.0`, `>=4.0.0 <4.3.1 → >=4.3.1 <5.0.0`) to close a HIGH/CRITICAL severity advisory, as part of a wider dependency patch sweep across the pnpm workspaces ([#7032](https://github.com/mem0ai/mem0/pull/7032))
</Update>
<Update label="2026-08-05" description="n8n-nodes-mem0 v0.1.3">
**Changes:**
@@ -3033,21 +2887,6 @@ Existing memories written by the previous versions are not rewritten. If your me
<Tab title="Zapier">
<Update label="2026-08-24" description="Zapier app v0.1.2">
**Changes:**
- **Connection label:** The saved connection now shows the account's email (`{{user_email}}`, read from the `/v1/ping/` test response) in the Zap editor instead of a static "Mem0" label, so a user with more than one Mem0 connection can tell them apart ([#6985](https://github.com/mem0ai/mem0/pull/6985))
- **Action and search copy:** Reworded labels and descriptions to address Zapier's publishing review: **Get Memories** is now **Find Memories by User**, **Search Memories** is now **Find Memories**, and every description now reads as a third-person statement of what the step does ([#6985](https://github.com/mem0ai/mem0/pull/6985))
- **Attribution:** Add Memory now tags writes with `source: "ZAPIER"` in the request body, so usage is attributed to this integration server-side ([#6985](https://github.com/mem0ai/mem0/pull/6985))
**Removed:**
- **Client-side telemetry:** Deleted the embedded PostHog telemetry client (`telemetry.ts`) and its call sites in Add Memory, Get Memories, and Search Memories. Usage attribution now happens server-side via the `source: "ZAPIER"` tag above instead of a separate fire-and-forget analytics call from inside the published app ([#6985](https://github.com/mem0ai/mem0/pull/6985))
**Security:**
- **Dependencies:** Tightened the `undici` pnpm override to `<7.29.0 → >=7.29.0 <8.0.0` / `>=8.0.0 <8.9.0 → >=8.9.0 <9.0.0` and added a `brace-expansion` override ([#6847](https://github.com/mem0ai/mem0/pull/6847)), then added a `js-yaml` override (`<3.15.1 → >=3.15.1 <4.0.0`, `>=4.0.0 <4.3.1 → >=4.3.1 <5.0.0`) closing a further HIGH/CRITICAL severity advisory ([#7032](https://github.com/mem0ai/mem0/pull/7032))
</Update>
<Update label="2026-08-04" description="Zapier app v0.1.1">
**Bug Fixes:**
@@ -99,7 +99,6 @@ Here are the parameters available for configuring the Hugging Face embedder:
| `embedding_dims` | Dimensions of the embedding model | `selected_model_dimensions` |
| `model_kwargs` | Additional arguments for the model | `None` |
| `huggingface_base_url` | URL to connect to Text Embeddings Inference (TEI) API | `None` |
| `api_key` | API key for the endpoint; falls back to the `HUGGINGFACE_API_KEY` env var. Only used on the `huggingface_base_url` path | `"hf"` |
</Tab>
<Tab title="TypeScript">
| Parameter | Description | Default Value |
-27
View File
@@ -79,33 +79,6 @@ For detailed guidance on pull requests, refer to [GitHub's documentation](https:
---
## Installing from Source
If you just want to run the latest, unreleased SDK code instead of the published `mem0ai` package, for example to try out a fix before it ships, or to depend on a fork, install directly from a local clone rather than setting up the full contributor environment below.
### Python SDK
```bash
git clone https://github.com/mem0ai/mem0.git
cd mem0
pip install -e .
```
This installs `mem0ai` in editable mode, so edits under `mem0/` take effect immediately without reinstalling. Add an extra if you need one, e.g. `pip install -e ".[vector-stores]"` (see `pyproject.toml` for the full list). If you are contributing to the SDK itself and need every optional dependency for the test suite, use `hatch` instead, see [Dependency Management](#dependency-management).
### TypeScript SDK
```bash
git clone https://github.com/mem0ai/mem0.git
cd mem0/mem0-ts
pnpm install
pnpm run build
```
This builds `mem0-ts/dist` (CJS + ESM). To use it from another local project, add it as a `file:` dependency pointing at `mem0-ts`, or run `pnpm link --global` inside `mem0-ts` and `pnpm link --global mem0ai` in the consuming project.
---
## Python SDK (`mem0/`)
### Dependency Management
@@ -0,0 +1,519 @@
---
title: Control Memory Ingestion
description: "Filter speculation, enforce formats, and gate low-confidence data before it persists."
---
<Info icon="cloud">
**Works with:** Mem0 Platform (`MemoryClient`)
</Info>
AI assistants plugged with memory systems face a problem - they often store everything. Not every conversation needs to be remembered, and not every detail should go to the memory store. Without proper controls, memory systems accumulate unreliable data.
Mem0 lets you control your memory ingestion pipeline. In this cookbook, we'll demonstrate these controls using a medical assistant example - showing how to filter unwanted data, enforce data formats, and implement confidence-based storage.
---
## Overview
Without controls, everything gets stored - speculation, low-confidence data, and information that shouldn't persist. This uncontrolled ingestion leads to cluttered memory and retrieval failures.
Mem0 provides **three tools to control** what gets stored:
1. **Custom instructions** define what to remember and what to ignore.
2. **Confidence thresholds** ensure only verified facts persist.
3. **Memory updates** let you change information without creating duplicates.
In this tutorial, we will:
- Filter speculative statements with custom instructions
- Configure confidence thresholds for fact verification
- Update stored information without duplication
- Build a complete ingestion pipeline
---
## Setup
```python
from mem0 import MemoryClient
client = MemoryClient(api_key="your-api-key")
```
<Note>
Replace `your-api-key` with your actual Mem0 API key from the <a href="https://app.mem0.ai?utm_source=oss&utm_medium=cookbook-memory-ingestion" rel="nofollow">dashboard</a>. Without proper API authentication, memory operations will fail.
</Note>
---
## The Problem
Uncontrolled ingestion stores everything, including speculation:
```python
# Patient mentions speculation
messages = [{"role": "user", "content": "I think I might be allergic to penicillin"}]
client.add(messages, user_id="patient_123")
# Check what got stored
results = client.search("patient allergies", filters={"user_id": "patient_123"})
print(results['results'][0]['memory'])
```
**Output:**
```
Patient is allergic to penicillin
```
<Warning>
Without custom instructions, AI assistants treat speculation as confirmed facts. "I think I might be allergic" becomes "Patient is allergic": a dangerous transformation in sensitive domains like healthcare, legal, or financial services.
</Warning>
The speculation became a confirmed fact. Let's add controls.
---
## Custom Instructions
Custom instructions tell Mem0 what to store and what to ignore.
```python
instructions = """
Only store CONFIRMED medical facts.
Store:
- Confirmed diagnoses from doctors
- Known allergies with documented reactions
- Current medications being taken
Ignore:
- Speculation (words like "might", "maybe", "I think")
- Unverified symptoms
- Casual mentions without confirmation
"""
client.project.update(custom_instructions=instructions)
# Same speculative statement
messages = [{"role": "user", "content": "I think I might be allergic to penicillin"}]
client.add(messages, user_id="patient_123")
# Check what got stored
results = client.get_all(filters={"user_id": "patient_123"})
print(f"Memories stored: {len(results['results'])}")
```
**Output:**
```
Memories stored: 0
```
<Info>
**Expected output:** Zero memories stored. The speculative statement "I think I might be allergic" was filtered out before reaching storage. Custom instructions are actively blocking unreliable data.
</Info>
The speculation was filtered out.
---
## Designing Custom Instructions
When designing instructions, consider the trade-off between precision and recall:
**Too restrictive:** You'll miss important information (false negatives)
```python
# Too strict - filters out useful context
"""
Only store information if explicitly stated by a doctor with full name,
date, time, and medical license number.
"""
```
**Too permissive:** You'll store unreliable data (false positives)
```python
# Too loose - stores speculation as fact
"""
Store any health-related information mentioned.
"""
```
**Balanced approach:**
```python
# Clear categories with examples
"""
Store CONFIRMED facts:
- Diagnoses: "Dr. Smith diagnosed hypertension on March 15th"
- Allergies: "Patient had hives reaction to penicillin"
- Medications: "Taking Lisinopril 10mg daily"
Ignore SPECULATION:
- "I think I might have..."
- "Maybe it's..."
- "Could be related to..."
"""
```
<Tip>
Start with strict instructions (only store confirmed facts), then relax them based on your use case. It's easier to allow more data than to clean up polluted memory. Test with sample conversations before deploying to production.
</Tip>
Start with clear categories and iterate based on retrieval quality.
---
## Confidence Thresholds
Mem0 assigns confidence scores to extracted memories. Use these to filter low-quality data.
### Setting Thresholds
Setting the right confidence threshold depends on your application:
- **High-stakes domains** (medical, legal): Require 0.8+ confidence
- **General assistants**: 0.6+ confidence is often sufficient
- **Exploratory systems**: Lower thresholds (0.4+) capture more data
Test your pipeline with multiple input examples and threshold combinations to find what works for your use case.
```python
# Configure stricter instructions
client.project.update(
custom_instructions="""
Only extract memories with HIGH confidence.
Require specific details (dates, dosages, doctor names) for medical facts.
Skip vague or uncertain statements.
"""
)
# Test with uncertain statement
messages = [{"role": "user", "content": "The doctor mentioned something about my blood pressure"}]
result1 = client.add(messages, user_id="patient_123")
# Test with confirmed fact
messages = [{"role": "user", "content": "Dr. Smith diagnosed me with hypertension on March 15th"}]
result2 = client.add(messages, user_id="patient_123")
print("Vague statement stored:", len(result1['results']) > 0)
print("Confirmed fact stored:", len(result2['results']) > 0)
```
**Output:**
```
Vague statement stored: False
Confirmed fact stored: True
```
<Info icon="check">
**Expected behavior:** Low-confidence extractions are now filtered out automatically. Only verified facts with specific details (names, dates, dosages) persist in memory. The confidence threshold is working.
</Info>
The vague statement was filtered for low confidence. The confirmed fact with specific details was stored.
---
## Filtering Sensitive Information
Custom instructions can prevent storing personal identifiers:
```python
client.project.update(
custom_instructions="""
Medical memory rules:
STORE:
- Confirmed diagnoses
- Verified allergies
- Current medications
NEVER STORE:
- Social Security Numbers
- Insurance policy numbers
- Credit card information
- Full addresses
- Phone numbers
Replace identifiers with generic references if mentioned.
"""
)
# Test with PII
messages = [
{"role": "user", "content": "My SSN is 123-45-6789 and I'm allergic to penicillin"}
]
client.add(messages, user_id="patient_123")
# Check what was stored
results = client.get_all(filters={"user_id": "patient_123"})
for result in results['results']:
print(result['memory'])
```
**Output:**
```
Patient is allergic to penicillin
```
The SSN was filtered out, but the allergy was stored.
---
## Updating Memories
When information changes, update existing memories instead of creating duplicates.
```python
# Initial allergy stored
result = client.add(
[{"role": "user", "content": "Patient confirmed allergy to penicillin with documented hives reaction"}],
user_id="patient_123"
)
memory_id = result['results'][0]['id']
print(f"Stored memory: {memory_id}")
# Later, patient gets retested - allergy was false positive
client.update(
memory_id=memory_id,
text="Patient tested negative for penicillin allergy on April 2nd, 2025. Previous allergy was false positive.",
metadata={"verified": True, "updated_date": "2025-04-02"}
)
# Retrieve the updated memory
updated = client.get(memory_id)
print(f"\\nUpdated memory: {updated['memory']}")
print(f"Metadata: {updated['metadata']}")
```
**Output:**
```
Stored memory: mem_abc123
Updated memory: Patient tested negative for penicillin allergy on April 2nd, 2025. Previous allergy was false positive.
Metadata: {'verified': True, 'updated_date': '2025-04-02'}
```
### Benefits of Updating
**Preserves history:**
- `created_at` shows when the memory was first stored
- `updated_at` shows when it was modified
- Audit trail for compliance
**Avoids conflicts:**
- No duplicate or contradicting memories
- Single source of truth for each fact
<Warning>
That “no duplicates” promise comes from the inference pipeline. Keep `infer=True` when you rely on automatic updates. Raw imports (`infer=False`) skip conflict checks, so mixing the two modes for the same fact will create duplicates.
</Warning>
### Pick the right inference mode
| Mode | What it does | Best for | Watch out for |
| --- | --- | --- | --- |
| `infer=True` *(default)* | Runs the LLM pipeline so Mem0 extracts structured facts and resolves conflicts automatically. | Daily conversations, preference tracking, anything you want deduped. | Slightly slower because inference runs on every write. |
| `infer=False` | Stores your payload exactly as-is: no inference, no dedupe. | Bulk imports, compliance snapshots, curated facts you already trust. | Later `infer=True` calls for the same fact will create duplicates you must clean manually. |
<Tip>
Stay consistent per data source. If you need both behaviors, keep them in separate scopes (e.g., different `app_id` or `run_id`) so you always know which memories are inferred vs direct imports.
</Tip>
---
## Update vs Delete
When should you update vs delete?
### Update when:
- Information changes but remains relevant
- You need audit history
- The memory has relationships to other data
```python
# Medication dosage changed
client.update(
memory_id=med_id,
text="Taking Lisinopril 20mg daily (increased from 10mg on March 1st)"
)
```
### Delete when:
- Information was completely wrong
- Memory is no longer relevant
- Duplicate entry
```python
# Duplicate entry
client.delete(memory_id)
```
---
## Putting It Together
Here's a complete ingestion pipeline with all controls:
```python
from mem0 import MemoryClient
import os
# Initialize client
client = MemoryClient(api_key=os.getenv("MEM0_API_KEY"))
# Configure custom instructions
client.project.update(
custom_instructions="""
Medical memory assistant rules:
STORE:
- Confirmed diagnoses (with doctor name and date)
- Verified allergies (with reaction details)
- Current medications (with dosage)
IGNORE:
- Speculation (might, maybe, possibly)
- Unverified symptoms
- Personal identifiers (SSN, insurance numbers)
CONFIDENCE:
Require high confidence. Reject vague or uncertain statements.
Require specific details: names, dates, dosages.
"""
)
# Helper function for safe ingestion
def add_medical_memory(content, user_id, metadata=None):
"""Add memory with automatic filtering."""
result = client.add(
[{"role": "user", "content": content}],
user_id=user_id,
metadata=metadata or {}
)
if result['results']:
print(f"✓ Stored: {result['results'][0]['memory']}")
else:
print(f"✗ Filtered: {content}")
return result
# Test cases
print("Testing ingestion pipeline:\\n")
test_cases = [
"I think I might be allergic to penicillin",
"Dr. Johnson confirmed penicillin allergy on Jan 15th with hives reaction",
"Patient SSN is 123-45-6789",
"Currently taking Lisinopril 10mg daily for hypertension",
"Feeling tired lately",
"Dr. Martinez diagnosed Type 2 diabetes on February 3rd, 2025"
]
for content in test_cases:
add_medical_memory(content, user_id="patient_123")
print()
```
**Output:**
```
Testing ingestion pipeline:
✗ Filtered: I think I might be allergic to penicillin
✓ Stored: Patient has confirmed penicillin allergy diagnosed by Dr. Johnson on January 15th with hives reaction
✗ Filtered: Patient SSN is 123-45-6789
✓ Stored: Patient is currently taking Lisinopril 10mg daily for hypertension
✗ Filtered: Feeling tired lately
✓ Stored: Patient diagnosed with Type 2 diabetes by Dr. Martinez on February 3rd, 2025
```
---
## Per-Call Instructions
You can override project-level instructions for specific conversations:
First define custom instructions
```python
custom_instructions="""Emergency intake mode:Store ALL symptoms and observations immediately.
Flag for later review and verification."""
```
```python
# Emergency intake - store everything temporarily
emergency_messages = [
{"role": "user", "content": "Patient arrived with chest pain and shortness of breath"}
]
client.add(
emergency_messages,
user_id="patient_456",
custom_instructions=custom_instructions,
metadata={"type": "emergency", "review_required": True}
)
```
This is useful for:
- Different conversation types (emergency vs routine)
- Channel-specific rules (phone vs in-person)
- Temporary data collection that needs review
---
## What You Built
You now have a medical assistant with production-grade memory controls:
- **Custom instructions** - Filter speculation and enforce confirmed facts only
- **Confidence thresholds** - Gate extractions below 0.7 confidence score
- **Memory updates** - Modify stored information without creating duplicates
- **Per-call instructions** - Apply temporary rules for specific conversations
- **PII filtering** - Block sensitive data (SSNs, insurance numbers) automatically
These controls prevent retrieval failures and ensure your AI assistant works with reliable, verified information.
---
## Summary
Start with conservative filters (only store confirmed facts) and iterate based on your application's needs. Combine custom instructions with confidence thresholds for the most reliable memory ingestion pipeline.
<Card title="Build a Mem0 Companion" icon="users" href="/cookbooks/essentials/building-ai-companion">
Learn core memory patterns including temporary vs permanent data handling.
</Card>
<Snippet file="star-on-github.mdx" />
@@ -332,10 +332,10 @@ You learned how to:
href="/platform/features/v2-memory-filters"
/>
<Card
title="Custom Instructions"
description="Pair scoped storage with instructions that steer what Mem0 extracts and stores."
title="Control Memory Ingestion"
description="Pair scoped storage with rules that block low-quality facts."
icon="shield-check"
href="/platform/features/custom-instructions"
href="/cookbooks/essentials/controlling-memory-ingestion"
/>
</CardGroup>
@@ -287,8 +287,8 @@ Use **`get_all()`** for bulk retrieval, **`search()`** for specific questions, a
<Card title="Build a Mem0 Companion" icon="users" href="/cookbooks/essentials/building-ai-companion">
Learn core memory patterns including temporary vs permanent data handling.
</Card>
<Card title="Custom Instructions" icon="filter" href="/platform/features/custom-instructions">
Steer what Mem0 extracts so only verified insights make it into your export pipeline.
<Card title="Control Memory Ingestion" icon="filter" href="/cookbooks/essentials/controlling-memory-ingestion">
Ensure only verified insights make it into your export pipeline.
</Card>
</CardGroup>
@@ -249,8 +249,8 @@ Categories make retrieval faster and compliance easier. Define 3-5 clear categor
Instead of searching through everything, agents jump directly to the information type they need: billing issues, account details, or support tickets.
<CardGroup cols={2}>
<Card title="Custom Instructions" icon="filter" href="/platform/features/custom-instructions">
Keep categories meaningful by steering what Mem0 extracts before it lands in storage.
<Card title="Control Memory Ingestion" icon="filter" href="/cookbooks/essentials/controlling-memory-ingestion">
Keep categories meaningful by filtering noise before it lands in storage.
</Card>
<Card title="Export Tagged Memories" icon="download" href="/cookbooks/essentials/exporting-memories">
Use categories to drive audits, migrations, and compliance reports.
@@ -293,8 +293,8 @@ run().catch(console.error);
<Card title="Agents SDK Tool with Mem0" icon="robot" href="/cookbooks/integrations/agents-sdk-tool">
Extend the OpenAI Agents SDK with Mem0 integration capabilities.
</Card>
<Card title="Custom Instructions" icon="filter" href="/platform/features/custom-instructions">
Steer what memories get stored during tool calls.
<Card title="Control Memory Ingestion" icon="filter" href="/cookbooks/essentials/controlling-memory-ingestion">
Fine-tune what memories get stored during tool calls.
</Card>
</CardGroup>
@@ -360,8 +360,8 @@ Mem0 enables a seamless, intelligent content-writing workflow, perfect for conte
---
<CardGroup cols={2}>
<Card title="Custom Instructions" icon="filter" href="/platform/features/custom-instructions">
Steer what Mem0 extracts and stores to maintain a consistent writing style.
<Card title="Control Memory Ingestion" icon="filter" href="/cookbooks/essentials/controlling-memory-ingestion">
Filter and curate content examples to maintain consistent writing style.
</Card>
<Card title="Email Automation with Mem0" icon="envelope" href="/cookbooks/operations/email-automation">
Automate email drafting with memory-powered context and tone matching.
+6 -12
View File
@@ -35,7 +35,7 @@ Every cookbook opens with a **Works with** badge naming the SDK surface it uses.
| [Memory-Powered Support Agent](/cookbooks/operations/support-inbox) | Operations | OSS only |
</Tab>
<Tab title="Hosted Platform">
Twenty cookbooks run on the hosted Platform, using `MemoryClient` or the Mem0 MCP server with a `MEM0_API_KEY`.
Twenty-one cookbooks run on the hosted Platform, using `MemoryClient` or the Mem0 MCP server with a `MEM0_API_KEY`.
| Cookbook | Category | Works with |
| --- | --- | --- |
@@ -43,6 +43,7 @@ Every cookbook opens with a **Works with** badge naming the SDK surface it uses.
| [Research Assistant for YouTube](/cookbooks/companions/youtube-research) | Companions | Platform only |
| [Voice-First AI Companion](/cookbooks/companions/voice-companion-openai) | Companions | Platform only |
| [Build a Companion with Mem0](/cookbooks/essentials/building-ai-companion) | Essentials | OSS and Platform |
| [Control Memory Ingestion](/cookbooks/essentials/controlling-memory-ingestion) | Essentials | Platform only |
| [Export Stored Memories](/cookbooks/essentials/exporting-memories) | Essentials | Platform only |
| [Partition Memories by Entity](/cookbooks/essentials/entity-partitioning-playbook) | Essentials | Platform only |
| [Tag and Organize Memories](/cookbooks/essentials/tagging-and-organizing-memories) | Essentials | Platform only |
@@ -96,18 +97,11 @@ The most popular cookbooks to get going fast:
Balance personalization with consistent behavior across users, agents, and apps.
</Card>
<Card
title="Tag and Organize Memories"
icon="tags"
href="/cookbooks/essentials/tagging-and-organizing-memories"
title="Control Memory Ingestion"
icon="filter"
href="/cookbooks/essentials/controlling-memory-ingestion"
>
Use categories to keep retrieval fast and audits simple.
</Card>
<Card
title="Export Memories"
icon="download"
href="/cookbooks/essentials/exporting-memories"
>
Back up, migrate, and audit stored memory data.
Filter speculation and low-confidence data.
</Card>
</CardGroup>
+65 -62
View File
@@ -1,69 +1,68 @@
---
title: Memory Types
description: "What memory_type actually does in Mem0: procedural memory is implemented, semantic and episodic are not."
description: "See how Mem0 layers conversation, session, and user memories to keep agents contextual."
icon: "tag"
iconType: "solid"
---
# Memory Types
# How Mem0 Organizes Memory
Mem0's Python SDK exposes a `memory_type` parameter on `add()`. The underlying `MemoryType` enum defines three values, but only one of them is wired up. This page states plainly which is which so you don't build against a type that doesn't exist yet.
Mem0 separates memory into layers so agents remember the right detail at the right time. Think of it like a notebook: a sticky note for the current task, a daily journal for the session, and an archive for everything a user has shared.
## Status
## Key terms
| Type | Enum value | Status | Notes |
| --- | --- | --- | --- |
| Procedural memory | `procedural_memory` | **Implemented** | Python OSS only (`Memory`/`AsyncMemory`). Pass `memory_type="procedural_memory"` and `agent_id` to `add()`. Not available on the Platform `MemoryClient`, and not available in the TypeScript SDK (OSS or Platform). |
| Semantic memory | `semantic_memory` | **Not implemented** | Defined in the `MemoryType` enum but never read anywhere else in the codebase. Passing it to `add()` raises a validation error. There is no evidence in this repo of a roadmap date for this. |
| Episodic memory | `episodic_memory` | **Not implemented** | Same as above: defined, never wired into the extraction pipeline, rejected by validation, no documented roadmap. |
- **Conversation memory**: In-flight messages inside a single turn (what was just said).
- **Session memory**: Short-lived facts that apply for the current task or channel.
- **User memory**: Long-lived knowledge tied to a person, account, or workspace.
- **Organizational memory**: Shared context available to multiple agents or teams.
<Warning>
Only `procedural_memory` is a real, working value. Calling `memory.add(messages, memory_type="semantic_memory")` (or `episodic_memory`) is rejected and tells you to pass `procedural_memory` instead. Sync `Memory.add()` raises `Mem0ValidationError`; `AsyncMemory.add()` raises a plain `ValueError`.
</Warning>
## Procedural memory
Procedural memory stores step-by-step task knowledge (how an agent performs a workflow) rather than facts about a user. It requires `agent_id`:
```python
from mem0 import Memory
memory = Memory()
memory.add(
[
{"role": "user", "content": "Book a flight from SFO to NYC"},
{"role": "assistant", "content": "1. Search flights. 2. Filter by price. 3. Confirm booking."},
],
agent_id="travel-agent",
memory_type="procedural_memory",
)
```mermaid
graph LR
A[Conversation turn] --> B[Session memory]
B --> C[User memory]
C --> D[Org memory]
C --> E[Mem0 retrieval layer]
```
Omit `memory_type` entirely and Mem0 stores the messages as an ordinary memory: there is no semantic/episodic pathway for it to fall into. Any other explicit value is rejected by validation rather than quietly falling back to an ordinary memory.
## Short-term vs long-term memory
## How every other memory is scoped
Short-term memory keeps the current conversation coherent. It includes:
Outside of the `procedural_memory` special case, Mem0 does not sort memories into named types. Every memory is scoped by the identifiers you pass in, and the same identifiers are used to retrieve it later:
- **Conversation history**: recent turns in order so the agent remembers what was just said.
- **Working memory**: temporary state such as tool outputs or intermediate calculations.
- **Attention context**: the immediate focus of the assistant, similar to what a person holds in mind mid-sentence.
- **`user_id`**: ties a memory to a specific person or account.
- **`agent_id`**: ties a memory to a specific agent or assistant persona.
- **`run_id`**: ties a memory to a specific session, task, or conversation thread.
- **`app_id`** (Platform only): ties a memory to a specific application or tenant, in addition to the three above. See <Link href="/platform/features/entity-scoped-memory">Entity-Scoped Memory</Link>.
Long-term memory preserves knowledge across sessions. It captures:
At least one identifier is required on `add()`. Passing more than one narrows the scope further (for example, `user_id` + `run_id` together).
- **Factual memory**: user preferences, account details, and domain facts.
- **Episodic memory**: summaries of past interactions or completed tasks.
- **Semantic memory**: relationships between concepts so agents can reason about them later.
Mem0 maps these classic categories onto its layered storage so you can decide what should fade quickly versus what should last for months.
## How does it work?
Mem0 stores each layer separately and merges them when you query:
1. **Capture**: Messages enter the conversation layer while the turn is active.
2. **Promote**: Relevant details persist to session or user memory based on your `user_id`, `run_id`, and metadata.
3. **Retrieve**: The search pipeline pulls from all layers, ranking user memories first, then session notes, then raw history.
```python
import os
from mem0 import Memory
memory = Memory()
# Sticky note: conversation memory
memory.add(
"I'm Alex and I prefer boutique hotels.",
["I'm Alex and I prefer boutique hotels."],
user_id="alex",
run_id="trip-planning-2025",
)
# Later in the session, pull long-term + session context
results = memory.search(
"Any hotel preferences?",
filters={"user_id": "alex", "run_id": "trip-planning-2025"},
@@ -71,48 +70,52 @@ results = memory.search(
```
<Tip>
Use `run_id` when you want a set of memories to stay tied to one session or task; use `user_id` alone for anything that should persist across every session for that person.
Use `run_id` when you want short-term context to expire automatically; rely on `user_id` for lasting personalization.
</Tip>
## How memories are extracted and updated
## When should you use each layer?
When `infer=True` (the default) on `add()`, Mem0 runs a single pipeline rather than routing through separate type-specific paths:
- **Conversation memory**: Tool calls or chain-of-thought that only matter within the current turn.
- **Session memory**: Multi-step tasks (onboarding flows, debugging sessions) that should reset once complete.
- **User memory**: Personal preferences, account state, or compliance details that must persist across interactions.
- **Organizational memory**: Shared FAQs, product catalogs, or policies that every agent should recall.
1. **Context gathering**: pulls the most recent messages already stored for the same `user_id`/`agent_id`/`run_id` scope.
2. **Existing memory retrieval**: embeds the new messages and runs a vector search against memories already in that same scope, to find candidates that might need to change.
3. **Extraction**: a single LLM call compares the new messages against the retrieved candidates and decides, per fact, whether to `ADD`, `UPDATE`, `DELETE`, or leave a memory alone.
## How it compares
Alongside this, both OSS and Platform extract named entities (people, places, organizations) from memory text and use shared entities between memories to boost related results at search time. On Platform, that entity graph is also queryable directly; see <Link href="/platform/features/graph-memory">Graph Memory</Link>. In OSS, entities only affect ranking, there is no separate graph to query.
| Layer | Lifetime | Short or long term | Best for | Trade-offs |
| --- | --- | --- | --- | --- |
| Conversation | Single response | Short-term | Tool execution detail | Lost after the turn finishes |
| Session | Minutes to hours | Short-term | Multi-step flows | Clear it manually when done |
| User | Weeks to forever | Long-term | Personalization | Requires consent/governance |
| Org | Configured globally | Long-term | Shared knowledge | Needs owner to keep current |
<Warning>
Avoid storing secrets or unredacted PII in memories: they are retrievable by design. Encrypt or hash sensitive values before calling `add()`.
Avoid storing secrets or unredacted PII in user or org memories: Mem0 is retrievable by design. Encrypt or hash sensitive values first.
</Warning>
## Put it into practice
- Use the <Link href="/core-concepts/memory-operations/add">Add Memory</Link> guide to persist user preferences.
- Follow <Link href="/platform/advanced-memory-operations">Advanced Memory Operations</Link> to tune metadata and retrieval.
## See it live
- <Link href="/cookbooks/companions/ai-tutor">AI Tutor with Mem0</Link> shows session vs user memories in action.
- <Link href="/cookbooks/operations/support-inbox">Support Inbox with Mem0</Link> demonstrates shared org memory.
{/* DEBUG: verify CTA targets */}
<CardGroup cols={2}>
<Card
title="Explore Memory Operations"
description="Dive into the add/search/update/delete operations next."
description="Dive into the add/search/update/delete concepts next."
icon="circle-check"
href="/core-concepts/memory-operations/add"
/>
<Card
title="Advanced Memory Operations"
description="Tune metadata, filters, and retrieval on Platform."
icon="sliders"
href="/platform/advanced-memory-operations"
/>
<Card
title="AI Tutor Cookbook"
description="See user_id-scoped memory used in a real tutoring agent."
title="See a Cookbook"
description="Apply layered memories inside a customer support agent."
icon="rocket"
href="/cookbooks/companions/ai-tutor"
/>
<Card
title="Support Inbox Cookbook"
description="See user_id-scoped memory used in a support workflow."
icon="inbox"
href="/cookbooks/operations/support-inbox"
/>
</CardGroup>
+24 -74
View File
@@ -325,8 +325,7 @@
"integrations/google-ai-adk",
"integrations/mastra",
"integrations/vercel-ai-sdk",
"integrations/chatdev",
"integrations/strands"
"integrations/chatdev"
]
},
{
@@ -369,7 +368,6 @@
"icon": "terminal",
"pages": [
"integrations/claude-code",
"integrations/claude-ai",
"integrations/cursor",
"integrations/codex",
"integrations/opencode",
@@ -382,8 +380,7 @@
"pages": [
"integrations/openclaw",
"integrations/hermes",
"integrations/pi-agent",
"integrations/dsh-mem0"
"integrations/pi-agent"
]
}
]
@@ -404,6 +401,7 @@
"pages": [
"cookbooks/essentials/building-ai-companion",
"cookbooks/essentials/entity-partitioning-playbook",
"cookbooks/essentials/controlling-memory-ingestion",
"cookbooks/essentials/tagging-and-organizing-memories",
"cookbooks/essentials/exporting-memories"
]
@@ -954,20 +952,24 @@
"destination": "/platform/features/memory-export"
},
{
"source": "/v0x/components/:slug*",
"destination": "/components/:slug*"
"source": "/v0x/components/:a/:b/:c",
"destination": "/components/:a/:b/:c"
},
{
"source": "/v0x/core-concepts/:slug*",
"destination": "/core-concepts/:slug*"
"source": "/v0x/components/:a/:b",
"destination": "/components/:a/:b"
},
{
"source": "/v0x/integrations/:slug*",
"destination": "/integrations/:slug*"
"source": "/v0x/core-concepts/:a/:b",
"destination": "/core-concepts/:a/:b"
},
{
"source": "/v0x/open-source/:slug*",
"destination": "/open-source/:slug*"
"source": "/v0x/integrations/:slug",
"destination": "/integrations/:slug"
},
{
"source": "/v0x/open-source/:slug",
"destination": "/open-source/:slug"
},
{
"source": "/v0x/introduction",
@@ -1042,8 +1044,8 @@
"destination": "/platform/features/graph-memory"
},
{
"source": "/features/:slug*",
"destination": "/platform/features/:slug*"
"source": "/features/:slug",
"destination": "/platform/features/:slug"
},
{
"source": "/platform/features/online-memory",
@@ -1170,7 +1172,7 @@
"destination": "/open-source/overview"
},
{
"source": "/self-hosting/:slug*",
"source": "/self-hosting/:slug",
"destination": "/open-source/overview"
},
{
@@ -1178,7 +1180,7 @@
"destination": "/open-source/overview"
},
{
"source": "/self-hosted/:slug*",
"source": "/self-hosted/:slug",
"destination": "/open-source/overview"
},
{
@@ -1186,7 +1188,7 @@
"destination": "/platform/quickstart"
},
{
"source": "/getting-started/:slug*",
"source": "/getting-started/:slug",
"destination": "/platform/quickstart"
},
{
@@ -1194,7 +1196,7 @@
"destination": "/open-source/setup"
},
{
"source": "/deployment/:slug*",
"source": "/deployment/:slug",
"destination": "/open-source/setup"
},
{
@@ -1206,12 +1208,12 @@
"destination": "/open-source/overview"
},
{
"source": "/oss/:slug*",
"source": "/oss/:slug",
"destination": "/open-source/overview"
},
{
"source": "/concepts/:slug*",
"destination": "/core-concepts/:slug*"
"source": "/concepts/:slug",
"destination": "/core-concepts/:slug"
},
{
"source": "/pricing",
@@ -1248,58 +1250,6 @@
{
"source": "/integrations/keywords",
"destination": "/integrations/respan"
},
{
"source": "/cookbooks/essentials/controlling-memory-ingestion",
"destination": "/platform/features/custom-instructions"
},
{
"source": "/open-source/quickstart",
"destination": "/open-source/overview"
},
{
"source": "/open-source/graph-memory/overview",
"destination": "/open-source/overview"
},
{
"source": "/what-is-mem0",
"destination": "https://mem0.ai/"
},
{
"source": "/platform-vs-oss",
"destination": "/platform/platform-vs-oss"
},
{
"source": "/components/overview",
"destination": "/components/llms/overview"
},
{
"source": "/v0x/core-concepts/memory-operations/add",
"destination": "/core-concepts/memory-operations/add"
},
{
"source": "/v0x/integrations/llama-index",
"destination": "/integrations/llama-index"
},
{
"source": "/api-reference/event/get-event",
"destination": "/api-reference/events/get-event"
},
{
"source": "/api-reference/webhook/get-webhooks",
"destination": "/api-reference/webhook/get-webhook"
},
{
"source": "/api-reference/organization/update-organization-members",
"destination": "/api-reference/organization/update-org-member"
},
{
"source": "/api-reference/organization/delete-project",
"destination": "/api-reference/project/delete-project"
},
{
"source": "/api-reference/entities/get-entities",
"destination": "/api-reference/entities/get-users"
}
]
}
-85
View File
@@ -1,85 +0,0 @@
---
title: Claude.ai
description: "Add persistent memory to Claude.ai with the Mem0 remote MCP server: custom connector setup and native-memory troubleshooting."
---
Add persistent memory to [**Claude.ai**](https://claude.ai) (the hosted web app, not Claude Code) using Mem0's remote MCP server. Claude.ai connects to MCP servers over the internet as **custom connectors**; there is no local plugin or hook system here, since chats run in Anthropic's cloud, not on your machine.
## Prerequisites
1. A Mem0 Platform account: <a href="https://app.mem0.ai?utm_source=oss&utm_medium=integration-claude-ai" rel="nofollow">sign up at app.mem0.ai</a>
2. A Claude.ai account on any plan (free, Pro, Max, Team, or Enterprise)
You don't need an API key up front: the connector uses browser-based sign-in the first time Claude calls a Mem0 tool (see [Signing in](#signing-in)).
## Installation
### Individual accounts (Pro, Max, or free)
1. Go to **Customize > Connectors**
2. Click **+**, then **Add custom connector**
3. Enter the server URL: `https://mcp.mem0.ai/mcp/`
4. Click **Add**
<Note>
Free-tier accounts are limited to one custom connector. Pro, Max, Team, and Enterprise accounts can add multiple.
</Note>
### Team and Enterprise organizations
An organization owner registers the connector once for everyone:
1. Go to **Organization Settings > Connectors**
2. Click **Add**, hover **Custom**, then select **Web**
3. Enter the server URL: `https://mcp.mem0.ai/mcp/`
4. Click **Add**
Members then connect individually: **Customize > Connectors**, find **mem0**, and click **Connect**.
### Enabling in a chat
Connectors are opt-in per conversation. Click the **+** button next to the chat prompt, open **Connectors**, and toggle **mem0** on before you start.
## Signing in
The first time Claude calls a Mem0 tool, your browser opens a sign-in prompt to authorize the connector against your Mem0 account. Approve it once; Claude.ai stores and refreshes the resulting token for you. There's no API key to paste into the connector UI itself.
## What's Included
| Component | Included |
|-----------|:--------:|
| MCP Server (9 memory tools) | Yes |
| Lifecycle Hooks | No (Claude.ai has no local hook system) |
| Mem0 SDK Skill | No (Claude.ai has no local skills directory) |
## Available MCP Tools
| Tool | Description |
|------|-------------|
| `add_memory` | Save text or conversation history for a user/agent |
| `search_memories` | Semantic search across memories with filters |
| `get_memories` | List memories with filters and pagination |
| `get_memory` | Retrieve a specific memory by ID |
| `update_memory` | Overwrite a memory's text by ID |
| `delete_memory` | Delete a single memory by ID |
| `delete_all_memories` | Bulk delete all memories in scope |
| `delete_entities` | Delete a user/agent/app/run entity and its memories |
| `list_entities` | List users/agents/apps/runs stored in Mem0 |
## Troubleshooting
- **Claude never calls the mem0 tools, even though the connector shows as connected**: Claude.ai ships its own native memory (Settings > Capabilities > Memory, on by default), which synthesizes a running summary from your chat history automatically. When both are active, Claude's system prompt tends to favor its built-in memory and rarely reaches for a third-party memory tool on its own. Ask explicitly ("search my mem0 memories for...", "save this to mem0") to force the tool call, or pause Claude's native memory (Settings > Capabilities > Memory > Pause) if you want Mem0 to be the primary memory store for that account.
- **"Connection failed" or the connector won't add**: Confirm the URL is exactly `https://mcp.mem0.ai/mcp/`. Custom connectors reach your MCP server from Anthropic's cloud, not your device, so a localhost URL will never work here.
- **No tools appearing after adding the connector**: Make sure you toggled **mem0** on for the current conversation under the **+ > Connectors** menu; adding a connector doesn't enable it in every chat automatically.
- **Can't add a second connector**: Free-tier accounts are capped at one custom connector; upgrade to Pro/Max/Team/Enterprise for more.
<CardGroup cols={2}>
<Card title="Mem0 MCP Setup" icon="puzzle-piece" href="/platform/mem0-mcp">
Detailed MCP configuration for all clients
</Card>
<Card title="Claude Code Integration" icon="/images/provider-icons/anthropic.svg" href="/integrations/claude-code">
Add Mem0 memory to Claude Code workflows
</Card>
</CardGroup>
<Snippet file="star-on-github.mdx" />
+21
View File
@@ -157,6 +157,27 @@ When installed via the plugin marketplace, Mem0 hooks into Claude Code's lifecyc
What you type is stored as yours. What Claude produces (session summaries and compaction summaries) is stored as the assistant's, so its suggestions never become your stated preferences.
## Self-Hosted Mem0 Server (Optional)
By default the plugin talks to the hosted Mem0 Platform (`https://api.mem0.ai`). To point the REST-backed hooks at your own [self-hosted Mem0 server](/open-source/features/rest-api) instead, set `MEM0_BASE_URL` before starting your session:
```bash
export MEM0_BASE_URL="http://localhost:8000" # your server/ deployment
export MEM0_API_KEY="<your self-hosted API key>"
```
Or persist it in `~/.mem0/settings.json`:
```json
{ "base_url": "http://localhost:8000" }
```
`MEM0_BASE_URL` takes precedence over the settings file; leaving both unset keeps the hosted Platform behavior unchanged. The plugin automatically switches the auth header (`X-API-Key` instead of `Authorization: Token`) and the project-scoping field (`agent_id` instead of `app_id`) to match the self-hosted server's REST API.
<Warning>
The self-hosted `server/` package doesn't implement MCP, so **MCP tools always connect to the hosted Platform** regardless of `MEM0_BASE_URL`. Only the REST-backed lifecycle hooks (session-start banner, auto-import, auto-capture, summaries, and `/mem0:` skill searches) route to your self-hosted server. Automatic coding-category setup also requires the hosted Platform and skips cleanly when `MEM0_BASE_URL` points elsewhere.
</Warning>
## Example Workflow
```text
+3 -29
View File
@@ -91,23 +91,12 @@ To update, run `codex plugin marketplace upgrade` to pull the latest from the Me
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>
## Codex Cloud
[Codex Cloud](https://developers.openai.com/codex/cloud/environments) tasks run setup scripts and the agent in separate phases with different variable scoping:
- **Environment Variables** persist for the full duration of the task, through both the setup script and the agent phase.
- **Secrets** are only available to the setup script; they are wiped before the agent phase starts, so the agent itself cannot read them.
Because the `mem0` MCP server authenticates on every tool call the agent makes (not just during setup), set `MEM0_API_KEY` as an **Environment Variable** in your Codex Cloud environment configuration, not as a Secret. A Secret will let a setup script authenticate but the agent will lose access to `MEM0_API_KEY` once the task phase begins, breaking Mem0 MCP calls.
Lifecycle hooks that shell out to local scripts (Option A) are not applicable in Codex Cloud's ephemeral containers; use Option B (Direct MCP) with `MEM0_API_KEY` set as above.
## What's Included
| Component | Plugin Install | MCP Only |
|-----------|:--------------:|:--------:|
| MCP Server (9 memory tools) | Yes | Yes |
| Lifecycle Hooks | Opt-in (see below) | No |
| Lifecycle Hooks | Yes | No |
| Mem0 SDK Skill | Yes | No |
## Available MCP Tools
@@ -128,22 +117,7 @@ Once installed, the following tools are available in every Codex session:
## Lifecycle Hooks
Unlike Claude Code, Codex has no plugin-host mechanism for auto-wiring hooks from an installed plugin: it only reads hooks from `~/.codex/hooks.json` (or `<repo>/.codex/hooks.json`). Installing the plugin (Option A) does **not** turn hooks on by itself. To enable them, run the bundled installer once against your local clone:
```bash
python3 <path-to-your-clone>/integrations/mem0-plugin/scripts/install_codex_hooks.py
```
This merges Mem0's entries into `~/.codex/hooks.json` and is idempotent (safe to re-run after upgrading). It also requires the `codex_hooks` feature flag in `~/.codex/config.toml`:
```toml
[features]
codex_hooks = true
```
The installer prints a reminder if the flag isn't set. Restart Codex after installing hooks or editing the config. To remove: `python3 .../install_codex_hooks.py --uninstall`.
Once enabled, Mem0 hooks into Codex's lifecycle to automatically manage memory:
When installed via the plugin marketplace, Mem0 hooks into Codex's lifecycle to automatically manage memory:
| Hook | Event | What it does |
|------|-------|-------------|
@@ -181,7 +155,7 @@ You: Add WebSocket support for real-time notification delivery.
- **"Connection failed"**: Verify `MEM0_API_KEY` is set: `echo $MEM0_API_KEY`
- **No tools appearing**: Restart your Codex session after installation
- **Duplicate `mem0` MCP / "tool collision" errors**: You combined Option A with Option B. Remove the `[mcp_servers.mem0]` block from `~/.codex/config.toml`; the plugin registers it automatically
- **Hooks not firing**: Hooks are opt-in and are not installed by the marketplace install itself. Run `scripts/install_codex_hooks.py` (see [Lifecycle Hooks](#lifecycle-hooks)), confirm `codex_hooks = true` is set under `[features]` in `~/.codex/config.toml`, and restart Codex. MCP-only installs (Option B) never include hooks
- **Hooks not firing**: Ensure the plugin is installed via the marketplace (Option A). MCP-only installs do not include hooks
<CardGroup cols={2}>
<Card title="Mem0 MCP Setup" icon="puzzle-piece" href="/platform/mem0-mcp">
+23 -38
View File
@@ -39,10 +39,6 @@ os.environ["SERPER_API_KEY"] = "your-serper-api-key"
client = MemoryClient()
```
<Note>
Newer versions of CrewAI removed the `memory_config={"provider": "mem0"}` shortcut on `Crew(...)` that older guides referenced. CrewAI still offers a native Mem0 path through its `ExternalMemory` API, so that option remains open; check [CrewAI's memory documentation](https://docs.crewai.com/en/concepts/memory) for the shape your version expects. This guide wires Mem0 in explicitly through `MemoryClient` instead, which keeps retrieval under your control and stays valid as CrewAI's memory API changes.
</Note>
## Store User Preferences
Set up initial conversation and preferences storage:
@@ -73,21 +69,9 @@ messages = [
store_user_preferences("crew_user_1", messages)
```
## Retrieve Relevant Memories
Look up what Mem0 already knows about the user before planning a trip, so the crew's output reflects their actual preferences:
```python
def get_user_context(user_id: str, query: str) -> str:
"""Fetch relevant memories and format them for a task description"""
relevant_memories = client.search(query, filters={"user_id": user_id})
memories = [m["memory"] for m in relevant_memories.get("results", [])]
return "\n".join(f"- {memory}" for memory in memories)
```
## Create CrewAI Agent
Define an agent with search capabilities:
Define an agent with memory capabilities:
```python
def create_travel_agent():
@@ -99,60 +83,61 @@ def create_travel_agent():
goal="Plan personalized travel itineraries",
backstory="""You are a seasoned travel planner, known for your meticulous attention to detail.""",
allow_delegation=False,
memory=True,
tools=[search_tool],
)
```
## Define Tasks
Create a task that folds the retrieved memories into its description, so the agent plans around the user's known preferences:
Create tasks for your agent:
```python
def create_planning_task(agent, destination: str, user_context: str):
"""Create a travel planning task personalized with the user's stored preferences"""
def create_planning_task(agent, destination: str):
"""Create a travel planning task"""
return Task(
description=f"""Find places to live, eat, and visit in {destination}.
Known preferences for this user:
{user_context or "No stored preferences yet."}
""",
expected_output=f"A detailed list of places to live, eat, and visit in {destination}, tailored to the user's preferences.",
description=f"""Find places to live, eat, and visit in {destination}.""",
expected_output=f"A detailed list of places to live, eat, and visit in {destination}.",
agent=agent,
)
```
## Set Up Crew
Configure the crew. Mem0 handles persistence outside of CrewAI, so the crew itself does not need `memory=True` or a `memory_config`:
Configure the crew with memory integration:
```python
def setup_crew(agents: list, tasks: list):
"""Set up a crew; memory is managed through Mem0, not CrewAI's memory_config"""
"""Set up a crew with Mem0 memory integration"""
return Crew(
agents=agents,
tasks=tasks,
process=Process.sequential,
memory=True,
memory_config={
"provider": "mem0",
"config": {"user_id": "crew_user_1"},
}
)
```
## Main Execution Function
Implement the main function to run the travel planning system: retrieve context from Mem0, run the crew, then store the new conversation back:
Implement the main function to run the travel planning system:
```python
def plan_trip(destination: str, user_id: str):
# Create agent
travel_agent = create_travel_agent()
user_context = get_user_context(user_id, f"travel preferences for {destination}")
planning_task = create_planning_task(travel_agent, destination, user_context)
# Create task
planning_task = create_planning_task(travel_agent, destination)
# Setup crew
crew = setup_crew([travel_agent], [planning_task])
result = crew.kickoff()
client.add(
[{"role": "user", "content": f"Planned a trip to {destination}."}],
user_id=user_id,
)
return result
# Execute and return results
return crew.kickoff()
# Example usage
if __name__ == "__main__":
-89
View File
@@ -1,89 +0,0 @@
---
title: DeepSeek Harness
description: "Add persistent Mem0 memory to the DeepSeek Harness (Cordis) agent with two native tools: search and add."
---
Add persistent memory to the [**DeepSeek Harness**](https://github.com/deepseek-ai/deepseek-harness) with `@mem0/dsh-mem0`. The Harness agent forgets everything between sessions. This plugin gives it two Mem0-backed tools so recall and writes persist across runs, sharing the same memory bank you already use from Claude Code, Codex, and other agents.
## Overview
The plugin registers two agent-callable tools:
| Tool | Does |
|---|---|
| `search_memory` | Recall facts from Mem0 relevant to a query |
| `add_memory` | Store a fact in Mem0 for future sessions |
Unlike file-based memory plugins, Mem0 is a managed backend: server-side extraction, semantic dedup, and conflict resolution, with the same memory reusable across every agent you connect.
## How it works
A Cordis plugin is a module exporting `apply(ctx, config)`. This one declares `inject = ['tools']` so it waits for the harness tool registry, then registers the two tools via `ctx.tools.register(...)`. When the plugin unmounts, the tools are removed automatically (Cordis revertible effects).
## Prerequisites
1. A Mem0 Platform account and API key:
- <a href="https://app.mem0.ai?utm_source=oss&utm_medium=integration-dsh-mem0" rel="nofollow">Sign up at app.mem0.ai</a>
- <a href="https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=integration-dsh-mem0" rel="nofollow">Get your API key</a> (starts with `m0-`)
2. The DeepSeek Harness installed.
3. Your API key exported in your shell:
<CodeGroup>
```bash zsh
echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.zshrc
source ~/.zshrc
```
```bash bash
echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.bashrc
source ~/.bashrc
```
</CodeGroup>
## Try it locally
1. Build the plugin:
```sh
cd integrations/dsh-mem0
pnpm install
pnpm build
```
2. Point the Harness at it. Copy `cordis.example.yml`, set the absolute path to `dist/index.js` and your `userId`, then load it:
```sh
pnpm dsh web --patch ./integrations/dsh-mem0/cordis.example.yml
```
3. Open the web UI and ask the agent to remember something, then recall it in a later turn.
The `cordis.yml` entry looks like this:
```yaml
- name: "@deepseek-ai/dsh-system-prompt"
- name: "@deepseek-ai/dsh-tools"
- insert:
- id: mem0
name: "/absolute/path/to/integrations/dsh-mem0/dist/index.js"
config:
# apiKey is read from MEM0_API_KEY when omitted here.
userId: "your-user-id"
# host: "https://your-onprem.mem0.ai" # optional: Platform on-prem / dedicated base URL
```
For a Mem0 Platform on-prem or dedicated deployment, point `config.host` at that base URL (defaults to `api.mem0.ai`). `host` is a Platform base-URL override, not a switch to self-hosted Mem0 OSS.
## Configuration
| Field | Required | Default | Notes |
|---|---|---|---|
| `apiKey` | no | `$MEM0_API_KEY` | Mem0 platform API key |
| `userId` | yes | | Default entity that owns the memories |
| `host` | no | `api.mem0.ai` | Platform base URL (on-prem / dedicated) |
Both tools also accept optional per-call `userId`, `agentId`, and `runId` params so a single install can partition memory by entity, agent, or session; when omitted they fall back to the configured `userId`.
## Telemetry
Writes are tagged `source="DEEPSEEK_HARNESS"` so Mem0's backend can attribute usage to this integration.
-69
View File
@@ -1,69 +0,0 @@
---
title: Strands Agents
description: "Add persistent long-term memory to AWS Strands agents with Mem0, as a native MemoryStore that plugs into the agent loop."
---
Integrate [**Mem0**](https://github.com/mem0ai/mem0) with [Strands Agents](https://github.com/strands-agents/sdk-python), AWS's open-source SDK for building AI agents. The [`strands-mem0`](https://github.com/mem0ai/mem0/tree/main/integrations/strands-mem0) package ships a native `MemoryStore`, so recall and writes happen automatically inside the agent loop, not as tool calls the model has to remember.
## Overview
1. A `MemoryStore` the `MemoryManager` drives on every turn: it searches Mem0 and injects the results into the prompt, and writes memory back when extraction is enabled.
2. Server-side extraction: because the store implements `add_messages`, enabling `extraction` routes raw conversation turns to Mem0's own extraction pipeline, with no extra client-side model call.
3. Works with the hosted Mem0 Platform (an API key) or self-hosted Mem0 OSS (a config dict).
## Prerequisites
Before setting up Mem0 with Strands, ensure you have:
1. Installed the required packages:
```bash
pip install strands-mem0
```
2. A valid API key:
- <a href="https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=integration-strands" rel="nofollow">Mem0 API Key</a> (set as `MEM0_API_KEY`)
## Basic Integration Example
Hand a `Mem0MemoryStore` to a `MemoryManager`, and the agent gets automatic recall and memory writes:
```python
import os
from strands import Agent
from strands.memory import MemoryManager
from strands_mem0 import Mem0MemoryStore
os.environ["MEM0_API_KEY"] = "your-mem0-api-key"
# extraction=True routes conversation turns to Mem0's server-side extraction.
store = Mem0MemoryStore(user_id="alex", extraction=True)
agent = Agent(memory_manager=MemoryManager(stores=[store]))
agent("Remember I use Neovim and deploy on Fridays.") # writes memory
print(agent("What editor do I use?")) # recalls it, injected automatically
```
Scope memories with any of `user_id`, `agent_id`, `run_id`, or `app_id` (`app_id` is platform-only). Pass `max_search_results` to bound how many memories are injected per turn.
## Self-hosted Mem0 (OSS)
To run against self-hosted Mem0 instead of the platform, pass a `config` dict:
```python
store = Mem0MemoryStore(
user_id="alex",
extraction=True,
config={
"vector_store": {"provider": "qdrant", "config": {"host": "localhost", "port": 6333}},
},
)
```
## Explicit memory tool
If you want the model to call memory explicitly instead of (or alongside) the automatic store, use the `mem0_memory` tool from `strands-agents-tools`. A store and the tool can share the same Mem0 backend and namespace.
## Learn more
- [strands-mem0 on GitHub](https://github.com/mem0ai/mem0/tree/main/integrations/strands-mem0)
- [Strands Agents documentation](https://strandsagents.com)
+2 -4
View File
@@ -185,7 +185,7 @@ If the user is on a pre-current major (Python < 2, TS < 3, or a Platform call st
## Core Concepts
- [How Mem0 Works](https://docs.mem0.ai/core-concepts/how-it-works) [Both]: Use when explaining the end-to-end pipeline: extraction (ADD-only distillation), storage across vector/entity/history stores, and multi-signal retrieval.
- [Memory Types](https://docs.mem0.ai/core-concepts/memory-types) [Both]: Use when checking which `memory_type` values actually work: `procedural_memory` is implemented, `semantic_memory` and `episodic_memory` are defined in the enum but rejected by validation.
- [Memory Types](https://docs.mem0.ai/core-concepts/memory-types) [Both]: Use when explaining working, factual, episodic, and semantic memory distinctions.
- [Memory Operations - Add](https://docs.mem0.ai/core-concepts/memory-operations/add) [Both]: Use when explaining how `add()` extracts facts, resolves conflicts, and writes to both stores.
- [Memory Operations - Search](https://docs.mem0.ai/core-concepts/memory-operations/search) [Both]: Use when explaining how queries are processed and ranked.
- [Memory Operations - Update](https://docs.mem0.ai/core-concepts/memory-operations/update) [Both]: Use when memories need to be edited in place or reconciled against new info.
@@ -257,17 +257,14 @@ If the user is on a pre-current major (Python < 2, TS < 3, or a Platform call st
- [ChatDev](https://docs.mem0.ai/integrations/chatdev) [Platform]: Use when the user is on ChatDev.
- [Hermes](https://docs.mem0.ai/integrations/hermes) [Both]: Use when the user is on Hermes.
- [Pi Agent](https://docs.mem0.ai/integrations/pi-agent) [Platform]: Use when adding persistent memory to Pi Agent with the Mem0 plugin.
- [DeepSeek Harness](https://docs.mem0.ai/integrations/dsh-mem0) [Platform]: Use when adding persistent memory to the DeepSeek Harness (Cordis) agent via the Mem0 plugin.
- [OpenAI Agents SDK](https://docs.mem0.ai/integrations/openai-agents-sdk) [Platform]: Use when the user is on the OpenAI Agents SDK.
- [Google AI ADK](https://docs.mem0.ai/integrations/google-ai-adk) [Platform]: Use when the user is on Google's Agent Development Kit.
- [Mastra](https://docs.mem0.ai/integrations/mastra) [Platform]: Use when the user is on Mastra (TypeScript).
- [OpenClaw](https://docs.mem0.ai/integrations/openclaw) [Both]: Use when wiring Mem0 into Claude Code or editors via OpenClaw.
- [Vercel AI SDK](https://docs.mem0.ai/integrations/vercel-ai-sdk) [Both]: Use when the user is on the Vercel AI SDK.
- [Strands Agents](https://docs.mem0.ai/integrations/strands) [Both]: Use when the user is on AWS Strands and wants a native MemoryStore.
### AI Coding Tools
- [Claude Code](https://docs.mem0.ai/integrations/claude-code) [Both]: Use when wiring memory into Claude Code.
- [Claude.ai](https://docs.mem0.ai/integrations/claude-ai) [Platform]: Use when connecting Mem0 to Claude.ai (the hosted web app) via a custom remote MCP connector, or when Claude's native memory seems to be crowding out mem0 tool calls.
- [Cursor](https://docs.mem0.ai/integrations/cursor) [Platform]: Use when wiring memory into Cursor.
- [Codex](https://docs.mem0.ai/integrations/codex) [Platform]: Use when wiring memory into Codex / other editor assistants.
- [OpenCode](https://docs.mem0.ai/integrations/opencode) [Platform]: Use when wiring memory into OpenCode.
@@ -297,6 +294,7 @@ If the user is on a pre-current major (Python < 2, TS < 3, or a Platform call st
### Essentials
- [Building an AI Companion](https://docs.mem0.ai/cookbooks/essentials/building-ai-companion) [Both]: Use when starting a companion app from scratch.
- [Partition Memories by Entity](https://docs.mem0.ai/cookbooks/essentials/entity-partitioning-playbook) [Both]: Use when isolating multi-tenant memories.
- [Controlling Memory Ingestion](https://docs.mem0.ai/cookbooks/essentials/controlling-memory-ingestion) [Both]: Use when deciding what to store and what to skip.
- [Tagging and Organizing Memories](https://docs.mem0.ai/cookbooks/essentials/tagging-and-organizing-memories) [Both]: Use when memory taxonomy matters.
- [Exporting Memories](https://docs.mem0.ai/cookbooks/essentials/exporting-memories) [Both]: Use when backing up or migrating memory data.
@@ -6,10 +6,6 @@ icon: "filter"
Enhanced metadata filtering in Mem0 lets you run complex queries across memory metadata. Combine comparisons, logical operators, and wildcard matches to zero in on the exact memories your agent needs.
<Info>
This page covers the self-hosted `Memory` / `AsyncMemory` grammar. Sibling top-level keys are implicitly ANDed on both self-hosted and the hosted Platform API, so a flat filter like `{"user_id": "alice", "category": "work"}` works without wrapping it in `AND` on either. Two real differences remain: the `nin` operator documented below is not part of the Platform contract, and Platform validates every top-level key against a fixed allow-list, rejecting anything else with a 400. See [Memory Filters (Platform)](/platform/features/v2-memory-filters) for the hosted grammar.
</Info>
---
## Feature anatomy
@@ -23,7 +19,7 @@ This page covers the self-hosted `Memory` / `AsyncMemory` grammar. Sibling top-l
| `lt` / `lte` | Less than / less than or equal | Cap numeric values (e.g., ratings, timestamps). |
| `in` / `nin` | In list / not in list | Pre-approve or block sets of values without chaining multiple filters. |
| `contains` / `icontains` | Case-sensitive / case-insensitive substring match | Scan text fields for keywords. |
| `*` | Wildcard | Match regardless of value. Exact semantics (field-must-exist vs. no-op) vary by vector store, see [Wildcard matching](#wildcard-matching). |
| `*` | Wildcard | Require that a field exists, regardless of value. |
| `AND` / `OR` / `NOT` | Combine filters | Build logic trees so multiple conditions work together. |
</Accordion>
</AccordionGroup>
@@ -115,7 +111,7 @@ results = m.search(
### Wildcard matching
Match any value for a field, handy when the mere presence of a field matters.
Allow any value for a field while still requiring the field to exist: handy when the mere presence of a field matters.
```python
# Match any value for a field
@@ -128,18 +124,10 @@ results = m.search(
)
```
<Warning>
Wildcard semantics differ by vector store. pgvector requires the key to exist in the payload (a real "field exists" check). Qdrant and Chroma have no native "field exists" filter, so `*` is a no-op there: the field condition is dropped and every record passes, including ones where the field is missing entirely. Do not rely on `*` to exclude records with a missing field unless you have confirmed your store's behavior in `mem0/vector_stores/<provider>.py`.
</Warning>
### Logical combinations
Combine filters with `AND`, `OR`, and `NOT` to express complex decision trees. Nest logical operators to encode multi-branch workflows.
<Warning>
The examples on this page use `search()`. `get_all()` accepts the same entity and comparison-operator filters, but the `AND` / `OR` / `NOT` wrapper is only translated at the `search()` layer before it reaches the vector store. Whether it also works on `get_all()` depends on your vector store: Qdrant recognizes raw `AND` / `OR` / `NOT` keys natively, but pgvector does not, so a logical wrapper passed to `get_all()` on pgvector silently matches nothing. Stick to `search()` for logical trees, or use plain sibling keys (implicitly ANDed) with `get_all()`.
</Warning>
```python
# Logical AND
results = m.search(
@@ -257,18 +245,25 @@ avoid_filters = {
When you reorder filters so indexed fields come first (`good_filters` example), queries typically return faster than the `avoid_filters` pattern where expensive text searches run before simple checks.
</Info>
Vector store support varies widely. This table reflects what each provider's filter-translation code (`mem0/vector_stores/<provider>.py`) actually implements, confirm before shipping if you use a store not listed:
Vector store support varies. Confirm operator coverage before shipping:
| Store | `eq`/`ne`/`gt`/`gte`/`lt`/`lte`/`in`/`nin` | `contains`/`icontains` | `AND`/`OR`/`NOT` | `*` wildcard |
| --- | --- | --- | --- | --- |
| Qdrant | Full support (`in`/`nin` must be a list, or the Qdrant client raises a validation error) | Yes | Yes, nested | No-op: matches every record regardless of whether the field is present |
| pgvector | Full support (`in`/`nin` must be a list, or the call raises `ValueError`) | Yes, via SQL `LIKE`/`ILIKE` | Yes, nested | Requires the key to exist in the payload |
| Chroma | Full support | No: silently falls back to equality | Yes, one level of nesting | No-op: the filter is dropped |
| Pinecone | Full support | Not implemented | Not implemented: filters are only ANDed field-by-field | Not implemented: `"*"` is matched as the literal string `"*"`, not a wildcard |
| Weaviate | Not implemented: only `user_id`, `agent_id` and `run_id` are filtered on, as exact equality. Every other key, including all metadata, is silently dropped | Not implemented | Not implemented: only an implicit AND across the three supported keys | Not implemented |
<AccordionGroup>
<Accordion title="Qdrant">
Full comparison, list, and logical support. Handles deeply nested boolean logic efficiently.
</Accordion>
<Accordion title="Chroma">
Equality and basic comparisons only. Limited nesting: break large trees into smaller calls.
</Accordion>
<Accordion title="Pinecone">
Comparisons plus `in`/`nin`. Text operators are constrained; rely on tags where possible.
</Accordion>
<Accordion title="Weaviate">
Full operator coverage with advanced text filters. Best option when you need hybrid text + metadata queries.
</Accordion>
</AccordionGroup>
<Warning>
If an operator is unsupported, most stores silently ignore it or fall back to equality rather than raising an error. Test filters against your actual store instead of assuming operator parity with Qdrant.
If an operator is unsupported, most stores silently ignore that branch. Add validation before execution so you can fall back to simpler queries instead of returning empty results.
</Warning>
### Migrate from earlier filters
@@ -432,7 +427,4 @@ except ValueError as e:
<Card title="Tag and Organize Memories" icon="tag" href="/cookbooks/essentials/tagging-and-organizing-memories">
Practice building workflows that label and retrieve memories with clear metadata filters.
</Card>
<Card title="Memory Filters (Platform)" icon="cloud" href="/platform/features/v2-memory-filters">
Using the hosted API instead? See the allow-listed top-level fields, the missing `nin` operator, and Platform-only fields like `created_at` and `memory_ids`.
</Card>
</CardGroup>
+12 -2
View File
@@ -76,8 +76,18 @@ await client.add(messages, { userId: "alice" })
```json Output
{
"event_id": "evt-uuid",
"status": "PENDING"
"results": [
{
"memory": "Name is Alice",
"event": "ADD",
"id": "7ae113a3-3cb5-46e9-b6f7-486c36391847"
},
{
"memory": "Likes large pizza with toppings including cherry tomatoes, black olives, green spinach, yellow bell peppers, diced ham, and sliced mushrooms",
"event": "ADD",
"id": "56545065-7dee-4acf-8bf2-a5b2535aabb3"
}
]
}
```
</CodeGroup>
+9 -4
View File
@@ -82,11 +82,11 @@ client.add(messages, { userId: "user1", timestamp: unixTimestamp })
```
```bash cURL
curl -X POST "https://api.mem0.ai/v3/memories/add/" \
curl -X POST "https://api.mem0.ai/v1/memories/" \
-H "Authorization: Token your-api-key" \
-H "Content-Type: application/json" \
-d '{
"messages": [{"role": "user", "content": "Travelling to SF"}],
"messages": [{"role": "user", "content": "I'm travelling to SF"}],
"user_id": "user1",
"timestamp": 1721577600
}'
@@ -94,8 +94,13 @@ curl -X POST "https://api.mem0.ai/v3/memories/add/" \
```json Output
{
"event_id": "evt-uuid",
"status": "PENDING"
"results": [
{
"id": "a1b2c3d4-e5f6-4g7h-8i9j-k0l1m2n3o4p5",
"data": {"memory": "Travelling to SF"},
"event": "ADD"
}
]
}
```
+8 -57
View File
@@ -29,46 +29,6 @@ Filters use a nested JSON structure with logical operators at the root:
}
```
A single bare condition needs no wrapper:
```python
# Works: one condition, no wrapper needed
filters = {"user_id": "user_123"}
```
Sibling top-level keys in a flat object are accepted too: the API implicitly ANDs them, so the flat form and the explicit `AND` form are equivalent:
```python
# Works: sibling keys are implicitly ANDed
filters = {
"user_id": "user_123",
"categories": {"in": ["finance"]}
}
# Equivalent, explicit form
filters = {
"AND": [
{"user_id": "user_123"},
{"categories": {"in": ["finance"]}}
]
}
```
Reach for an explicit `AND`, `OR`, or `NOT` wrapper when you need `OR` or `NOT` semantics, or when you need to nest conditions. What the API does reject is an unrecognized top-level key:
```python
# Rejected: unknown top-level key
filters = {"user_id": "user_123", "bogus_key": "z"}
# 400: Top-level key must be a logical operator or an allowed field:
# ['AND', 'OR', 'NOT', 'user_id', 'agent_id', 'app_id', 'run_id',
# 'created_at', 'updated_at', 'timestamp', 'expiration_date', 'text',
# 'categories', 'metadata', 'keywords_search', 'memory_ids', 'keywords']
```
<Callout type="info" icon="cloud" color="#00A8FF">
Both the hosted Platform API and the self-hosted OSS SDK implicitly AND sibling top-level keys, so the flat and explicit `AND` forms behave the same way on either. The real differences: Platform validates each top-level key against a fixed allow-list and returns a 400 for anything else, and Platform has no `nin` operator. See [Enhanced Metadata Filtering](/open-source/features/metadata-filtering) for the OSS grammar.
</Callout>
## Available fields and operators
### Entity fields
@@ -82,15 +42,14 @@ Both the hosted Platform API and the self-hosted OSS SDK implicitly AND sibling
### Time fields
| Field | Operators | Example |
|-------|-----------|---------|
| `created_at` | `gt`, `gte`, `lt`, `lte`, `ne`, `in`. Explicit `eq` is rejected, pass a bare value instead | `{"created_at": {"gte": "2024-01-01"}}` |
| `updated_at` | `gt`, `gte`, `lt`, `lte`, `ne`, `in`. Explicit `eq` is rejected, pass a bare value instead | `{"updated_at": {"lt": "2024-12-31"}}` |
| `timestamp` | `gt`, `gte`, `lt`, `lte`, `ne`, `in`. Explicit `eq` is rejected, pass a bare value instead | `{"timestamp": {"gt": "2024-01-01"}}` |
| `expiration_date` | `gt`, `gte`, `lt`, `lte`, `ne`, `in`. Explicit `eq` is rejected, pass a bare value instead | `{"expiration_date": {"lte": "2026-12-31"}}` |
| `created_at` | `gt`, `gte`, `lt`, `lte`, `eq`, `ne` | `{"created_at": {"gte": "2024-01-01"}}` |
| `updated_at` | `gt`, `gte`, `lt`, `lte`, `eq`, `ne` | `{"updated_at": {"lt": "2024-12-31"}}` |
| `timestamp` | `gt`, `gte`, `lt`, `lte`, `eq`, `ne` | `{"timestamp": {"gt": "2024-01-01"}}` |
### Content fields
| Field | Operators | Example |
|-------|-----------|---------|
| `categories` | `in` (takes a list, matches any category in it), `contains` (case-insensitive). `eq` and `ne` are rejected | `{"categories": {"in": ["finance"]}}` |
| `categories` | `eq`, `ne`, `in` (matches any category in the list), `contains` (case-insensitive) | `{"categories": {"in": ["finance"]}}` |
| `metadata` | `eq`, `ne`, `contains` | `{"metadata": {"key": "value"}}` |
| `keywords` | `contains` (case-sensitive), `icontains` (case-insensitive) | `{"keywords": {"icontains": "invoice"}}` |
@@ -107,14 +66,6 @@ The `*` wildcard matches any non-null value. Records with null values for that f
Use operator keywords exactly as shown (`eq`, `ne`, `gte`, etc.). SQL-style symbols such as `>=` or `!=` are rejected by the Platform API.
</Callout>
<Callout type="warning" icon="exclamation-triangle" color="#E74C3C">
There is no `nin` ("not in") operator on Platform. It is an OSS-only operator; see [Enhanced Metadata Filtering](/open-source/features/metadata-filtering). To exclude a set of values on Platform, wrap an `in` clause in `NOT`:
```python
{"NOT": {"categories": {"in": ["spam", "test"]}}}
```
</Callout>
## Common filter patterns
Use these ready-made filters to target typical retrieval scenarios without rebuilding logic from scratch.
@@ -377,7 +328,7 @@ Level up foundational patterns with compound filters that coordinate entity scop
## Best practices
<Callout type="tip" icon="lightbulb" color="#26A17B">
The root does not have to be `AND`, `OR`, or `NOT`. A bare filter like `{"user_id": "alice"}` works on its own, and several top-level keys in one flat filter are implicitly ANDed. Wrap conditions in a logical operator when you need OR/NOT semantics or nested grouping.
The root does not have to be `AND`, `OR`, or `NOT`. A bare filter like `{"user_id": "alice"}` works on its own; wrap conditions in a logical operator only when you need to combine more than one.
</Callout>
<Callout type="tip" icon="lightbulb" color="#26A17B">
@@ -441,7 +392,7 @@ A filter object with both an `AND` key and a sibling `OR` key at the same level
<AccordionGroup>
<Accordion title="Do I need AND/OR/NOT?">
No. A bare filter like `{"user_id": "u1"}` works on its own, and several top-level keys in one flat filter like `{"user_id": "u1", "agent_id": "a1"}` are implicitly ANDed. Reach for `AND`, `OR`, or `NOT` when you need OR/NOT semantics or nested grouping, not merely to combine conditions.
No. A bare filter like `{"user_id": "u1"}` works on its own. Add `AND`, `OR`, or `NOT` only when you need to combine more than one condition.
</Accordion>
<Accordion title="What does * match?">
@@ -470,8 +421,8 @@ A filter object with both an `AND` key and a sibling `OR` key at the same level
"AND": [
{"user_id": "user_123"},
{"OR": [
{"categories": {"in": ["finance"]}},
{"categories": {"in": ["health"]}}
{"categories": "finance"},
{"categories": "health"}
]}
]
}
+19 -12
View File
@@ -28,15 +28,11 @@ npx mcp-add \
--name mem0-mcp \
--type http \
--url "https://mcp.mem0.ai/mcp" \
--clients "claude code,cursor,windsurf,vscode,opencode"
--clients "claude,claude code,cursor,windsurf,vscode,opencode"
```
`mcp-add` is a helper that writes the Mem0 server into each client's own MCP config file, so you do not have to edit them by hand. Name only the clients you actually use. If you would rather see the change yourself, every client's manual config is under [Client-specific setup](#client-specific-setup).
<Note>
Claude Desktop is not in the list above: it rejects the `mcp-add` command. Add it through Settings instead, see [Claude Desktop](#client-specific-setup) below.
</Note>
Restart each client afterwards so it picks up the new server.
## Signing in
@@ -85,14 +81,25 @@ You can also configure individual clients:
<AccordionGroup>
<Accordion title="Claude Desktop">
Claude Desktop does not support the `mcp-add` command, it rejects the server as "not a valid MCP server." Add the Mem0 server through the Settings UI instead:
```bash
npx mcp-add \
--name mem0-mcp \
--type http \
--url "https://mcp.mem0.ai/mcp" \
--clients "claude"
```
1. Open Claude Desktop and go to **Settings > Connectors**.
2. Click **Add custom connector**.
3. Enter a name (for example `mem0-mcp`) and the URL `https://mcp.mem0.ai/mcp`.
4. Save, then restart Claude Desktop.
The first time you use a Mem0 tool, Claude Desktop opens a browser window to sign in, see [Signing in](#signing-in).
Or manually add to your Claude Desktop configuration (`claude_desktop_config.json`):
```json
{
"mcpServers": {
"mem0-mcp": {
"type": "http",
"url": "https://mcp.mem0.ai/mcp"
}
}
}
```
</Accordion>
<Accordion title="Claude Code">
+56 -52
View File
@@ -6,7 +6,7 @@ icon: "code-compare"
## Which Mem0 is right for you?
Mem0 offers two ways to add memory to your AI applications. Both run the same core extraction and retrieval logic; the Platform adds hosting, a small set of v3-only capabilities, and management surfaces that OSS does not have.
Mem0 offers two powerful ways to add memory to your AI applications. Choose based on your priorities:
<CardGroup cols={2}>
<Card
@@ -16,7 +16,7 @@ Mem0 offers two ways to add memory to your AI applications. Both run the same co
>
**Managed, hassle-free**
Get started in 5 minutes with our hosted solution. No vector store, LLM, or embedder to configure.
Get started in 5 minutes with our hosted solution. Perfect for fast iteration and production apps.
</Card>
<Card
@@ -32,85 +32,89 @@ Mem0 offers two ways to add memory to your AI applications. Both run the same co
---
## What's the same
The core memory loop is identical on both: `add`, `search`, `get`, `get_all`, `update`, `delete`, `delete_all`, and per-memory `history` all exist on the self-hosted `Memory`/`AsyncMemory` classes and on the hosted `MemoryClient`/`AsyncMemoryClient`. Both support:
- **Entity scoping** by `user_id`, `agent_id`, and `run_id`
- **Filter grouping**: both accept `AND`/`OR`/`NOT` wrappers, both implicitly AND a flat multi-key filter like `{"user_id": "alice", "agent_id": "a1"}`, and both accept `*` as a wildcard value. Which fields you may filter on, and which operators each field accepts, differ (see below)
- **Entity-aware ranking**: both extract entities from memory text and use shared entities to boost related results at search time
- **Multimodal input**, **memory expiration** (`expiration_date`), **reranking**, **procedural memory** (Python), and **custom extraction instructions** (`custom_instructions`)
- Python and JavaScript SDKs, plus a REST API (self-hosted via `server/`, or hosted)
## What's actually different
## Feature Comparison
<AccordionGroup>
<Accordion title="Hosting & infrastructure" icon="server">
<Accordion title="Setup & Getting Started" icon="rocket">
| Feature | Platform | Open Source |
|---------|----------|-------------|
| **Vector store, LLM, embedder** | Run and tuned by Mem0 | You provision, configure, and pay for each |
| **Scaling & availability** | Managed | Your responsibility |
| **Web dashboard** | `app.mem0.ai` | Not included |
| **Time to first memory** | Minutes (API key only) | Depends on your vector DB / LLM setup |
| **Time to first memory** | 5 minutes | 15-30 minutes |
| **Infrastructure needed** | None | Vector DB + Python/Node env |
| **API key setup** | One environment variable | Configure LLM + embedder + vector DB |
| **Maintenance** | Fully managed by Mem0 | Self-managed |
</Accordion>
<Accordion title="Entity scoping & workspace structure" icon="layer-group">
<Accordion title="Core Memory Features" icon="brain">
| Feature | Platform | Open Source |
|---------|----------|-------------|
| **Scoping identifiers** | `user_id`, `agent_id`, `run_id`, plus `app_id` for app/tenant separation | `user_id`, `agent_id`, `run_id` only, no `app_id` |
| **Organizations & projects** | Multi-org, multi-project, with member roles ([API reference](/api-reference/organization/get-org)) | No org/project concept; a single local config |
| **Project-wide event feed** | `GET /v1/events/` lists recent add/search/delete events per org and project, usable for dashboards, alerting, or audit trails | Only per-memory `history(memory_id)`, no project-wide event log |
See [Entity-Scoped Memory](/platform/features/entity-scoped-memory) for the full `app_id` model.
| **User & agent memories** | ✅ | ✅ |
| **Smart deduplication** | ✅ | ✅ |
| **Semantic search** | ✅ | ✅ |
| **Memory updates** | ✅ | ✅ |
| **Multi-language SDKs** | Python, JavaScript | Python, JavaScript |
</Accordion>
<Accordion title="Search-time ranking (v3-only)" icon="sparkles">
<Accordion title="Advanced Capabilities" icon="sparkles">
| Feature | Platform | Open Source |
|---------|----------|-------------|
| **Graph Memory** | Native, always-on graph over extracted entities; connections feed directly into the ranking `score` ([details](/platform/features/graph-memory)) | Removed. OSS previously connected external graph stores (Neo4j, Memgraph, Kuzu, Apache AGE); that integration was dropped when the v3 pipeline landed. OSS still extracts entities and uses them to boost ranking, but there is no queryable graph and no `relations` field |
| **Memory Decay** | Opt-in per project; reinforces recently-used memories and gently dampens stale ones at search time ([details](/platform/features/memory-decay)) | Not supported. Passing `decay` raises `"The decay parameter is not supported by the OSS Memory SDK."` |
| **Temporal Reasoning** | Boosts memories whose event dates match the time expressed in a query (`timestamp` / `reference_date`) ([details](/platform/features/temporal-reasoning)) | Not supported. Both parameters raise a "not supported by the OSS Memory SDK" error |
| **Dream (background consolidation)** | Continuously synthesizes patterns, supersedes outdated facts, and merges duplicates per user ([details](/platform/features/dream)) | Not available |
| **Multimodal support** | ✅ | ✅ |
| **Custom categories** | ✅ | Limited |
| **Advanced retrieval** | ✅ | ✅ |
| **Temporal reasoning** | ✅ (v3) | ❌ |
| **Memory decay** | ✅ (v3) | ❌ |
| **Graph memory** | ✅ Built-in | ✅ External graph store |
| **Memory filters v2** | ✅ | ⚠️ (via metadata) |
| **Webhooks** | ✅ | ❌ |
| **Memory export** | ✅ | ❌ |
</Accordion>
<Accordion title="Configuration & data operations" icon="sliders">
<Accordion title="Infrastructure & Scaling" icon="server">
| Feature | Platform | Open Source |
|---------|----------|-------------|
| **Custom categories** | Set per project or per `add` call ([details](/platform/features/custom-categories)) | Not supported. `Memory.add()` has no `custom_categories` parameter, and OSS project updates are rejected outright |
| **Webhooks** | Project-scoped HTTP callbacks on memory and ingest events ([details](/platform/features/webhooks)) | Not available |
| **Memory Export** | Schema-driven structured export jobs over filtered memories ([details](/platform/features/memory-export)) | Not available |
| **Batch operations** | `batch_update` and `batch_delete` apply up to 1000 memories per call | Not available. Loop over the single-memory `update`/`delete` calls |
| **Feedback** | `feedback(memory_id, ...)` records `POSITIVE`, `NEGATIVE`, or `VERY_NEGATIVE` signals against a retrieved memory ([details](/platform/features/feedback-mechanism)) | Not available |
| **Summaries** | `get_summary(filters)` returns a generated summary over the matching memories | Not available |
| **Filterable fields** | Top-level filter keys come from a fixed allowlist: `user_id`, `agent_id`, `app_id`, `run_id`, `created_at`, `updated_at`, `timestamp`, `expiration_date`, `categories`, `metadata`, `keywords`, `memory_ids`, plus the `AND`/`OR`/`NOT` operators and a few endpoint-specific extras. Any other key is rejected with a `400` ([details](/platform/features/v2-memory-filters)) | Any metadata key is filterable directly, with no allowlist |
| **Comparison operators** | Depends on the field: `metadata` accepts `eq`, `ne`, `contains`; `categories` accepts `contains`, `in`; the date fields accept the range operators | The filter layer accepts the full set on any key: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `contains`, `icontains`. What actually runs depends on the vector store you configured, so confirm operator coverage for yours ([details](/open-source/features/metadata-filtering)) |
| **Hosting** | Managed by Mem0 | Self-hosted |
| **Auto-scaling** | ✅ | Manual |
| **High availability** | ✅ Built-in | DIY setup |
| **Vector DB choice** | Managed | 20+ stores: Qdrant, Pinecone, Chroma, Weaviate, Milvus, pgvector |
| **LLM choice** | Managed (optimized) | 15+ providers: OpenAI, Anthropic, Gemini, Groq, Ollama, Together |
| **Data residency** | US (expandable) | Your choice |
</Accordion>
<Accordion title="Support" icon="life-ring">
| Channel | Platform | Open Source |
|---------|----------|-------------|
| **Community & maintainers** | Discord, GitHub Discussions, direct calls with the founders | Same: Discord, GitHub Discussions, direct calls with the founders |
<Accordion title="Pricing & Cost" icon="dollar-sign">
| Aspect | Platform | Open Source |
|--------|----------|-------------|
| **License** | Usage-based pricing | Apache 2.0 (free) |
| **Infrastructure costs** | Included in pricing | You pay for VectorDB + LLM + hosting |
| **Support** | Included | Community + GitHub |
| **Best for** | Fast iteration, production apps | Cost-sensitive, custom requirements |
</Accordion>
Support channels are currently the same for both. If you need something contractual (a support SLA, for example), ask before assuming it exists: it is not documented as a Platform benefit today.
<Accordion title="Development & Integration" icon="code">
| Feature | Platform | Open Source |
|---------|----------|-------------|
| **REST API** | ✅ | ✅ (self-hosted server) |
| **Python SDK** | ✅ | ✅ |
| **JavaScript SDK** | ✅ | ✅ |
| **Framework integrations** | LangChain, CrewAI, LlamaIndex, and 20+ more | Same |
| **Dashboard** | ✅ Web-based | ❌ |
| **Analytics** | ✅ Built-in | DIY |
</Accordion>
</AccordionGroup>
User Profiles are not listed above. The feature is still being finalized internally, so this page does not present it as an available Platform benefit.
---
## Decision Guide
**Choose Platform if you want:**
- Zero infrastructure: no vector store, LLM, or embedder to provision or tune.
- The v3-only ranking features: Graph Memory, Memory Decay, Temporal Reasoning, and Dream.
- App-level and org/project-level scoping, plus webhooks, memory export, and custom categories.
- Fast time to market: get your AI app with memory live in hours, not weeks.
- Production-ready hosting: auto-scaling, high availability, and managed infrastructure.
- Built-in analytics: track memory usage, query patterns, and user engagement through the dashboard.
- Advanced features: webhooks, memory export, custom categories, and priority support.
**Choose Open Source if you need:**
- Full data control: host everything on your own infrastructure.
- Custom configuration: your own vector DB, LLM provider, and embedder ([25 vector stores](/components/vectordbs/overview), [18 LLM providers](/components/llms/overview), [11 embedders](/components/embedders/overview) at the time of writing).
- Extensibility: modify the codebase, add custom providers, and contribute back.
- Cost optimization: local LLMs (Ollama), self-hosted vector DBs, no usage-based billing.
- Full data control: host everything on your infrastructure with complete data residency.
- Custom configuration: choose your own vector DB, LLM provider, embedder, and deployment strategy.
- Extensibility: modify the codebase, add custom features, and contribute back to the community.
- Cost optimization: use local LLMs (Ollama), self-hosted vector DBs, and optimize for your use case.
---
+3 -2
View File
@@ -25,7 +25,7 @@ Cookbooks are narrative tutorials. They start with a real problem, show the brok
## ✅ COPY THIS: Content Skeleton
Paste the block below into a new cookbook, then replace all placeholders. Remove any section you don't need **only after** the happy path works.
````mdx
```mdx
---
title: [Cookbook title: action oriented]
description: [1 sentence outcome]
@@ -175,7 +175,7 @@ Call out the most common mistake or edge case for this layer.
href="#next-link"
/>
</CardGroup>
````
```
---
@@ -189,6 +189,7 @@ Call out the most common mistake or edge case for this layer.
- [ ] Linked related docs (cookbooks, guides, reference) in Next Steps.
Stick to the skeleton above. If you need to deviate, document the rationale in the PR so we can update the template for everyone else.
```
## Browse Other Templates
+2 -5
View File
@@ -9,12 +9,10 @@ Agent and editor integrations. Each subdirectory is self-contained: its own `pac
| `mem0-plugin/` | Claude Code / Cursor / Codex plugin | none | none | pytest |
| `mem0-plugin/.opencode-plugin/` | `@mem0/opencode-plugin` | Bun | none | tsc type-check |
| `pi-agent-plugin/` | `@mem0/pi-agent-plugin` | tsup | none | vitest |
| `dsh-mem0/` | `@mem0/dsh-mem0` | tsup (ESM) | none | vitest |
| `n8n-nodes-mem0/` | `@mem0/n8n-nodes-mem0` | tsc | ESLint (n8n-nodes-base) | none |
| `zapier-mem0/` | `@mem0/zapier` | tsc | none | offline unit tests + `zapier validate` |
| `strands-mem0/` | `strands-mem0` (PyPI) | hatch | Ruff + mypy | pytest |
pnpm everywhere except `.opencode-plugin/` (Bun) and `strands-mem0/` (Python: pip / hatch). Never npm, never yarn.
pnpm everywhere except `.opencode-plugin/`, which uses Bun. Never npm, never yarn.
## Commands
@@ -41,10 +39,9 @@ Run the type check after every TypeScript change: `pnpm run typecheck` or `tsc -
- **`vercel-ai-sdk/`** wraps the Vercel AI SDK through a `createMem0` provider. Integrations for AI-SDK repos go through this wrapper, not raw `MemoryClient`.
- **`mem0-plugin/`** connects Claude Code, Cursor, and Codex to the MCP server at `mcp.mem0.ai` and installs lifecycle hooks for automatic memory capture. Exposes 9 MCP tools: `add_memory`, `search_memories`, `get_memories`, `get_memory`, `update_memory`, `delete_memory`, `delete_all_memories`, `delete_entities`, `list_entities`.
- **`openclaw/`**, **`pi-agent-plugin/`**, **`dsh-mem0/`** are editor and agent plugins with the same shape. `dsh-mem0/` registers Mem0 search/add tools as a native DeepSeek Harness (Cordis) plugin.
- **`openclaw/`**, **`pi-agent-plugin/`** are editor and agent plugins with the same shape.
- **`n8n-nodes-mem0/`** is an n8n community node: add, search, get, update, delete.
- **`zapier-mem0/`** is a Zapier Platform CLI app: add, search, get, delete. It deploys to Zapier, not npm, so it is **not** in the release router. Deploy it with `gh workflow run zapier-mem0-cd.yml --ref main` (needs the `ZAPIER_DEPLOY_KEY` secret).
- **`strands-mem0/`** is a native Strands `MemoryStore` (Python, published to PyPI as `strands-mem0`). It plugs into the Strands `MemoryManager` for automatic recall and server-side extraction, over the hosted Mem0 platform or self-hosted Mem0 OSS. The package lives under `strands-mem0/python/`.
## Adding an integration
-7
View File
@@ -1,7 +0,0 @@
# The DeepSeek Harness SDK (@deepseek-ai/dsh-tools) declares the rest of the
# harness runtime (dsh-llm, dsh-agent, dsh-scope, ...) as peer dependencies.
# The host harness provides those at runtime; this adapter only needs cordis +
# dsh-tools to build and type-check. Auto-installing the full peer graph pulls
# unpublished internal packages (e.g. dsh-type-meta), so keep it off.
auto-install-peers=false
package-manager-strict-version=false
-201
View File
@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [2023] [Taranjeet Singh]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-60
View File
@@ -1,60 +0,0 @@
# dsh-mem0
[Mem0](https://mem0.ai) long-term memory as a native [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (Cordis) plugin.
It gives a Harness agent two memory tools backed by the Mem0 SDK, so recall and writes persist across sessions:
| Tool | Does |
|---|---|
| `search_memory` | Recall facts from Mem0 relevant to a query |
| `add_memory` | Store a fact in Mem0 for future sessions |
Unlike the local/file-based memory plugins in the ecosystem, Mem0 is a managed backend: server-side extraction, semantic dedup and conflict resolution, and the same memory bank reusable across Harness, Claude Code, Codex, and other agents.
## How it works
A Cordis plugin is a module exporting `apply(ctx, config)`. This one declares `inject = ['tools']` so it waits for the harness tool registry, then registers the two tools via `ctx.tools.register(defineTool(...))`. When the plugin unmounts, the tools are removed automatically (Cordis revertible effects).
```
[ mem0ai SDK ] <-- managed memory, owned by Mem0
|
[ dsh-mem0: apply(ctx) -> ctx.tools.register(...) ] <-- this package
|
[ DeepSeek Harness ] <-- the agent, loaded via cordis.yml
```
## Try it locally
1. Build the plugin:
```sh
cd integrations/dsh-mem0
pnpm install
pnpm build
```
2. Set your Mem0 key:
```sh
export MEM0_API_KEY=...
```
3. Point Harness at it. Copy `cordis.example.yml`, set the absolute path to `dist/index.js` and your `userId`, then:
```sh
pnpm dsh web --patch ./integrations/dsh-mem0/cordis.example.yml
```
4. Open http://127.0.0.1:3080 and ask the agent to remember something, then recall it in a later turn.
For a Mem0 Platform on-prem or dedicated deployment, point `config.host` at that base URL (defaults to `api.mem0.ai`). `host` is a Platform base-URL override — it is not a switch to self-hosted Mem0 OSS, whose server exposes a different API surface.
## Configuration
| Field | Required | Default | Notes |
|---|---|---|---|
| `apiKey` | no | `$MEM0_API_KEY` | Mem0 platform API key |
| `userId` | yes | | Entity that owns the memories |
| `host` | no | `api.mem0.ai` | Platform base URL (on-prem / dedicated) |
## Telemetry
Writes are tagged `source="DEEPSEEK_HARNESS"` so Mem0's backend can attribute usage to this integration. For it to surface by name (rather than bucketing into `OTHERS`), `DEEPSEEK_HARNESS` must be present in the backend's `KNOWN_EVENT_SOURCES` allowlist, a one-line platform change matching the existing `ZAPIER` / `STRANDS` sources.
## Status
Developer preview. Tracks the DeepSeek Harness v0.1 plugin API (`@deepseek-ai/cordis`, `@deepseek-ai/dsh-tools`), which is young and moving; pin versions once it stabilizes. Auto-capture (store turns without an explicit tool call) and auto-recall (inject memory into the prompt at assembly) are planned once the harness session/assembly event API is confirmed.
-19
View File
@@ -1,19 +0,0 @@
# Example DeepSeek Harness config that loads dsh-mem0 alongside the built-in
# tools plugin. Load with:
#
# pnpm dsh web --patch ./integrations/dsh-mem0/cordis.example.yml
#
# The tools plugin (and its system-prompt dependency) must be present, because
# the memory tools contribute schemas the system prompt renders.
- name: "@deepseek-ai/dsh-system-prompt"
- name: "@deepseek-ai/dsh-tools"
- insert:
- id: mem0
# Absolute path to the built plugin (run `pnpm build` first), or point
# at src/index.ts when running through tsx during development.
name: "/absolute/path/to/mem0/integrations/dsh-mem0/dist/index.js"
config:
# apiKey is read from the MEM0_API_KEY env var when omitted here.
userId: "your-user-id"
# host: "https://your-onprem.mem0.ai" # optional: Platform on-prem / dedicated base URL
-59
View File
@@ -1,59 +0,0 @@
{
"name": "@mem0/dsh-mem0",
"version": "0.1.0",
"description": "Mem0 long-term memory as a native DeepSeek Harness (Cordis) plugin.",
"type": "module",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/mem0ai/mem0",
"directory": "integrations/dsh-mem0"
},
"keywords": [
"deepseek-harness",
"dsh",
"dsh-plugin",
"cordis",
"mem0",
"memory",
"agent"
],
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"publishConfig": {
"access": "public"
},
"files": [
"dist",
"src",
"README.md",
"LICENSE"
],
"scripts": {
"build": "tsup",
"test": "vitest run",
"test:watch": "vitest",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"mem0ai": "^3.0.7"
},
"peerDependencies": {
"@deepseek-ai/cordis": "*",
"@deepseek-ai/dsh-tools": "*"
},
"devDependencies": {
"@deepseek-ai/cordis": "4.0.1",
"@deepseek-ai/dsh-tools": "0.0.1-rc.1",
"@types/node": "^22.15.0",
"tsup": "^8.5.0",
"typescript": "^5.6.0",
"vitest": "^4.1.7"
}
}
File diff suppressed because it is too large Load Diff
-69
View File
@@ -1,69 +0,0 @@
/**
* Compact, token-cheap rendering of Mem0 results for the model context.
*
* Mirrors the format used by the sibling Mem0 plugins
* (integrations/pi-agent-plugin/src/memory/formatting.ts) so a memory reads the
* same way across every harness: `[category] text (age) [mem0:id]`. Dumping the
* raw search envelope instead would spend most of the tokens on JSON scaffolding.
*/
export interface MemoryLike {
id: string;
memory?: string;
categories?: string[];
createdAt?: Date | string;
}
export function formatAge(date: Date | string): string {
const d = typeof date === "string" ? new Date(date) : date;
const ms = Date.now() - d.getTime();
const minutes = Math.floor(ms / 60_000);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days}d ago`;
}
export function formatMemoryCompact(mem: MemoryLike): string {
const cat = mem.categories?.[0] ?? "uncategorized";
const age = mem.createdAt ? ` (${formatAge(mem.createdAt)})` : "";
return `[${cat}] ${mem.memory ?? "(empty)"}${age} [mem0:${mem.id}]`;
}
export function formatMemoryList(memories: MemoryLike[]): string {
if (memories.length === 0) return "No memories found.";
return memories
.map((m, i) => `${i + 1}. ${formatMemoryCompact(m)}`)
.join("\n");
}
/**
* One-line confirmation for a write.
*
* `client.add` hits the async `/v3/memories/add/` endpoint, which returns
* `{ event_id, status: "PENDING" }` — extraction runs server-side *after* the
* call returns, so the extracted memories are not in this response. Report the
* write as queued in that case; only render a list when the backend actually
* returns memories (older / OSS shapes).
*/
export function formatAddResult(result: unknown): string {
const items: MemoryLike[] = Array.isArray(result)
? (result as MemoryLike[])
: ((result as { results?: MemoryLike[] } | null)?.results ??
(result ? [result as MemoryLike] : []));
const pending = items.find(
(r) => (r as { status?: string }).status === "PENDING",
) as { eventId?: string; event_id?: string } | undefined;
if (pending) {
// The SDK camel-cases response keys (event_id -> eventId); accept either.
const id = pending.eventId ?? pending.event_id;
const evt = id ? ` (event ${id})` : "";
return `Memory queued for background extraction${evt}; it will be searchable shortly.`;
}
if (items.length === 0) return "Memory stored.";
const noun = items.length === 1 ? "memory" : "memories";
return `Stored ${items.length} ${noun}:\n${formatMemoryList(items)}`;
}
-138
View File
@@ -1,138 +0,0 @@
/**
* dsh-mem0: Mem0 long-term memory as a native DeepSeek Harness (Cordis) plugin.
*
* Registers two agent-callable tools backed by the Mem0 SDK:
* - `search_memory` recalls facts relevant to a query
* - `add_memory` stores a fact for future sessions
*
* A plugin is a Cordis module that exports `apply(ctx, config)`. Declaring
* `inject = ['tools']` holds the plugin until the harness tool registry exists;
* tools registered via `ctx.tools.register(...)` are auto-unregistered when the
* plugin unmounts (Cordis revertible effects).
*/
import type { Context } from "@deepseek-ai/cordis";
import { defineTool } from "@deepseek-ai/dsh-tools";
import { MemoryClient } from "mem0ai";
import { formatMemoryList, formatAddResult } from "./formatting.ts";
import { truncateOutput } from "./output.ts";
import { resolveSearchFilters, resolveAddParams } from "./scoping.ts";
export const name = "mem0";
export const inject = ["tools"];
// Tags writes so Mem0's backend attributes them to this integration in
// telemetry. The backend keeps recognized values via its KNOWN_EVENT_SOURCES
// allowlist; unknown values bucket into "OTHERS", so "DEEPSEEK_HARNESS" must be
// added to that allowlist for usage to surface by name (a one-line backend PR,
// same pattern as the ZAPIER / STRANDS sources).
const SOURCE = "DEEPSEEK_HARNESS";
const DEFAULT_SEARCH_LIMIT = 10;
export interface Config {
/** Mem0 API key. Defaults to the MEM0_API_KEY env var. */
apiKey?: string;
/** Default entity that owns the memories (Mem0 user scope). */
userId: string;
/** Optional Mem0 Platform base-URL override (on-prem / dedicated); defaults to api.mem0.ai. Not a switch to self-hosted OSS. */
host?: string;
}
// Both tools return a single text string; the render is identical, so lift it
// into one shared declaration instead of repeating the block per tool.
const textOutput = {
schema: { type: "string" } as const,
render: (_args: unknown, value: string) => [
{ type: "text" as const, text: value },
],
};
// Optional per-call scoping params, shared by both tools. A single harness
// install can serve more than one entity, so the model may override the
// mount-time default per call (see scoping.ts).
const scopeParams = {
userId: {
type: "string",
description:
"Entity that owns the memory. Defaults to the plugin's configured userId; set this only to read or write another user's memories.",
},
agentId: {
type: "string",
description: "Optional agent scope, to partition memories by agent.",
},
runId: {
type: "string",
description: "Optional run/session scope, to partition memories by session.",
},
} as const;
export function apply(ctx: Context, config: Config): void {
const apiKey = config.apiKey ?? process.env.MEM0_API_KEY;
if (!apiKey) {
throw new Error("dsh-mem0: set config.apiKey or the MEM0_API_KEY env var");
}
const userId = config.userId;
if (!userId) {
throw new Error("dsh-mem0: config.userId is required");
}
const client = new MemoryClient({
apiKey,
...(config.host ? { host: config.host } : {}),
});
// Recall. The platform rejects top-level entity params on search, so scope
// goes inside `filters` (unlike add below, which takes them top-level).
ctx.tools.register(
defineTool({
name: "search_memory",
description:
"Search the user's long-term Mem0 memory for facts relevant to a query. Use proactively before answering anything that may depend on what the user told you earlier.",
parameters: {
query: { type: "string", description: "What to recall.", required: true },
limit: {
type: "integer",
description: `Max results to return (default ${DEFAULT_SEARCH_LIMIT}).`,
},
...scopeParams,
},
output: textOutput,
async execute({ query, limit, userId: u, agentId, runId }) {
const filters = resolveSearchFilters({ userId: u, agentId, runId }, userId);
try {
const topK = limit && limit > 0 ? limit : DEFAULT_SEARCH_LIMIT;
const { results } = await client.search(query, { filters, topK });
return truncateOutput(formatMemoryList(results ?? []));
} catch (err) {
return `search_memory failed: ${err instanceof Error ? err.message : String(err)}`;
}
},
}),
);
// Write. `source` tags the memory for telemetry attribution.
ctx.tools.register(
defineTool({
name: "add_memory",
description:
"Store a fact in the user's long-term Mem0 memory for later sessions. Extraction runs asynchronously server-side, so a stored fact may take a moment to become searchable; do not immediately search to confirm the write.",
parameters: {
text: { type: "string", description: "The fact to remember.", required: true },
...scopeParams,
},
output: textOutput,
async execute({ text, userId: u, agentId, runId }) {
const addParams = resolveAddParams({ userId: u, agentId, runId }, userId);
try {
const result = await client.add([{ role: "user", content: text }], {
...addParams,
source: SOURCE,
});
return truncateOutput(formatAddResult(result));
} catch (err) {
return `add_memory failed: ${err instanceof Error ? err.message : String(err)}`;
}
},
}),
);
}
-33
View File
@@ -1,33 +0,0 @@
/**
* Hard cap on tool output before it reaches the model context.
*
* Same guard the sibling plugins apply (200 lines / 50KB, see
* integrations/pi-agent-plugin/src/memory/tools.ts): a large recall or a wide
* result set can otherwise flood the context window in a single tool call.
*/
export const MAX_OUTPUT_LINES = 200;
export const MAX_OUTPUT_BYTES = 50_000;
export function truncateOutput(text: string): string {
const lines = text.split("\n");
if (lines.length <= MAX_OUTPUT_LINES && text.length <= MAX_OUTPUT_BYTES) {
return text;
}
const kept = lines.slice(0, MAX_OUTPUT_LINES);
let result = kept.join("\n");
const byteCapped = result.length > MAX_OUTPUT_BYTES;
if (byteCapped) {
result = result.slice(0, MAX_OUTPUT_BYTES);
}
const dropped = lines.length - kept.length;
const reasons: string[] = [];
if (dropped > 0) reasons.push(`showing ${kept.length} of ${lines.length} lines`);
if (byteCapped) reasons.push(`cut at ${Math.floor(MAX_OUTPUT_BYTES / 1000)}KB`);
if (reasons.length > 0) {
result += `\n\n[Output truncated: ${reasons.join(", ")}]`;
}
return result;
}
-53
View File
@@ -1,53 +0,0 @@
/**
* Per-call memory scoping.
*
* The plugin is mounted with one default `userId`, but a single harness install
* can serve more than one entity, so both tools accept optional `userId` /
* `agentId` / `runId` params that override the mount-time default per call.
* A missing or blank param falls back to the configured user.
*
* The two call sites need different key casing, and it is deliberate rather than
* incidental: search passes scope inside `filters`, sent to the platform raw, so
* it must be snake_case; add takes the entity params top-level, through the
* SDK's camel->snake converter, so it must be camelCase. Keeping the split
* explicit (like integrations/pi-agent-plugin/src/memory/scoping.ts) means the
* asymmetry is visible in the code, not load-bearing on a converter no-op.
*/
export interface EntityParams {
userId?: string;
agentId?: string;
runId?: string;
}
const clean = (v: string | undefined) => v?.trim() || undefined;
/** Search: snake_case, spread into `filters` and passed to the platform raw. */
export function resolveSearchFilters(
params: EntityParams,
defaultUserId: string,
): Record<string, string> {
const filters: Record<string, string> = {
user_id: clean(params.userId) ?? defaultUserId,
};
const agentId = clean(params.agentId);
if (agentId) filters.agent_id = agentId;
const runId = clean(params.runId);
if (runId) filters.run_id = runId;
return filters;
}
/** Add: camelCase, top-level params run through the SDK's camel->snake converter. */
export function resolveAddParams(
params: EntityParams,
defaultUserId: string,
): Record<string, string> {
const out: Record<string, string> = {
userId: clean(params.userId) ?? defaultUserId,
};
const agentId = clean(params.agentId);
if (agentId) out.agentId = agentId;
const runId = clean(params.runId);
if (runId) out.runId = runId;
return out;
}
-143
View File
@@ -1,143 +0,0 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
// Offline mock of the Mem0 SDK so these tests never touch the network.
const mockSearch = vi.fn();
const mockAdd = vi.fn();
vi.mock("mem0ai", () => ({
MemoryClient: class {
search = mockSearch;
add = mockAdd;
},
}));
// The real `@deepseek-ai/dsh-tools` runtime transitively imports harness peer
// packages the host provides at runtime but which aren't installed here. For
// these unit tests we only need `defineTool` to hand back the definition it was
// given, so the registered tool's `execute`/`name` can be exercised directly.
vi.mock("@deepseek-ai/dsh-tools", () => ({
defineTool: (options: unknown) => options,
}));
import { apply, type Config } from "../src/index.ts";
interface RegisteredTool {
name: string;
execute(args: unknown, exec: unknown): Promise<unknown>;
}
function applyAndCollect(config: Config): Map<string, RegisteredTool> {
const tools = new Map<string, RegisteredTool>();
const ctx = {
tools: { register: (t: RegisteredTool) => tools.set(t.name, t) },
};
apply(ctx as never, config);
return tools;
}
let savedKey: string | undefined;
beforeEach(() => {
savedKey = process.env.MEM0_API_KEY;
mockSearch.mockReset();
mockAdd.mockReset();
});
afterEach(() => {
if (savedKey === undefined) delete process.env.MEM0_API_KEY;
else process.env.MEM0_API_KEY = savedKey;
});
describe("apply() config validation", () => {
it("throws when no apiKey is set and MEM0_API_KEY is absent", () => {
delete process.env.MEM0_API_KEY;
expect(() => applyAndCollect({ userId: "u" } as Config)).toThrow(/apiKey|MEM0_API_KEY/);
});
it("throws when userId is missing", () => {
expect(() => applyAndCollect({ apiKey: "k", userId: "" } as Config)).toThrow(/userId/);
});
it("registers both memory tools", () => {
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
expect([...tools.keys()].sort()).toEqual(["add_memory", "search_memory"]);
});
});
describe("search_memory tool", () => {
it("returns a formatted list scoped to the configured user", async () => {
mockSearch.mockResolvedValue({
results: [{ id: "m1", memory: "Likes tea", categories: ["preference"] }],
});
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
const out = await tools.get("search_memory")!.execute({ query: "drink" }, {});
expect(out).toContain("Likes tea");
expect(out).toContain("[mem0:m1]");
expect(mockSearch).toHaveBeenCalledWith("drink", {
filters: { user_id: "u" },
topK: 10,
});
});
it("honors a per-call userId override and limit", async () => {
mockSearch.mockResolvedValue({ results: [] });
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
await tools.get("search_memory")!.execute({ query: "x", userId: "alice", limit: 3 }, {});
expect(mockSearch).toHaveBeenCalledWith("x", {
filters: { user_id: "alice" },
topK: 3,
});
});
it("returns a graceful failure line instead of rejecting on error", async () => {
mockSearch.mockRejectedValue(new Error("network down"));
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
const out = await tools.get("search_memory")!.execute({ query: "x" }, {});
expect(out).toContain("search_memory failed");
expect(out).toContain("network down");
});
});
describe("add_memory tool", () => {
it("reports the write as queued on the async PENDING response, with camelCase scope + source", async () => {
// The real /v3/memories/add/ response — not an array of memories. The SDK
// camel-cases response keys, so it surfaces as `eventId`, not `event_id`.
mockAdd.mockResolvedValue({ eventId: "evt-123", status: "PENDING" });
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
const out = await tools.get("add_memory")!.execute({ text: "remember this" }, {});
expect(out).toContain("queued");
expect(out).toContain("evt-123");
expect(out).not.toContain("No new distinct memory");
expect(mockAdd).toHaveBeenCalledWith(
[{ role: "user", content: "remember this" }],
{ userId: "u", source: "DEEPSEEK_HARNESS" },
);
});
it("renders a list when the backend returns memories", async () => {
mockAdd.mockResolvedValue([{ id: "m1", memory: "Fact" }]);
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
const out = await tools.get("add_memory")!.execute({ text: "x" }, {});
expect(out).toContain("Stored 1 memory");
expect(out).toContain("[mem0:m1]");
});
it("returns a graceful failure line on error", async () => {
mockAdd.mockRejectedValue(new Error("boom"));
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
const out = await tools.get("add_memory")!.execute({ text: "x" }, {});
expect(out).toContain("add_memory failed");
expect(out).toContain("boom");
});
});
@@ -1,71 +0,0 @@
import { describe, it, expect } from "vitest";
import {
formatAge,
formatMemoryCompact,
formatMemoryList,
formatAddResult,
} from "../src/formatting.ts";
describe("formatAge", () => {
it("formats minutes, hours, and days", () => {
expect(formatAge(new Date(Date.now() - 30 * 60_000))).toBe("30m ago");
expect(formatAge(new Date(Date.now() - 3 * 3_600_000))).toBe("3h ago");
expect(formatAge(new Date(Date.now() - 5 * 86_400_000))).toBe("5d ago");
});
});
describe("formatMemoryCompact", () => {
it("renders one line with category, text, and id", () => {
const line = formatMemoryCompact({
id: "abc-123",
memory: "User prefers dark mode",
categories: ["preference"],
createdAt: new Date(),
});
expect(line).toContain("[preference]");
expect(line).toContain("User prefers dark mode");
expect(line).toContain("[mem0:abc-123]");
});
it("falls back to uncategorized and (empty)", () => {
expect(formatMemoryCompact({ id: "x" })).toContain("[uncategorized]");
expect(formatMemoryCompact({ id: "x" })).toContain("(empty)");
});
});
describe("formatMemoryList", () => {
it("numbers multiple memories", () => {
const output = formatMemoryList([
{ id: "id-1", memory: "Fact one", categories: ["insight"] },
{ id: "id-2", memory: "Fact two", categories: ["convention"] },
]);
expect(output).toContain("1.");
expect(output).toContain("2.");
});
it("returns a plain message when there are no memories", () => {
expect(formatMemoryList([])).toBe("No memories found.");
});
});
describe("formatAddResult", () => {
it("reports queued for the async PENDING response, with the event id", () => {
// SDK camel-cases response keys, so the real shape is `eventId`.
const out = formatAddResult({ eventId: "evt-9", status: "PENDING" });
expect(out).toContain("queued");
expect(out).toContain("evt-9");
});
it("reports the stored count when the backend returns memories", () => {
expect(formatAddResult([{ id: "1", memory: "A" }])).toContain("Stored 1 memory");
expect(formatAddResult([{ id: "1" }, { id: "2" }])).toContain("Stored 2 memories");
});
it("unwraps a { results: [...] } envelope", () => {
expect(formatAddResult({ results: [{ id: "1", memory: "A" }] })).toContain("Stored 1 memory");
});
it("handles an empty result", () => {
expect(formatAddResult([])).toBe("Memory stored.");
});
});
@@ -1,30 +0,0 @@
import { describe, it, expect } from "vitest";
import { truncateOutput, MAX_OUTPUT_LINES } from "../src/output.ts";
describe("truncateOutput", () => {
it("passes small output through untouched", () => {
expect(truncateOutput("a\nb\nc")).toBe("a\nb\nc");
});
it("caps output at MAX_OUTPUT_LINES and appends a notice", () => {
const many = Array.from({ length: MAX_OUTPUT_LINES + 50 }, (_, i) => `line ${i}`).join("\n");
const out = truncateOutput(many);
expect(out.split("\n").length).toBeLessThanOrEqual(MAX_OUTPUT_LINES + 3);
expect(out).toContain("[Output truncated:");
expect(out).toContain(`of ${MAX_OUTPUT_LINES + 50} lines`);
});
it("caps output that is few lines but very large by bytes", () => {
const huge = "x".repeat(60_000);
const out = truncateOutput(huge);
expect(out.length).toBeLessThan(huge.length);
expect(out).toContain("[Output truncated:");
});
it("reports both reasons when the line cap and the byte cap fire together", () => {
const wide = Array.from({ length: MAX_OUTPUT_LINES + 50 }, () => "x".repeat(300)).join("\n");
const out = truncateOutput(wide);
expect(out).toContain(`of ${MAX_OUTPUT_LINES + 50} lines`);
expect(out).toContain("cut at 50KB");
});
});
@@ -1,44 +0,0 @@
import { describe, it, expect } from "vitest";
import { resolveSearchFilters, resolveAddParams } from "../src/scoping.ts";
describe("resolveSearchFilters (snake_case, for filters)", () => {
it("falls back to the configured default userId", () => {
expect(resolveSearchFilters({}, "default-user")).toEqual({ user_id: "default-user" });
});
it("lets a per-call userId override the default", () => {
expect(resolveSearchFilters({ userId: "alice" }, "default-user")).toEqual({
user_id: "alice",
});
});
it("treats a blank/whitespace userId as absent and falls back", () => {
expect(resolveSearchFilters({ userId: " " }, "default-user")).toEqual({
user_id: "default-user",
});
});
it("includes snake_case agent/run scope only when provided", () => {
expect(
resolveSearchFilters({ userId: "alice", agentId: "agent-1", runId: "run-9" }, "d"),
).toEqual({ user_id: "alice", agent_id: "agent-1", run_id: "run-9" });
});
it("omits blank agent/run scope", () => {
const f = resolveSearchFilters({ agentId: " ", runId: "run-9" }, "default");
expect(f).toEqual({ user_id: "default", run_id: "run-9" });
expect(f).not.toHaveProperty("agent_id");
});
});
describe("resolveAddParams (camelCase, for top-level add params)", () => {
it("uses camelCase keys and falls back to the default userId", () => {
expect(resolveAddParams({}, "default-user")).toEqual({ userId: "default-user" });
});
it("includes camelCase agent/run scope only when provided", () => {
expect(
resolveAddParams({ userId: "alice", agentId: "agent-1", runId: "run-9" }, "d"),
).toEqual({ userId: "alice", agentId: "agent-1", runId: "run-9" });
});
});
-22
View File
@@ -1,22 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"lib": ["ES2022"],
"declaration": true,
"outDir": "dist",
"rootDir": "src",
"strict": true,
"types": ["node"],
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"allowImportingTsExtensions": true,
"noEmit": true
},
"include": ["src"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
-12
View File
@@ -1,12 +0,0 @@
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/index.ts"],
format: ["esm"],
dts: true,
sourcemap: true,
clean: true,
// The harness runtime and the Mem0 SDK are provided by the host / installed
// separately; keep them out of the bundle.
external: [/^node:/, /^@deepseek-ai\//, "mem0ai", /^mem0ai\//],
});
@@ -1,6 +1,6 @@
{
"name": "mem0",
"version": "0.2.15",
"version": "0.2.13",
"description": "Persistent memory for Claude Code. Remembers decisions, patterns, and preferences across sessions.",
"author": {
"name": "Mem0",
@@ -1,6 +1,6 @@
{
"name": "mem0",
"version": "0.2.15",
"version": "0.2.13",
"description": "Persistent memory for Codex. Remembers decisions, patterns, and preferences across sessions.",
"author": {
"name": "Mem0",
@@ -1,6 +1,6 @@
{
"name": "mem0",
"version": "0.2.15",
"version": "0.2.13",
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search using the Mem0 Platform MCP server.",
"author": {
"name": "Mem0",
@@ -13,5 +13,5 @@
"keywords": ["mem0", "memory", "mcp", "personalization", "semantic-search"],
"skills": "./skills/",
"hooks": "./hooks/cursor-hooks.json",
"mcpServers": "./.cursor-mcp.json"
"mcpServers": ".cursor-mcp.json"
}
+24 -4
View File
@@ -48,6 +48,29 @@ Humans setting up Mem0 by hand should continue with Step 1 below.
# Should print: m0-your-api-key
```
## Self-hosted Mem0 server (optional)
By default the plugin talks to the hosted Mem0 Platform (`https://api.mem0.ai`). To point the REST-backed hooks at your own [self-hosted Mem0 server](https://docs.mem0.ai/open-source/features/rest-api) instead, set `MEM0_BASE_URL` before starting your session:
```bash
export MEM0_BASE_URL="http://localhost:8000" # your server/ deployment
export MEM0_API_KEY="<your self-hosted API key>"
```
Or persist it in `~/.mem0/settings.json`:
```json
{ "base_url": "http://localhost:8000" }
```
`MEM0_BASE_URL` (env var) takes precedence over the settings file; leaving both unset keeps the hosted Platform behavior unchanged.
**What works self-hosted:** every lifecycle hook that reads or writes memories over REST — the session-start banner and recent-activity timeline, auto-import of `CLAUDE.md`/`AGENTS.md`, auto-capture, pre-compact and session summaries, and `/mem0:` skill searches. The plugin automatically switches the auth header (`X-API-Key` instead of `Authorization: Token`) and the project-scoping field (`agent_id` instead of `app_id`, since the self-hosted server has no `app_id` concept) — no other config needed.
**What doesn't work self-hosted (skips cleanly, no crash):**
- **MCP tools** (`add_memory`, `search_memories`, etc., and the `/mem0:` skills that call them through MCP) — the self-hosted `server/` package doesn't implement MCP, so the MCP server connection always talks to the hosted Platform regardless of `MEM0_BASE_URL`. Use the REST-backed hooks above for self-hosted memory capture and the `/mem0:` search skills instead.
- **Automatic coding-category setup** (`auto_setup_categories.py`, `setup_coding_categories.py`) — `project.update(custom_categories=...)` is a Platform-only SDK call with no self-hosted equivalent. Both scripts detect a self-hosted `MEM0_BASE_URL` and skip with a log message rather than failing.
## Step 2: Install the plugin
Choose one of the options below. All require `MEM0_API_KEY` to be set first (see above).
@@ -100,16 +123,13 @@ This points Codex at the repo's `.agents/plugins/marketplace.json`, which refere
python3 ~/codex-plugins/mem0-source/integrations/mem0-plugin/scripts/install_codex_hooks.py
```
This merges six event handlers into `~/.codex/hooks.json` with absolute paths pointing into your clone:
This merges three entries into `~/.codex/hooks.json` with absolute paths pointing into your clone:
| Event | What it does |
|-------|--------------|
| `SessionStart` | Loads prior memories as bootstrap context |
| `UserPromptSubmit` | Injects relevant memories into the prompt |
| `PreToolUse` (3 handlers) | Blocks MEMORY.md writes; enforces `user_id`/`app_id` on mem0 tool calls; scans files being read for relevant memory context |
| `PostToolUse` (2 handlers) | Tracks stats, scans bash errors for related memories |
| `Stop` | Reminds the agent to persist learnings at turn end |
| `PreCompact` | Stores a summary before the context is compacted |
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.
+3 -3
View File
@@ -7,13 +7,13 @@
{
"name": "mem0-ensure-deps",
"type": "command",
"command": "ANTIGRAVITY_PLUGIN_ROOT=${extensionPath} CLAUDE_PLUGIN_ROOT=${extensionPath} bash ${extensionPath}/scripts/ensure_deps.sh || true",
"command": "ANTIGRAVITY_PLUGIN_ROOT=${extensionPath} CLAUDE_PLUGIN_ROOT=${extensionPath} bash ${extensionPath}/scripts/ensure_deps.sh 2>/dev/null || true",
"timeout": 60
},
{
"name": "mem0-session-start",
"type": "command",
"command": "ANTIGRAVITY_PLUGIN_ROOT=${extensionPath} CLAUDE_PLUGIN_ROOT=${extensionPath} bash ${extensionPath}/scripts/on_session_start.sh || true"
"command": "ANTIGRAVITY_PLUGIN_ROOT=${extensionPath} CLAUDE_PLUGIN_ROOT=${extensionPath} bash ${extensionPath}/scripts/on_session_start.sh 2>/dev/null || true"
}
]
}
@@ -24,7 +24,7 @@
{
"name": "mem0-user-prompt",
"type": "command",
"command": "ANTIGRAVITY_PLUGIN_ROOT=${extensionPath} CLAUDE_PLUGIN_ROOT=${extensionPath} bash ${extensionPath}/scripts/on_user_prompt.sh || true",
"command": "ANTIGRAVITY_PLUGIN_ROOT=${extensionPath} CLAUDE_PLUGIN_ROOT=${extensionPath} bash ${extensionPath}/scripts/on_user_prompt.sh 2>/dev/null || true",
"timeout": 8
}
]
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "mem0",
"name": "mem0",
"version": "0.1.7",
"version": "0.1.6",
"description": "Persistent semantic memory for Antigravity agents. Cross-session, user-level recall via the Mem0 Platform MCP server. 16 slash commands, lifecycle hooks for auto-capture and metadata enforcement.",
"author": { "name": "Mem0", "email": "support@mem0.ai" },
"publisher": "mem0ai",
+135
View File
@@ -0,0 +1,135 @@
"""Translate memory API calls between the hosted Mem0 Platform and a self-hosted server.
The hosted Platform speaks versioned /v1 and /v3 paths, `Authorization: Token`
auth, and scopes writes/searches with a top-level app_id. The self-hosted OSS
server (server/) speaks unversioned /memories and /search paths, `X-API-Key`
auth, and has no app_id concept -- agent_id is the closest first-class
scoping dimension there. Every helper below branches on resolve_base_url().
"""
from __future__ import annotations
import json
import urllib.error
import urllib.request
from _identity import DEFAULT_BASE_URL, resolve_base_url
FETCH_TIMEOUT = 5
SELF_HOSTED_COUNT_CAP = 100
def is_self_hosted(base_url: str | None = None) -> bool:
base_url = base_url if base_url is not None else resolve_base_url()
return base_url.rstrip("/") != DEFAULT_BASE_URL
def auth_headers(api_key: str) -> dict[str, str]:
if is_self_hosted():
return {"X-API-Key": api_key}
return {"Authorization": f"Token {api_key}"}
def project_field() -> str:
"""Body/filter key for project scoping: agent_id (self-hosted) or app_id (hosted Platform)."""
return "agent_id" if is_self_hosted() else "app_id"
def add_url() -> str:
base_url = resolve_base_url()
if is_self_hosted(base_url):
return f"{base_url}/memories"
return f"{base_url}/v3/memories/add/"
def search_url() -> str:
base_url = resolve_base_url()
if is_self_hosted(base_url):
return f"{base_url}/search"
return f"{base_url}/v3/memories/search/"
def delete_url(memory_id: str) -> str:
base_url = resolve_base_url()
if is_self_hosted(base_url):
return f"{base_url}/memories/{memory_id}"
return f"{base_url}/v1/memories/{memory_id}/"
def fetch_recent(api_key: str, user_id: str, project_id: str, top_k: int = 1, global_search: bool = False) -> list:
"""Fetch recent memories for a scope. Returns [] on any error; never raises.
Hosted Platform lists via POST with an AND/OR filters body. The self-hosted
server has no such endpoint -- it only supports GET /memories with flat
user_id/agent_id/top_k query params, so global (cross-user) listing there
is best-effort and may return nothing if the caller lacks admin rights.
"""
base_url = resolve_base_url()
headers = auth_headers(api_key)
try:
if is_self_hosted(base_url):
headers = dict(headers)
params = f"top_k={top_k}"
if not global_search:
params = f"user_id={user_id}&{project_field()}={project_id}&{params}"
req = urllib.request.Request(f"{base_url}/memories?{params}", headers=headers, method="GET")
else:
if global_search:
filters = {"OR": [{"user_id": "*"}]}
else:
filters = {"AND": [{"user_id": user_id}, {project_field(): project_id}]}
body = json.dumps({"filters": filters}).encode()
headers = dict(headers)
headers["Content-Type"] = "application/json"
req = urllib.request.Request(
f"{base_url}/v3/memories/?page=1&page_size={top_k}", data=body, headers=headers, method="POST"
)
with urllib.request.urlopen(req, timeout=FETCH_TIMEOUT) as r:
data = json.loads(r.read())
if isinstance(data, list):
return data
if isinstance(data, dict):
return data.get("results", [])
return []
except (urllib.error.URLError, OSError, json.JSONDecodeError, ValueError):
return []
def count_memories(api_key: str, user_id: str, project_id: str, global_search: bool = False) -> str:
"""Total memory count for a scope, as a string ("?" on any error).
Hosted Platform's list endpoint reports a true total in its `count` field
even with page_size=1. The self-hosted server has no such total, so its
count is a best-effort len() over a capped GET (accurate up to SELF_HOSTED_COUNT_CAP).
"""
base_url = resolve_base_url()
headers = auth_headers(api_key)
try:
if is_self_hosted(base_url):
params = f"top_k={SELF_HOSTED_COUNT_CAP}"
if not global_search:
params = f"user_id={user_id}&{project_field()}={project_id}&{params}"
req = urllib.request.Request(f"{base_url}/memories?{params}", headers=headers, method="GET")
with urllib.request.urlopen(req, timeout=FETCH_TIMEOUT) as r:
data = json.loads(r.read())
results = data.get("results", []) if isinstance(data, dict) else data
return str(len(results)) if isinstance(results, list) else "?"
if global_search:
filters = {"OR": [{"user_id": "*"}]}
else:
filters = {"AND": [{"user_id": user_id}, {project_field(): project_id}]}
body = json.dumps({"filters": filters}).encode()
headers = {**headers, "Content-Type": "application/json"}
req = urllib.request.Request(
f"{base_url}/v3/memories/?page=1&page_size=1", data=body, headers=headers, method="POST"
)
with urllib.request.urlopen(req, timeout=FETCH_TIMEOUT) as r:
data = json.loads(r.read())
if isinstance(data, dict) and "count" in data:
return str(data["count"])
if isinstance(data, list):
return str(len(data))
return "?"
except (urllib.error.URLError, OSError, json.JSONDecodeError, ValueError):
return "?"
@@ -14,6 +14,11 @@ User ID resolution:
Settings resolution:
~/.mem0/settings.json (user-editable, falls back to defaults)
Base URL resolution (first non-empty wins):
1. MEM0_BASE_URL env var (explicit override, for self-hosted servers)
2. "base_url" in ~/.mem0/settings.json
3. DEFAULT_BASE_URL (the hosted Mem0 Platform)
"""
from __future__ import annotations
@@ -75,6 +80,9 @@ def resolve_user_id() -> str:
return os.environ.get("USER") or "default"
DEFAULT_BASE_URL = "https://api.mem0.ai"
def resolve_config() -> dict:
"""Resolve settings from ~/.mem0/settings.json (primary) with env var overrides."""
try:
@@ -88,9 +96,21 @@ def resolve_config() -> dict:
"retention_session_days": 90,
"confidence_threshold": 0.3,
"debug": False,
"base_url": "",
}
def resolve_base_url() -> str:
"""Resolve the Mem0 API base URL: MEM0_BASE_URL env var, then settings.json, then the hosted platform."""
explicit = os.environ.get("MEM0_BASE_URL", "").strip()
if explicit:
return explicit.rstrip("/")
configured = str(resolve_config().get("base_url", "")).strip()
if configured:
return configured.rstrip("/")
return DEFAULT_BASE_URL
try:
from _project import resolve_branch, resolve_project_id, save_project_mapping
except ImportError:
@@ -83,5 +83,12 @@ else
fi
export MEM0_AUTO_SAVE MEM0_AUTO_SEARCH MEM0_SEARCH_LIMIT MEM0_RETENTION_SESSION_DAYS MEM0_CONFIDENCE_THRESHOLD MEM0_GLOBAL_SEARCH MEM0_DEBUG
# Resolve base URL: MEM0_BASE_URL env var > settings.json "base_url" > hosted platform
if [ -z "${MEM0_BASE_URL:-}" ] && command -v python3 >/dev/null 2>&1; then
MEM0_BASE_URL=$(PYTHONPATH="$_SCRIPT_DIR" python3 -c "from _identity import resolve_base_url; print(resolve_base_url())" 2>/dev/null || echo "https://api.mem0.ai")
fi
: "${MEM0_BASE_URL:=https://api.mem0.ai}"
export MEM0_BASE_URL
# Also resolve project context
. "$_SCRIPT_DIR/_project.sh"
+8 -10
View File
@@ -1,7 +1,8 @@
"""Shared mem0 search API helper.
Wraps POST /v3/memories/search/ into a single function call.
All pre-fetch hooks use this instead of duplicating urllib boilerplate.
Wraps the search endpoint (hosted Platform or self-hosted server) into a
single function call. All pre-fetch hooks use this instead of duplicating
urllib boilerplate.
"""
from __future__ import annotations
@@ -11,7 +12,8 @@ import os
import sys
import urllib.request
SEARCH_URL = "https://api.mem0.ai/v3/memories/search/"
from _api import auth_headers, project_field, search_url
SEARCH_TIMEOUT = 5
@@ -35,12 +37,8 @@ def should_rerank() -> bool:
def _do_search(api_key: str, payload: dict) -> list[dict]:
body = json.dumps(payload).encode()
req = urllib.request.Request(
SEARCH_URL,
data=body,
headers={"Authorization": f"Token {api_key}", "Content-Type": "application/json"},
method="POST",
)
headers = {**auth_headers(api_key), "Content-Type": "application/json"}
req = urllib.request.Request(search_url(), data=body, headers=headers, method="POST")
with urllib.request.urlopen(req, timeout=SEARCH_TIMEOUT) as r:
data = json.loads(r.read())
return data if isinstance(data, list) else data.get("results", [])
@@ -65,7 +63,7 @@ def search_memories(
if global_search:
filters: dict = {"OR": [{"user_id": "*"}]}
else:
base_clauses: list[dict] = [{"user_id": user_id}, {"app_id": project_id}]
base_clauses: list[dict] = [{"user_id": user_id}, {project_field(): project_id}]
if metadata_type:
base_clauses.append({"metadata": {"type": metadata_type}})
if metadata_filters:
@@ -20,6 +20,7 @@ import urllib.error
import urllib.request
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _api import add_url, auth_headers, project_field
from _identity import resolve_api_key, resolve_user_id
from _instructions import load_instructions
from _project import resolve_branch, resolve_project_id
@@ -40,7 +41,6 @@ if os.environ.get("MEM0_DEBUG"):
except OSError:
pass
API_URL = "https://api.mem0.ai"
TAIL_LINES = 200
MAX_CONTENT_CHARS = 8000
MIN_CONTENT_CHARS = 100
@@ -123,7 +123,7 @@ def store_exchange(api_key: str, messages: list[dict], user_id: str,
body = {
"messages": messages,
"user_id": user_id,
"app_id": project_id,
project_field(): project_id,
"metadata": metadata,
"infer": True,
}
@@ -131,15 +131,8 @@ def store_exchange(api_key: str, messages: list[dict], user_id: str,
body.update(load_instructions())
data = json.dumps(body).encode("utf-8")
req = urllib.request.Request(
f"{API_URL}/v3/memories/add/",
data=data,
headers={
"Content-Type": "application/json",
"Authorization": f"Token {api_key}",
},
method="POST",
)
headers = {**auth_headers(api_key), "Content-Type": "application/json"}
req = urllib.request.Request(add_url(), data=data, headers=headers, method="POST")
try:
with urllib.request.urlopen(req, timeout=15) as resp:
if resp.status in (200, 201):
+12 -27
View File
@@ -21,6 +21,7 @@ import urllib.error
import urllib.request
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _api import add_url, auth_headers, delete_url, project_field, search_url
from _chunking import filter_and_truncate, split_by_headers
from _identity import resolve_api_key, resolve_user_id
from _project import resolve_branch, resolve_project_id, save_project_mapping
@@ -41,7 +42,6 @@ if os.environ.get("MEM0_DEBUG"):
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")
@@ -127,19 +127,15 @@ def already_imported(api_key: str, user_id: str, project_id: str, filename: str)
"filters": {
"AND": [
{"user_id": user_id},
{"app_id": project_id},
{project_field(): project_id},
{"metadata": {"source": "auto-import"}},
]
},
"top_k": 10,
"threshold": 0.0,
}).encode()
req = urllib.request.Request(
f"{API_URL}/v3/memories/search/",
data=body,
headers={"Content-Type": "application/json", "Authorization": f"Token {api_key}"},
method="POST",
)
headers = {**auth_headers(api_key), "Content-Type": "application/json"}
req = urllib.request.Request(search_url(), data=body, headers=headers, method="POST")
try:
with urllib.request.urlopen(req, timeout=5) as r:
data = json.loads(r.read())
@@ -161,19 +157,15 @@ def _delete_stale_chunks(api_key: str, user_id: str, project_id: str, filename:
"filters": {
"AND": [
{"user_id": user_id},
{"app_id": project_id},
{project_field(): project_id},
{"metadata": {"source": "auto-import"}},
]
},
"top_k": 20,
"threshold": 0.0,
}).encode()
req = urllib.request.Request(
f"{API_URL}/v3/memories/search/",
data=body,
headers={"Content-Type": "application/json", "Authorization": f"Token {api_key}"},
method="POST",
)
headers = {**auth_headers(api_key), "Content-Type": "application/json"}
req = urllib.request.Request(search_url(), data=body, headers=headers, method="POST")
ids_to_delete = []
try:
with urllib.request.urlopen(req, timeout=10) as r:
@@ -196,8 +188,8 @@ def _delete_stale_chunks(api_key: str, user_id: str, project_id: str, filename:
for mid in ids_to_delete:
try:
del_req = urllib.request.Request(
f"{API_URL}/v1/memories/{mid}/",
headers={"Authorization": f"Token {api_key}"},
delete_url(mid),
headers=auth_headers(api_key),
method="DELETE",
)
with urllib.request.urlopen(del_req, timeout=10):
@@ -227,21 +219,14 @@ def post_memory(api_key: str, content: str, user_id: str, filename: str, project
}
],
"user_id": user_id,
"app_id": project_id,
project_field(): project_id,
"metadata": metadata,
"infer": False,
}
data = json.dumps(body).encode("utf-8")
req = urllib.request.Request(
f"{API_URL}/v3/memories/add/",
data=data,
headers={
"Content-Type": "application/json",
"Authorization": f"Token {api_key}",
},
method="POST",
)
headers = {**auth_headers(api_key), "Content-Type": "application/json"}
req = urllib.request.Request(add_url(), data=data, headers=headers, method="POST")
try:
with urllib.request.urlopen(req, timeout=15) as resp:
@@ -38,6 +38,7 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
# Importing setup_coding_categories also injects the plugin venv's site-packages
# onto sys.path (its module-level bootstrap), so ``from mem0 import MemoryClient``
# works even when this script is run with the system python3.
from _api import is_self_hosted # noqa: E402
from _identity import resolve_api_key # noqa: E402
from setup_coding_categories import CODING_CATEGORIES, _categories_match # noqa: E402
@@ -189,6 +190,10 @@ def main() -> None:
log.debug("MEM0_API_KEY not set, skipping coding-categories setup")
return
if is_self_hosted():
log.debug("Self-hosted server detected; category customization requires the hosted Mem0 Platform, skipping")
return
key_fp = apikey_fingerprint(api_key)
cat_fp = categories_fingerprint()
@@ -25,6 +25,7 @@ import urllib.request
from datetime import date, timedelta
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _api import add_url, auth_headers, project_field
from _identity import resolve_api_key, resolve_user_id
from _instructions import load_instructions
from _project import resolve_branch, resolve_project_id
@@ -45,7 +46,6 @@ if os.environ.get("MEM0_DEBUG"):
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.
@@ -111,7 +111,7 @@ def store_summary(api_key: str, summary: str, user_id: str, session_id: str, pro
body = {
"messages": [{"role": "assistant", "content": summary}],
"user_id": user_id,
"app_id": project_id,
project_field(): project_id,
"metadata": metadata,
"infer": True,
"expiration_date": expires,
@@ -120,15 +120,8 @@ def store_summary(api_key: str, summary: str, user_id: str, session_id: str, pro
body.update(load_instructions(cwd))
data = json.dumps(body).encode("utf-8")
req = urllib.request.Request(
f"{API_URL}/v3/memories/add/",
data=data,
headers={
"Content-Type": "application/json",
"Authorization": f"Token {api_key}",
},
method="POST",
)
headers = {**auth_headers(api_key), "Content-Type": "application/json"}
req = urllib.request.Request(add_url(), data=data, headers=headers, method="POST")
try:
with urllib.request.urlopen(req, timeout=15) as resp:
if resp.status in (200, 201):
@@ -23,6 +23,7 @@ import urllib.request
from datetime import date, timedelta
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _api import add_url, auth_headers, project_field
from _identity import resolve_api_key, resolve_user_id
from _instructions import load_instructions
from _project import resolve_branch, resolve_project_id
@@ -43,7 +44,6 @@ if os.environ.get("MEM0_DEBUG"):
except OSError:
pass
API_URL = "https://api.mem0.ai"
MAX_TAIL_LINES = 3000
MAX_SUMMARY_CHARS = 50000
SUMMARY_EXPIRY_DAYS = 90
@@ -184,7 +184,7 @@ def store_summary(
body = {
"messages": [{"role": "assistant", "content": summary_prompt}],
"user_id": user_id,
"app_id": project_id,
project_field(): project_id,
"run_id": session_id,
"metadata": metadata,
"infer": True,
@@ -194,15 +194,8 @@ def store_summary(
body.update(load_instructions(cwd))
data = json.dumps(body).encode("utf-8")
req = urllib.request.Request(
f"{API_URL}/v3/memories/add/",
data=data,
headers={
"Content-Type": "application/json",
"Authorization": f"Token {api_key}",
},
method="POST",
)
headers = {**auth_headers(api_key), "Content-Type": "application/json"}
req = urllib.request.Request(add_url(), data=data, headers=headers, method="POST")
try:
with urllib.request.urlopen(req, timeout=15) as resp:
if resp.status in (200, 201):
@@ -25,6 +25,7 @@ import urllib.error
import urllib.request
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _api import add_url, auth_headers, project_field
from _chunking import (
filter_and_truncate,
split_by_headers,
@@ -33,7 +34,6 @@ from _chunking import (
from _identity import resolve_api_key, resolve_user_id
from _project import resolve_branch, resolve_project_id
API_URL = "https://api.mem0.ai"
HASH_STORE = os.path.expanduser("~/.mem0/import_hashes.json")
@@ -77,20 +77,13 @@ def post_memory(api_key: str, content: str, user_id: str, project_id: str, branc
body = {
"messages": [{"role": "user", "content": content}],
"user_id": user_id,
"app_id": project_id,
project_field(): project_id,
"metadata": metadata,
"infer": False,
}
data = json.dumps(body).encode("utf-8")
req = urllib.request.Request(
f"{API_URL}/v3/memories/add/",
data=data,
headers={
"Content-Type": "application/json",
"Authorization": f"Token {api_key}",
},
method="POST",
)
headers = {**auth_headers(api_key), "Content-Type": "application/json"}
req = urllib.request.Request(add_url(), data=data, headers=headers, method="POST")
try:
with urllib.request.urlopen(req, timeout=20) as resp:
return resp.status in (200, 201)
@@ -150,7 +150,7 @@ def main() -> int:
print(f"Installed Mem0 hooks into {HOOKS_FILE}")
print(f"Plugin path: {PLUGIN_ROOT}")
print("Events: PreToolUse, SessionStart, UserPromptSubmit, PostToolUse, Stop, PreCompact")
print("Events: PreToolUse, SessionStart, UserPromptSubmit, PostToolUse")
if not feature_flag_enabled():
print_feature_flag_hint()
@@ -18,6 +18,7 @@ DEFAULTS = {
"confidence_threshold": 0.3,
"global_search": False,
"debug": False,
"base_url": "",
}
@@ -23,6 +23,7 @@ import urllib.request
from datetime import date, timedelta
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _api import add_url, auth_headers, project_field
from _identity import resolve_api_key, resolve_user_id
from _project import resolve_branch, resolve_project_id
@@ -42,7 +43,6 @@ if os.environ.get("MEM0_DEBUG"):
except OSError:
pass
API_URL = "https://api.mem0.ai"
MAX_TAIL_LINES = 500
MAX_USER_MESSAGES = 30
MAX_BASH_COMMANDS = 20
@@ -173,22 +173,15 @@ def store_memory(api_key: str, content: str, user_id: str, source: str, session_
{"role": "user", "content": content}
],
"user_id": user_id,
"app_id": project_id,
project_field(): project_id,
"metadata": metadata,
"expiration_date": expires,
"infer": True,
}
data = json.dumps(body).encode("utf-8")
req = urllib.request.Request(
f"{API_URL}/v3/memories/add/",
data=data,
headers={
"Content-Type": "application/json",
"Authorization": f"Token {api_key}",
},
method="POST",
)
headers = {**auth_headers(api_key), "Content-Type": "application/json"}
req = urllib.request.Request(add_url(), data=data, headers=headers, method="POST")
try:
with urllib.request.urlopen(req, timeout=15) as resp:
@@ -71,37 +71,15 @@ fi
MEM0_COUNT="?"
if command -v python3 >/dev/null 2>&1; then
MEM0_COUNT=$(python3 -c "
import json, os, urllib.request, urllib.error
MEM0_COUNT=$(PYTHONPATH="$SCRIPT_DIR" python3 -c "
import os
from _api import count_memories
api_key = os.environ.get('MEM0_API_KEY', '')
user_id = os.environ.get('MEM0_RESOLVED_USER_ID', 'default')
app_id = os.environ.get('MEM0_PROJECT_ID', '')
project_id = os.environ.get('MEM0_PROJECT_ID', '')
global_search = os.environ.get('MEM0_GLOBAL_SEARCH', 'false') == 'true'
def get_count(filters):
body = json.dumps({'filters': filters}).encode()
req = urllib.request.Request(
'https://api.mem0.ai/v3/memories/?page=1&page_size=1',
headers={'Authorization': f'Token {api_key}', 'Content-Type': 'application/json'},
data=body, method='POST',
)
with urllib.request.urlopen(req, timeout=5) as r:
data = json.loads(r.read())
if isinstance(data, dict) and 'count' in data:
return data['count']
if isinstance(data, list):
return len(data)
return 0
try:
if global_search:
filters = {'OR': [{'user_id': '*'}]}
else:
filters = {'AND': [{'user_id': user_id}, {'app_id': app_id}]}
total = get_count(filters)
print(total)
except Exception:
print('?')
print(count_memories(api_key, user_id, project_id, global_search=global_search))
" 2>/dev/null || echo "?")
fi
@@ -11,51 +11,23 @@ Output: Compact timeline text to stdout (empty if nothing found)
from __future__ import annotations
import json
import os
import sys
import urllib.request
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _api import fetch_recent
from _formatting import TYPE_ICONS, format_age
from _identity import resolve_api_key, resolve_user_id
from _project import resolve_project_id
API_URL = "https://api.mem0.ai"
MAX_RECENT = 10
MAX_SUMMARIES = 3
FETCH_TIMEOUT = 5
def fetch_recent_memories(api_key: str, user_id: str, project_id: str) -> list[dict]:
"""Fetch the most recent memories for this project via GET list endpoint."""
"""Fetch the most recent memories for this project."""
global_search = os.environ.get("MEM0_GLOBAL_SEARCH", "false") == "true"
if global_search:
filters = {"OR": [{"user_id": "*"}]}
else:
filters = {"AND": [{"user_id": user_id}, {"app_id": project_id}]}
body = json.dumps({"filters": filters}).encode()
req = urllib.request.Request(
f"{API_URL}/v3/memories/?page=1&page_size={MAX_RECENT}",
data=body,
headers={
"Authorization": f"Token {api_key}",
"Content-Type": "application/json",
},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=FETCH_TIMEOUT) as r:
result = json.loads(r.read())
if isinstance(result, dict) and "results" in result:
return result["results"][:MAX_RECENT]
if isinstance(result, list):
return result[:MAX_RECENT]
return []
except Exception:
return []
return fetch_recent(api_key, user_id, project_id, top_k=MAX_RECENT, global_search=global_search)[:MAX_RECENT]
def format_timeline(memories: list[dict]) -> str:
@@ -24,6 +24,7 @@ import sys
_script_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, _script_dir)
from _api import is_self_hosted # noqa: E402
from _identity import resolve_api_key # noqa: E402
_plugin_root = os.environ.get("CLAUDE_PLUGIN_ROOT", os.path.join(_script_dir, ".."))
@@ -179,6 +180,16 @@ def main() -> int:
if not api_key:
print("ERROR: MEM0_API_KEY is not set. Export it or configure it via plugin userConfig.", file=sys.stderr)
return 1
if is_self_hosted():
print(
"Custom category taxonomies require the hosted Mem0 Platform (client.project.update\n"
"has no self-hosted equivalent). Unset MEM0_BASE_URL to use the hosted platform, or\n"
"skip this step on a self-hosted server.",
file=sys.stderr,
)
return 1
os.environ["MEM0_API_KEY"] = api_key
try:
@@ -0,0 +1,307 @@
"""Tests for self-hosted Mem0 server support (MEM0_BASE_URL routing).
Covers:
1. Default (no override) stays on the hosted Platform -- no behavior change.
2. An explicit override (env var or settings.json) routes to the self-hosted
server: different URLs, X-API-Key auth, agent_id instead of app_id.
3. A malformed/empty override never silently falls back to the hosted
Platform -- it is treated as self-hosted and fails closed instead of
leaking requests to api.mem0.ai.
"""
from __future__ import annotations
import json
from unittest.mock import MagicMock, patch
# --------------------------------------------------------------------------- #
# resolve_base_url() -- config plumbing #
# --------------------------------------------------------------------------- #
def test_resolve_base_url_defaults_to_hosted_platform(monkeypatch):
from _identity import DEFAULT_BASE_URL, resolve_base_url
monkeypatch.delenv("MEM0_BASE_URL", raising=False)
assert resolve_base_url() == DEFAULT_BASE_URL == "https://api.mem0.ai"
def test_resolve_base_url_env_var_overrides(monkeypatch):
from _identity import resolve_base_url
monkeypatch.setenv("MEM0_BASE_URL", "http://localhost:8000")
assert resolve_base_url() == "http://localhost:8000"
def test_resolve_base_url_strips_trailing_slash(monkeypatch):
from _identity import resolve_base_url
monkeypatch.setenv("MEM0_BASE_URL", "http://localhost:8000/")
assert resolve_base_url() == "http://localhost:8000"
def test_resolve_base_url_falls_back_to_settings_file(monkeypatch, tmp_path):
import load_settings
from _identity import resolve_base_url
monkeypatch.delenv("MEM0_BASE_URL", raising=False)
settings_path = tmp_path / "settings.json"
settings_path.write_text(json.dumps({"base_url": "http://self-hosted.local:8000"}))
monkeypatch.setattr(load_settings, "SETTINGS_PATH", settings_path)
assert resolve_base_url() == "http://self-hosted.local:8000"
def test_resolve_base_url_env_var_takes_precedence_over_settings(monkeypatch, tmp_path):
import load_settings
from _identity import resolve_base_url
settings_path = tmp_path / "settings.json"
settings_path.write_text(json.dumps({"base_url": "http://from-settings:8000"}))
monkeypatch.setattr(load_settings, "SETTINGS_PATH", settings_path)
monkeypatch.setenv("MEM0_BASE_URL", "http://from-env:9000")
assert resolve_base_url() == "http://from-env:9000"
def test_resolve_base_url_empty_env_var_falls_through(monkeypatch, tmp_path):
"""A blank MEM0_BASE_URL must not be treated as an explicit override."""
import load_settings
from _identity import DEFAULT_BASE_URL, resolve_base_url
monkeypatch.setenv("MEM0_BASE_URL", " ")
settings_path = tmp_path / "settings.json"
monkeypatch.setattr(load_settings, "SETTINGS_PATH", settings_path)
assert resolve_base_url() == DEFAULT_BASE_URL
# --------------------------------------------------------------------------- #
# is_self_hosted() #
# --------------------------------------------------------------------------- #
def test_is_self_hosted_false_by_default(monkeypatch):
from _api import is_self_hosted
monkeypatch.delenv("MEM0_BASE_URL", raising=False)
assert is_self_hosted() is False
def test_is_self_hosted_true_when_overridden(monkeypatch):
from _api import is_self_hosted
monkeypatch.setenv("MEM0_BASE_URL", "http://localhost:8000")
assert is_self_hosted() is True
def test_is_self_hosted_malformed_url_treated_as_self_hosted(monkeypatch):
"""A garbage override must not be silently coerced back to the hosted URL."""
from _api import is_self_hosted
monkeypatch.setenv("MEM0_BASE_URL", "not-a-url")
assert is_self_hosted() is True
# --------------------------------------------------------------------------- #
# auth_headers() / project_field() #
# --------------------------------------------------------------------------- #
def test_auth_headers_hosted_uses_token(monkeypatch):
from _api import auth_headers
monkeypatch.delenv("MEM0_BASE_URL", raising=False)
assert auth_headers("m0-secret") == {"Authorization": "Token m0-secret"}
def test_auth_headers_self_hosted_uses_api_key_header(monkeypatch):
from _api import auth_headers
monkeypatch.setenv("MEM0_BASE_URL", "http://localhost:8000")
assert auth_headers("m0sk-secret") == {"X-API-Key": "m0sk-secret"}
def test_project_field_hosted_is_app_id(monkeypatch):
from _api import project_field
monkeypatch.delenv("MEM0_BASE_URL", raising=False)
assert project_field() == "app_id"
def test_project_field_self_hosted_is_agent_id(monkeypatch):
from _api import project_field
monkeypatch.setenv("MEM0_BASE_URL", "http://localhost:8000")
assert project_field() == "agent_id"
# --------------------------------------------------------------------------- #
# URL builders #
# --------------------------------------------------------------------------- #
def test_add_url_hosted(monkeypatch):
from _api import add_url
monkeypatch.delenv("MEM0_BASE_URL", raising=False)
assert add_url() == "https://api.mem0.ai/v3/memories/add/"
def test_add_url_self_hosted(monkeypatch):
from _api import add_url
monkeypatch.setenv("MEM0_BASE_URL", "http://localhost:8000")
assert add_url() == "http://localhost:8000/memories"
def test_search_url_hosted(monkeypatch):
from _api import search_url
monkeypatch.delenv("MEM0_BASE_URL", raising=False)
assert search_url() == "https://api.mem0.ai/v3/memories/search/"
def test_search_url_self_hosted(monkeypatch):
from _api import search_url
monkeypatch.setenv("MEM0_BASE_URL", "http://localhost:8000")
assert search_url() == "http://localhost:8000/search"
def test_delete_url_hosted(monkeypatch):
from _api import delete_url
monkeypatch.delenv("MEM0_BASE_URL", raising=False)
assert delete_url("mem-123") == "https://api.mem0.ai/v1/memories/mem-123/"
def test_delete_url_self_hosted(monkeypatch):
from _api import delete_url
monkeypatch.setenv("MEM0_BASE_URL", "http://localhost:8000")
assert delete_url("mem-123") == "http://localhost:8000/memories/mem-123"
def test_search_url_malformed_override_does_not_leak_to_hosted(monkeypatch):
"""A garbage override must build a URL against itself, never api.mem0.ai."""
from _api import search_url
monkeypatch.setenv("MEM0_BASE_URL", "not-a-url")
url = search_url()
assert "api.mem0.ai" not in url
assert url == "not-a-url/search"
# --------------------------------------------------------------------------- #
# End-to-end: write path (auto_import.post_memory) #
# --------------------------------------------------------------------------- #
def test_auto_import_post_memory_hosted_unchanged(monkeypatch):
"""Default (no override) still posts to the hosted Platform with app_id."""
from auto_import import post_memory
monkeypatch.delenv("MEM0_BASE_URL", raising=False)
captured = {}
def mock_urlopen(req, timeout=None):
captured["url"] = req.full_url
captured["headers"] = req.headers
captured.update(json.loads(req.data.decode("utf-8")))
resp = MagicMock()
resp.status = 200
resp.__enter__ = lambda s: s
resp.__exit__ = MagicMock(return_value=False)
return resp
with patch("urllib.request.urlopen", side_effect=mock_urlopen):
result = post_memory("test-key", "content", "user", "CLAUDE.md", "my-project", "main")
assert result is True
assert captured["url"] == "https://api.mem0.ai/v3/memories/add/"
assert captured["headers"]["Authorization"] == "Token test-key"
assert captured["app_id"] == "my-project"
assert "agent_id" not in captured
def test_auto_import_post_memory_self_hosted_routes_correctly(monkeypatch):
"""MEM0_BASE_URL override posts to the self-hosted server with agent_id."""
from auto_import import post_memory
monkeypatch.setenv("MEM0_BASE_URL", "http://localhost:8000")
captured = {}
def mock_urlopen(req, timeout=None):
captured["url"] = req.full_url
captured["headers"] = req.headers
captured.update(json.loads(req.data.decode("utf-8")))
resp = MagicMock()
resp.status = 200
resp.__enter__ = lambda s: s
resp.__exit__ = MagicMock(return_value=False)
return resp
with patch("urllib.request.urlopen", side_effect=mock_urlopen):
result = post_memory("m0sk-key", "content", "user", "CLAUDE.md", "my-project", "main")
assert result is True
assert captured["url"] == "http://localhost:8000/memories"
assert captured["headers"]["X-api-key"] == "m0sk-key"
assert captured["agent_id"] == "my-project"
assert "app_id" not in captured
# --------------------------------------------------------------------------- #
# End-to-end: search path (_search.search_memories) #
# --------------------------------------------------------------------------- #
def test_search_memories_self_hosted_routes_correctly(monkeypatch):
from _search import search_memories
monkeypatch.setenv("MEM0_BASE_URL", "http://localhost:8000")
captured = {}
def mock_urlopen(req, timeout=None):
captured["url"] = req.full_url
captured["headers"] = req.headers
captured.update(json.loads(req.data.decode("utf-8")))
resp = MagicMock()
resp.read.return_value = json.dumps({"results": []}).encode()
resp.__enter__ = lambda s: s
resp.__exit__ = MagicMock(return_value=False)
return resp
with patch("urllib.request.urlopen", side_effect=mock_urlopen):
search_memories("m0sk-key", "user", "proj", "query")
assert captured["url"] == "http://localhost:8000/search"
assert captured["headers"]["X-api-key"] == "m0sk-key"
filters = captured["filters"]
assert {"agent_id": "proj"} in filters["AND"]
assert {"app_id": "proj"} not in filters["AND"]
def test_search_memories_malformed_base_url_fails_closed_not_hosted(monkeypatch):
"""A garbage override must fail (return []) rather than leak to api.mem0.ai.
No urlopen mock here on purpose: urllib rejects the schemeless URL before
any network I/O happens, proving the request never reaches a real host --
hosted or otherwise. search_memories' broad except turns that into [].
"""
from _search import search_memories
monkeypatch.setenv("MEM0_BASE_URL", "not-a-url")
with patch("urllib.request.urlopen") as mock_urlopen:
results = search_memories("key", "user", "proj", "query")
mock_urlopen.assert_not_called()
assert results == []
# --------------------------------------------------------------------------- #
# Category-taxonomy scripts: no self-hosted equivalent, must skip cleanly #
# --------------------------------------------------------------------------- #
def test_auto_setup_categories_skips_when_self_hosted(monkeypatch):
import auto_setup_categories as asc
monkeypatch.setattr(asc, "resolve_api_key", lambda: "m0sk-key")
monkeypatch.setenv("MEM0_BASE_URL", "http://localhost:8000")
calls = []
monkeypatch.setattr(asc, "make_client", lambda: calls.append("make_client"))
asc.main()
assert calls == []
+2 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@mem0/n8n-nodes-mem0",
"version": "0.1.4",
"version": "0.1.3",
"description": "n8n community node for Mem0 — the memory layer for AI agents. Add, search, get, update, and delete long-term memories.",
"keywords": [
"n8n-community-node-package",
@@ -73,9 +73,7 @@
"lodash@<=4.17.23": ">=4.18.0",
"brace-expansion@<1.1.16": ">=1.1.16 <2.0.0",
"brace-expansion@>=2.0.0 <2.1.2": ">=2.1.2 <3.0.0",
"brace-expansion@>=5.0.0 <5.0.8": ">=5.0.8",
"js-yaml@<3.15.1": ">=3.15.1 <4.0.0",
"js-yaml@>=4.0.0 <4.3.1": ">=4.3.1 <5.0.0"
"brace-expansion@>=5.0.0 <5.0.8": ">=5.0.8"
}
}
}
+167 -169
View File
@@ -11,8 +11,6 @@ overrides:
brace-expansion@<1.1.16: '>=1.1.16 <2.0.0'
brace-expansion@>=2.0.0 <2.1.2: '>=2.1.2 <3.0.0'
brace-expansion@>=5.0.0 <5.0.8: '>=5.0.8'
js-yaml@<3.15.1: '>=3.15.1 <4.0.0'
js-yaml@>=4.0.0 <4.3.1: '>=4.3.1 <5.0.0'
importers:
@@ -26,7 +24,7 @@ importers:
version: 20.19.43
'@typescript-eslint/parser':
specifier: ^8.0.0
version: 8.67.0(eslint@8.57.1)(typescript@5.9.3)
version: 8.62.1(eslint@8.57.1)(typescript@5.9.3)
eslint:
specifier: ^8.57.0
version: 8.57.1
@@ -44,7 +42,7 @@ importers:
version: 2.16.0
prettier:
specifier: ^3.3.0
version: 3.9.6
version: 3.9.4
rimraf:
specifier: ^5.0.0
version: 5.0.10
@@ -69,8 +67,8 @@ packages:
resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==}
engines: {node: '>=6.9.0'}
'@babel/generator@7.29.8':
resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==}
'@babel/generator@7.29.7':
resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==}
engines: {node: '>=6.9.0'}
'@babel/helper-compilation-targets@7.29.7':
@@ -111,8 +109,8 @@ packages:
resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==}
engines: {node: '>=6.9.0'}
'@babel/parser@7.29.8':
resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==}
'@babel/parser@7.29.7':
resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==}
engines: {node: '>=6.0.0'}
hasBin: true
@@ -211,19 +209,19 @@ packages:
resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
engines: {node: '>=6.9.0'}
'@babel/traverse@7.29.8':
resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==}
'@babel/traverse@7.29.7':
resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==}
engines: {node: '>=6.9.0'}
'@babel/types@7.29.8':
resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==}
'@babel/types@7.29.7':
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
engines: {node: '>=6.9.0'}
'@bcoe/v8-coverage@0.2.3':
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
'@eslint-community/eslint-utils@4.10.1':
resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==}
'@eslint-community/eslint-utils@4.9.1':
resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
@@ -432,60 +430,60 @@ packages:
'@types/yargs@17.0.35':
resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==}
'@typescript-eslint/parser@8.67.0':
resolution: {integrity: sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==}
'@typescript-eslint/parser@8.62.1':
resolution: {integrity: sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/project-service@8.67.0':
resolution: {integrity: sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==}
'@typescript-eslint/project-service@8.62.1':
resolution: {integrity: sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/scope-manager@8.67.0':
resolution: {integrity: sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==}
'@typescript-eslint/scope-manager@8.62.1':
resolution: {integrity: sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/tsconfig-utils@8.67.0':
resolution: {integrity: sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==}
'@typescript-eslint/tsconfig-utils@8.62.1':
resolution: {integrity: sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/types@8.67.0':
resolution: {integrity: sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==}
'@typescript-eslint/types@8.62.1':
resolution: {integrity: sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@8.67.0':
resolution: {integrity: sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==}
'@typescript-eslint/typescript-estree@8.62.1':
resolution: {integrity: sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/utils@8.67.0':
resolution: {integrity: sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==}
'@typescript-eslint/utils@8.62.1':
resolution: {integrity: sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/visitor-keys@8.67.0':
resolution: {integrity: sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==}
'@typescript-eslint/visitor-keys@8.62.1':
resolution: {integrity: sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@ungap/structured-clone@1.3.3':
resolution: {integrity: sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==}
'@ungap/structured-clone@1.3.2':
resolution: {integrity: sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==}
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
acorn@8.18.0:
resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==}
acorn@8.17.0:
resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==}
engines: {node: '>=0.4.0'}
hasBin: true
@@ -500,8 +498,8 @@ packages:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
ansi-regex@6.3.0:
resolution: {integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==}
ansi-regex@6.2.2:
resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
engines: {node: '>=12'}
ansi-styles@4.3.0:
@@ -615,8 +613,8 @@ packages:
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
baseline-browser-mapping@2.11.15:
resolution: {integrity: sha512-FwMjJJ7HnyZpWe+oWxegG0fezZyBZUagI5LZEoO3GCbtbKNwRfMH9Ue5d5v01PNePBy1QSfPSDTTeVL0Hb9EzA==}
baseline-browser-mapping@2.11.1:
resolution: {integrity: sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A==}
engines: {node: '>=6.0.0'}
hasBin: true
@@ -627,22 +625,22 @@ packages:
bl@5.1.0:
resolution: {integrity: sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==}
brace-expansion@1.1.18:
resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==}
brace-expansion@1.1.17:
resolution: {integrity: sha512-w+aeW/mkgM4PyRMOJCgi3fOrTm5Q8QY1OSfn2TO2iuDj3ezIHqejmuxbjfPrqUkgqRew1iqkyAn0tr0ZwHD9+w==}
brace-expansion@2.1.4:
resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==}
brace-expansion@2.1.3:
resolution: {integrity: sha512-DRdx5neNsG/QXbniLFWi2YmC/68oeOOmKz6zOjVk6ZS1ZLXgLIKqVEc6hWsmkjBbgii0SwaBTcJ5XKj5gzY/4A==}
brace-expansion@5.0.9:
resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==}
brace-expansion@5.0.8:
resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==}
engines: {node: 20 || >=22}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
browserslist@4.28.8:
resolution: {integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==}
browserslist@4.28.7:
resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
@@ -686,8 +684,8 @@ packages:
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
engines: {node: '>=10'}
caniuse-lite@1.0.30001809:
resolution: {integrity: sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==}
caniuse-lite@1.0.30001806:
resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==}
chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
@@ -825,8 +823,8 @@ packages:
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
electron-to-chromium@1.5.409:
resolution: {integrity: sha512-ChI4N44d0B4A6C8prnNjMOaGgE59fUyEVYcRYm2XEXIjMbbvF5i9UL1cblDbpGqiU0uS8FE8UcKxqZqTXdmzbQ==}
electron-to-chromium@1.5.395:
resolution: {integrity: sha512-7zt9Aw+SrmxLWLN0zhaTWZQiCdryLVrYTq5R7iZakLvi2UQPYMMsROYV/2qVCzMeCiSXHwKOU+sZ4zOVVlrtKA==}
emittery@0.13.1:
resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==}
@@ -1015,8 +1013,8 @@ packages:
resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==}
engines: {node: ^10.12.0 || >=12.0.0}
flatted@3.4.4:
resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==}
flatted@3.4.2:
resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
for-each@0.3.5:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
@@ -1324,8 +1322,8 @@ packages:
resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==}
engines: {node: '>=0.10.0'}
isolated-vm@6.2.0:
resolution: {integrity: sha512-UuSlxSHWt2QuJ5WvBhzlIJx2VVZN/a44SqBbEZFKNdvuSyhOvhmyDo8SQ+njVbhnh/njoL/aW0bUTiFYlpweGQ==}
isolated-vm@6.1.2:
resolution: {integrity: sha512-GGfsHqtlZiiurZaxB/3kY7LLAXR3sgzDul0fom4cSyBjx6ZbjpTrFWiH3z/nUfLJGJ8PIq9LQmQFiAxu24+I7A==}
engines: {node: '>=22.0.0'}
istanbul-lib-coverage@3.2.2:
@@ -1494,12 +1492,12 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-yaml@3.15.1:
resolution: {integrity: sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==}
js-yaml@3.15.0:
resolution: {integrity: sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==}
hasBin: true
js-yaml@4.3.1:
resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==}
js-yaml@4.3.0:
resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==}
hasBin: true
jsesc@3.1.0:
@@ -1631,8 +1629,8 @@ packages:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
minimatch@10.2.6:
resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==}
minimatch@10.2.5:
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
engines: {node: 18 || 20 || >=22}
minimatch@3.1.5:
@@ -1675,8 +1673,8 @@ packages:
node-int64@0.4.0:
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
node-releases@2.0.53:
resolution: {integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==}
node-releases@2.0.51:
resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==}
engines: {node: '>=18'}
normalize-path@3.0.0:
@@ -1822,8 +1820,8 @@ packages:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
prettier@3.9.6:
resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==}
prettier@3.9.4:
resolution: {integrity: sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==}
engines: {node: '>=14'}
hasBin: true
@@ -1934,8 +1932,8 @@ packages:
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
sax@1.6.1:
resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==}
sax@1.6.0:
resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==}
engines: {node: '>=11.0.0'}
semver-greatest-satisfied-range@2.0.0:
@@ -2174,8 +2172,8 @@ packages:
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
update-browserslist-db@1.3.1:
resolution: {integrity: sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==}
update-browserslist-db@1.2.3:
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
hasBin: true
peerDependencies:
browserslist: '>= 4.21.0'
@@ -2313,14 +2311,14 @@ snapshots:
'@babel/core@7.29.7':
dependencies:
'@babel/code-frame': 7.29.7
'@babel/generator': 7.29.8
'@babel/generator': 7.29.7
'@babel/helper-compilation-targets': 7.29.7
'@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7)
'@babel/helpers': 7.29.7
'@babel/parser': 7.29.8
'@babel/parser': 7.29.7
'@babel/template': 7.29.7
'@babel/traverse': 7.29.8
'@babel/types': 7.29.8
'@babel/traverse': 7.29.7
'@babel/types': 7.29.7
'@jridgewell/remapping': 2.3.5
convert-source-map: 2.0.0
debug: 4.4.3
@@ -2330,10 +2328,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@babel/generator@7.29.8':
'@babel/generator@7.29.7':
dependencies:
'@babel/parser': 7.29.8
'@babel/types': 7.29.8
'@babel/parser': 7.29.7
'@babel/types': 7.29.7
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
jsesc: 3.1.0
@@ -2342,7 +2340,7 @@ snapshots:
dependencies:
'@babel/compat-data': 7.29.7
'@babel/helper-validator-option': 7.29.7
browserslist: 4.28.8
browserslist: 4.28.7
lru-cache: 5.1.1
semver: 6.3.1
@@ -2350,8 +2348,8 @@ snapshots:
'@babel/helper-module-imports@7.29.7':
dependencies:
'@babel/traverse': 7.29.8
'@babel/types': 7.29.8
'@babel/traverse': 7.29.7
'@babel/types': 7.29.7
transitivePeerDependencies:
- supports-color
@@ -2360,7 +2358,7 @@ snapshots:
'@babel/core': 7.29.7
'@babel/helper-module-imports': 7.29.7
'@babel/helper-validator-identifier': 7.29.7
'@babel/traverse': 7.29.8
'@babel/traverse': 7.29.7
transitivePeerDependencies:
- supports-color
@@ -2375,11 +2373,11 @@ snapshots:
'@babel/helpers@7.29.7':
dependencies:
'@babel/template': 7.29.7
'@babel/types': 7.29.8
'@babel/types': 7.29.7
'@babel/parser@7.29.8':
'@babel/parser@7.29.7':
dependencies:
'@babel/types': 7.29.8
'@babel/types': 7.29.7
'@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.7)':
dependencies:
@@ -2469,29 +2467,29 @@ snapshots:
'@babel/template@7.29.7':
dependencies:
'@babel/code-frame': 7.29.7
'@babel/parser': 7.29.8
'@babel/types': 7.29.8
'@babel/parser': 7.29.7
'@babel/types': 7.29.7
'@babel/traverse@7.29.8':
'@babel/traverse@7.29.7':
dependencies:
'@babel/code-frame': 7.29.7
'@babel/generator': 7.29.8
'@babel/generator': 7.29.7
'@babel/helper-globals': 7.29.7
'@babel/parser': 7.29.8
'@babel/parser': 7.29.7
'@babel/template': 7.29.7
'@babel/types': 7.29.8
'@babel/types': 7.29.7
debug: 4.4.3
transitivePeerDependencies:
- supports-color
'@babel/types@7.29.8':
'@babel/types@7.29.7':
dependencies:
'@babel/helper-string-parser': 7.29.7
'@babel/helper-validator-identifier': 7.29.7
'@bcoe/v8-coverage@0.2.3': {}
'@eslint-community/eslint-utils@4.10.1(eslint@8.57.1)':
'@eslint-community/eslint-utils@4.9.1(eslint@8.57.1)':
dependencies:
eslint: 8.57.1
eslint-visitor-keys: 3.4.3
@@ -2506,7 +2504,7 @@ snapshots:
globals: 13.24.0
ignore: 5.3.2
import-fresh: 3.3.1
js-yaml: 4.3.1
js-yaml: 4.3.0
minimatch: 3.1.5
strip-json-comments: 3.1.1
transitivePeerDependencies:
@@ -2546,7 +2544,7 @@ snapshots:
camelcase: 5.3.1
find-up: 4.1.0
get-package-type: 0.1.0
js-yaml: 3.15.1
js-yaml: 3.15.0
resolve-from: 5.0.0
'@istanbuljs/schema@0.1.6': {}
@@ -2739,7 +2737,7 @@ snapshots:
'@n8n/expression-runtime@0.8.0':
dependencies:
'@n8n/tournament': 1.0.6
isolated-vm: 6.2.0
isolated-vm: 6.1.2
js-base64: 3.7.2
jssha: 3.3.1
lodash: 4.18.1
@@ -2786,24 +2784,24 @@ snapshots:
'@types/babel__core@7.20.5':
dependencies:
'@babel/parser': 7.29.8
'@babel/types': 7.29.8
'@babel/parser': 7.29.7
'@babel/types': 7.29.7
'@types/babel__generator': 7.27.0
'@types/babel__template': 7.4.4
'@types/babel__traverse': 7.28.0
'@types/babel__generator@7.27.0':
dependencies:
'@babel/types': 7.29.8
'@babel/types': 7.29.7
'@types/babel__template@7.4.4':
dependencies:
'@babel/parser': 7.29.8
'@babel/types': 7.29.8
'@babel/parser': 7.29.7
'@babel/types': 7.29.7
'@types/babel__traverse@7.28.0':
dependencies:
'@babel/types': 7.29.8
'@babel/types': 7.29.7
'@types/graceful-fs@4.1.9':
dependencies:
@@ -2836,46 +2834,46 @@ snapshots:
dependencies:
'@types/yargs-parser': 21.0.3
'@typescript-eslint/parser@8.67.0(eslint@8.57.1)(typescript@5.9.3)':
'@typescript-eslint/parser@8.62.1(eslint@8.57.1)(typescript@5.9.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.67.0
'@typescript-eslint/types': 8.67.0
'@typescript-eslint/typescript-estree': 8.67.0(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.67.0
'@typescript-eslint/scope-manager': 8.62.1
'@typescript-eslint/types': 8.62.1
'@typescript-eslint/typescript-estree': 8.62.1(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.62.1
debug: 4.4.3
eslint: 8.57.1
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/project-service@8.67.0(typescript@5.9.3)':
'@typescript-eslint/project-service@8.62.1(typescript@5.9.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.67.0(typescript@5.9.3)
'@typescript-eslint/types': 8.67.0
'@typescript-eslint/tsconfig-utils': 8.62.1(typescript@5.9.3)
'@typescript-eslint/types': 8.62.1
debug: 4.4.3
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/scope-manager@8.67.0':
'@typescript-eslint/scope-manager@8.62.1':
dependencies:
'@typescript-eslint/types': 8.67.0
'@typescript-eslint/visitor-keys': 8.67.0
'@typescript-eslint/types': 8.62.1
'@typescript-eslint/visitor-keys': 8.62.1
'@typescript-eslint/tsconfig-utils@8.67.0(typescript@5.9.3)':
'@typescript-eslint/tsconfig-utils@8.62.1(typescript@5.9.3)':
dependencies:
typescript: 5.9.3
'@typescript-eslint/types@8.67.0': {}
'@typescript-eslint/types@8.62.1': {}
'@typescript-eslint/typescript-estree@8.67.0(typescript@5.9.3)':
'@typescript-eslint/typescript-estree@8.62.1(typescript@5.9.3)':
dependencies:
'@typescript-eslint/project-service': 8.67.0(typescript@5.9.3)
'@typescript-eslint/tsconfig-utils': 8.67.0(typescript@5.9.3)
'@typescript-eslint/types': 8.67.0
'@typescript-eslint/visitor-keys': 8.67.0
'@typescript-eslint/project-service': 8.62.1(typescript@5.9.3)
'@typescript-eslint/tsconfig-utils': 8.62.1(typescript@5.9.3)
'@typescript-eslint/types': 8.62.1
'@typescript-eslint/visitor-keys': 8.62.1
debug: 4.4.3
minimatch: 10.2.6
minimatch: 10.2.5
semver: 7.8.5
tinyglobby: 0.2.17
ts-api-utils: 2.5.0(typescript@5.9.3)
@@ -2883,29 +2881,29 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.67.0(eslint@8.57.1)(typescript@5.9.3)':
'@typescript-eslint/utils@8.62.1(eslint@8.57.1)(typescript@5.9.3)':
dependencies:
'@eslint-community/eslint-utils': 4.10.1(eslint@8.57.1)
'@typescript-eslint/scope-manager': 8.67.0
'@typescript-eslint/types': 8.67.0
'@typescript-eslint/typescript-estree': 8.67.0(typescript@5.9.3)
'@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1)
'@typescript-eslint/scope-manager': 8.62.1
'@typescript-eslint/types': 8.62.1
'@typescript-eslint/typescript-estree': 8.62.1(typescript@5.9.3)
eslint: 8.57.1
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/visitor-keys@8.67.0':
'@typescript-eslint/visitor-keys@8.62.1':
dependencies:
'@typescript-eslint/types': 8.67.0
'@typescript-eslint/types': 8.62.1
eslint-visitor-keys: 5.0.1
'@ungap/structured-clone@1.3.3': {}
'@ungap/structured-clone@1.3.2': {}
acorn-jsx@5.3.2(acorn@8.18.0):
acorn-jsx@5.3.2(acorn@8.17.0):
dependencies:
acorn: 8.18.0
acorn: 8.17.0
acorn@8.18.0: {}
acorn@8.17.0: {}
ajv@6.15.0:
dependencies:
@@ -2920,7 +2918,7 @@ snapshots:
ansi-regex@5.0.1: {}
ansi-regex@6.3.0: {}
ansi-regex@6.2.2: {}
ansi-styles@4.3.0:
dependencies:
@@ -3005,7 +3003,7 @@ snapshots:
babel-plugin-jest-hoist@29.6.3:
dependencies:
'@babel/template': 7.29.7
'@babel/types': 7.29.8
'@babel/types': 7.29.7
'@types/babel__core': 7.20.5
'@types/babel__traverse': 7.28.0
@@ -3048,7 +3046,7 @@ snapshots:
base64-js@1.5.1: {}
baseline-browser-mapping@2.11.15: {}
baseline-browser-mapping@2.11.1: {}
binary-extensions@2.3.0: {}
@@ -3058,16 +3056,16 @@ snapshots:
inherits: 2.0.4
readable-stream: 3.6.2
brace-expansion@1.1.18:
brace-expansion@1.1.17:
dependencies:
balanced-match: 1.0.2
concat-map: 0.0.1
brace-expansion@2.1.4:
brace-expansion@2.1.3:
dependencies:
balanced-match: 1.0.2
brace-expansion@5.0.9:
brace-expansion@5.0.8:
dependencies:
balanced-match: 4.0.4
@@ -3075,13 +3073,13 @@ snapshots:
dependencies:
fill-range: 7.1.1
browserslist@4.28.8:
browserslist@4.28.7:
dependencies:
baseline-browser-mapping: 2.11.15
caniuse-lite: 1.0.30001809
electron-to-chromium: 1.5.409
node-releases: 2.0.53
update-browserslist-db: 1.3.1(browserslist@4.28.8)
baseline-browser-mapping: 2.11.1
caniuse-lite: 1.0.30001806
electron-to-chromium: 1.5.395
node-releases: 2.0.51
update-browserslist-db: 1.2.3(browserslist@4.28.7)
bs-logger@0.2.6:
dependencies:
@@ -3126,7 +3124,7 @@ snapshots:
camelcase@6.3.0: {}
caniuse-lite@1.0.30001809: {}
caniuse-lite@1.0.30001806: {}
chalk@4.1.2:
dependencies:
@@ -3260,7 +3258,7 @@ snapshots:
eastasianwidth@0.2.0: {}
electron-to-chromium@1.5.409: {}
electron-to-chromium@1.5.395: {}
emittery@0.13.1: {}
@@ -3301,7 +3299,7 @@ snapshots:
eslint-plugin-n8n-nodes-base@1.16.7(eslint@8.57.1)(typescript@5.9.3):
dependencies:
'@typescript-eslint/utils': 8.67.0(eslint@8.57.1)(typescript@5.9.3)
'@typescript-eslint/utils': 8.62.1(eslint@8.57.1)(typescript@5.9.3)
camel-case: 4.1.2
indefinite: 2.5.2
pascal-case: 3.1.2
@@ -3324,14 +3322,14 @@ snapshots:
eslint@8.57.1:
dependencies:
'@eslint-community/eslint-utils': 4.10.1(eslint@8.57.1)
'@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1)
'@eslint-community/regexpp': 4.12.2
'@eslint/eslintrc': 2.1.4
'@eslint/js': 8.57.1
'@humanwhocodes/config-array': 0.13.0
'@humanwhocodes/module-importer': 1.0.1
'@nodelib/fs.walk': 1.2.8
'@ungap/structured-clone': 1.3.3
'@ungap/structured-clone': 1.3.2
ajv: 6.15.0
chalk: 4.1.2
cross-spawn: 7.0.6
@@ -3353,7 +3351,7 @@ snapshots:
imurmurhash: 0.1.4
is-glob: 4.0.3
is-path-inside: 3.0.3
js-yaml: 4.3.1
js-yaml: 4.3.0
json-stable-stringify-without-jsonify: 1.0.1
levn: 0.4.1
lodash.merge: 4.6.2
@@ -3367,8 +3365,8 @@ snapshots:
espree@9.6.1:
dependencies:
acorn: 8.18.0
acorn-jsx: 5.3.2(acorn@8.18.0)
acorn: 8.17.0
acorn-jsx: 5.3.2(acorn@8.17.0)
eslint-visitor-keys: 3.4.3
esprima-next@5.8.4: {}
@@ -3484,11 +3482,11 @@ snapshots:
flat-cache@3.2.0:
dependencies:
flatted: 3.4.4
flatted: 3.4.2
keyv: 4.5.4
rimraf: 3.0.2
flatted@3.4.4: {}
flatted@3.4.2: {}
for-each@0.3.5:
dependencies:
@@ -3808,7 +3806,7 @@ snapshots:
isobject@3.0.1: {}
isolated-vm@6.2.0:
isolated-vm@6.1.2:
dependencies:
node-gyp-build: 4.8.4
@@ -3817,7 +3815,7 @@ snapshots:
istanbul-lib-instrument@5.2.1:
dependencies:
'@babel/core': 7.29.7
'@babel/parser': 7.29.8
'@babel/parser': 7.29.7
'@istanbuljs/schema': 0.1.6
istanbul-lib-coverage: 3.2.2
semver: 6.3.1
@@ -3827,7 +3825,7 @@ snapshots:
istanbul-lib-instrument@6.0.3:
dependencies:
'@babel/core': 7.29.7
'@babel/parser': 7.29.8
'@babel/parser': 7.29.7
'@istanbuljs/schema': 0.1.6
istanbul-lib-coverage: 3.2.2
semver: 7.8.5
@@ -4097,10 +4095,10 @@ snapshots:
jest-snapshot@29.7.0:
dependencies:
'@babel/core': 7.29.7
'@babel/generator': 7.29.8
'@babel/generator': 7.29.7
'@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7)
'@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7)
'@babel/types': 7.29.8
'@babel/types': 7.29.7
'@jest/expect-utils': 29.7.0
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
@@ -4173,12 +4171,12 @@ snapshots:
js-tokens@4.0.0: {}
js-yaml@3.15.1:
js-yaml@3.15.0:
dependencies:
argparse: 1.0.10
esprima: 4.0.1
js-yaml@4.3.1:
js-yaml@4.3.0:
dependencies:
argparse: 2.0.1
@@ -4288,17 +4286,17 @@ snapshots:
mimic-fn@2.1.0: {}
minimatch@10.2.6:
minimatch@10.2.5:
dependencies:
brace-expansion: 5.0.9
brace-expansion: 5.0.8
minimatch@3.1.5:
dependencies:
brace-expansion: 1.1.18
brace-expansion: 1.1.17
minimatch@9.0.9:
dependencies:
brace-expansion: 2.1.4
brace-expansion: 2.1.3
minimist@1.2.8: {}
@@ -4344,7 +4342,7 @@ snapshots:
node-int64@0.4.0: {}
node-releases@2.0.53: {}
node-releases@2.0.51: {}
normalize-path@3.0.0: {}
@@ -4481,7 +4479,7 @@ snapshots:
prelude-ls@1.2.1: {}
prettier@3.9.6: {}
prettier@3.9.4: {}
pretty-format@29.7.0:
dependencies:
@@ -4582,7 +4580,7 @@ snapshots:
safer-buffer@2.1.2: {}
sax@1.6.1: {}
sax@1.6.0: {}
semver-greatest-satisfied-range@2.0.0:
dependencies:
@@ -4681,7 +4679,7 @@ snapshots:
strip-ansi@7.2.0:
dependencies:
ansi-regex: 6.3.0
ansi-regex: 6.2.2
strip-bom@4.0.0: {}
@@ -4806,9 +4804,9 @@ snapshots:
undici-types@6.21.0: {}
update-browserslist-db@1.3.1(browserslist@4.28.8):
update-browserslist-db@1.2.3(browserslist@4.28.7):
dependencies:
browserslist: 4.28.8
browserslist: 4.28.7
escalade: 3.2.0
picocolors: 1.1.1
@@ -4939,7 +4937,7 @@ snapshots:
xml2js@0.6.2:
dependencies:
sax: 1.6.1
sax: 1.6.0
xmlbuilder: 11.0.1
xmlbuilder@11.0.1: {}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@mem0/openclaw-mem0",
"version": "1.0.16",
"version": "1.0.15",
"type": "module",
"description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source",
"license": "Apache-2.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@mem0/pi-agent-plugin",
"version": "0.1.5",
"version": "0.1.4",
"type": "module",
"description": "Mem0 memory extension for Pi Agent persistent, scoped, semantic memory across sessions and projects",
"license": "Apache-2.0",
-15
View File
@@ -1,15 +0,0 @@
__pycache__/
*.py[cod]
*.egg-info/
build/
dist/
*.whl
.venv/
venv/
.pytest_cache/
.mypy_cache/
.ruff_cache/
.hatch/
.DS_Store
.idea/
.vscode/
-201
View File
@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [2023] [Taranjeet Singh]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-10
View File
@@ -1,10 +0,0 @@
strands-mem0
Copyright 2026 Mem0
This product includes software developed at Mem0 (https://mem0.ai).
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
-94
View File
@@ -1,94 +0,0 @@
<div align="center">
<h1>strands-mem0</h1>
<h3>Persistent long-term memory for Strands Agents, backed by Mem0</h3>
<p>
A community <a href="https://strandsagents.com/">Strands Agents</a> integration that plugs
<a href="https://mem0.ai">Mem0</a> in as a first-class <code>MemoryStore</code>.
</p>
</div>
---
`strands-mem0` gives [Strands](https://github.com/strands-agents/sdk-python) agents durable memory
that survives across sessions, backed by [Mem0](https://mem0.ai). Where the
`mem0_memory` tool is called explicitly by the model, `Mem0MemoryStore` plugs into the **agent loop**
directly: the manager recalls context and injects it automatically, and writes new memories, either
verbatim or by extracting facts from the conversation.
- **Automatic recall + injection** — relevant memories are searched and prepended to the prompt every turn, no tool call required.
- **Server-side extraction** — raw conversation turns are handed to Mem0, which distills and de-duplicates facts on its own pipeline (no extra client-side model call).
- **Hosted or self-hosted** — the managed [Mem0 Platform](https://app.mem0.ai) by default, or your own Mem0 OSS backend via a config dict.
## Install
```bash
pip install strands-mem0
```
## Usage
```python
from strands import Agent
from strands.memory import MemoryManager
from strands_mem0 import Mem0MemoryStore
# Recall + write, distilling facts from the conversation via Mem0's server-side extraction.
store = Mem0MemoryStore(user_id="alex", writable=True, extraction=True)
agent = Agent(memory_manager=MemoryManager(stores=[store]))
# The agent now recalls from and writes to Mem0 without any explicit tool call.
agent("Remember that I prefer dark-mode dashboards and only drink oat milk.")
agent("How do I like my dashboards?") # recalls the stored preference
```
Set `MEM0_API_KEY` for the hosted platform (get one at [app.mem0.ai](https://app.mem0.ai)), or pass
`api_key=...`. For a self-hosted Mem0 OSS backend, pass a `config=...` dict instead.
## How it works
`Mem0MemoryStore` implements all three `MemoryStore` hooks:
| Method | Maps to | When it runs |
|---|---|---|
| `search(query)` | `mem0.search(query, filters={...})` | Every turn, to recall and inject context |
| `add(content)` | `mem0.add(content, infer=False)` | The `add_memory` tool / a client-side extractor — stores a fact verbatim |
| `add_messages(messages)` | `mem0.add(rendered_turns, infer=True)` | Extraction — renders conversation turns to text, then hands them to Mem0's **server-side** extraction |
Because `add_messages` is implemented, enabling `extraction` routes conversation turns straight to Mem0's own
extraction pipeline. A store that only implemented `add` would instead need a client-side `ModelExtractor`
(an extra model call) to distill facts first.
### Configuration
| Argument | Default | Description |
|---|---|---|
| `user_id` / `agent_id` / `run_id` / `app_id` | _(at least one required)_ | Mem0 entity scope that owns the memories |
| `name` | `"mem0"` | Store identifier, used to target it from memory tools |
| `writable` | `True` | Whether the manager may write to the store |
| `extraction` | `None` | Automatic extraction (`bool` or `ExtractionConfig`) |
| `max_search_results` | `None` | Default result cap per search (falls back to 5) |
| `metadata` | `None` | Default metadata merged into every write |
| `api_key` / `host` | env | Mem0 platform key / base URL (`api_key` defaults to `$MEM0_API_KEY`) |
| `config` | `None` | Mem0 OSS config dict for a self-hosted backend |
## The explicit tool
For the model-called tool (`store` / `retrieve` / `get` / `delete`), use the
[`mem0_memory`](https://github.com/strands-agents/tools) tool from `strands-agents-tools`. The store and
the tool share one Mem0 backend and namespace.
## Development
The package lives under [`python/`](python/) (monorepo-style layout matching the
[Strands extension-template](https://github.com/strands-agents/extension-template)).
```bash
cd python
pip install hatch
hatch run test # pytest (no live server required — mocked client)
hatch run prepare # format + lint + typecheck + test
```
## License
[Apache-2.0](LICENSE). Mem0 is a trademark of its respective owner. Strands Agents is a project of its respective authors.
@@ -1,37 +0,0 @@
# strands-mem0 (Python)
Persistent long-term memory for [Strands Agents](https://github.com/strands-agents/sdk-python),
backed by [Mem0](https://mem0.ai).
See the [repository README](../README.md) for full usage. Quick start:
```bash
pip install strands-mem0
```
As a `MemoryStore` that plugs into the agent loop (Strands >= 1.45):
```python
from strands import Agent
from strands.memory import MemoryManager
from strands_mem0 import Mem0MemoryStore
store = Mem0MemoryStore(user_id="alex", writable=True, extraction=True)
agent = Agent(memory_manager=MemoryManager(stores=[store]))
```
Set `MEM0_API_KEY` for the hosted platform, or pass `config=...` for a self-hosted Mem0 OSS backend.
## Local development
```bash
pip install hatch
hatch run test # pytest (mocked client, no live server)
hatch run prepare # format + lint + typecheck + test
```
## Release
Publish a GitHub release tagged `strands-mem0-v*` (e.g. `strands-mem0-v0.1.0`). The
release router (`.github/workflows/release.yml`) dispatches `strands-mem0-cd.yml`,
which builds the wheel and publishes it to PyPI via trusted publishing (OIDC).
@@ -1,88 +0,0 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "strands-mem0"
version = "0.1.0"
description = "Persistent long-term memory for Strands agents, backed by Mem0."
readme = "README.md"
requires-python = ">=3.10"
license = "Apache-2.0"
authors = [
{name = "Mem0", email = "founders@mem0.ai"}
]
keywords = ["strands", "strands-agents", "agents", "ai", "memory", "mem0", "vector-search", "personalization"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
]
# strands-agents>=1.45.0: first release shipping the `strands.memory` module
# (MemoryStore / MemoryManager) that Mem0MemoryStore implements.
dependencies = [
"strands-agents>=1.45.0",
"mem0ai>=2.0.11",
]
[project.urls]
Homepage = "https://mem0.ai"
Documentation = "https://github.com/mem0ai/mem0/tree/main/integrations/strands-mem0#readme"
Repository = "https://github.com/mem0ai/mem0"
Issues = "https://github.com/mem0ai/mem0/issues"
[project.optional-dependencies]
dev = [
"pytest>=8.0.0,<9.0.0",
"pytest-asyncio>=0.25.0,<1.0.0",
"ruff>=0.11.0,<1.0.0",
"mypy>=1.15.0,<2.0.0",
"hatch",
]
[tool.hatch.build.targets.wheel]
packages = ["src/strands_mem0"]
[tool.hatch.envs.default]
dependencies = [
"pytest>=8.0.0,<9.0.0",
"pytest-asyncio>=0.25.0,<1.0.0",
"ruff>=0.11.0,<1.0.0",
"mypy>=1.15.0,<2.0.0",
]
[tool.hatch.envs.default.scripts]
test = "pytest {args}"
lint = "ruff check src tests"
format = "ruff format src tests"
typecheck = "mypy src"
prepare = ["format", "lint", "typecheck", "test"]
[tool.ruff]
line-length = 120
include = ["src/**/*.py", "tests/**/*.py"]
[tool.ruff.lint]
select = [
"E", # pycodestyle
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
]
[tool.mypy]
python_version = "3.10"
warn_return_any = true
warn_unused_configs = true
ignore_missing_imports = true
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
asyncio_mode = "auto"
@@ -1,26 +0,0 @@
"""Strands Mem0 -- persistent long-term memory for Strands agents, backed by Mem0.
:class:`Mem0MemoryStore` is a Strands ``MemoryStore`` that plugs into the agent
loop via a :class:`~strands.memory.MemoryManager`, with automatic memory injection
and extraction. It implements both write sinks, so ``extraction`` uses Mem0's
server-side extraction (no extra model call).
For the explicit, model-called tool (``store`` / ``retrieve`` / ``get`` / ``delete``),
use the ``mem0_memory`` tool from ``strands-agents-tools``; a store and the tool can
share one Mem0 backend and namespace.
"""
from importlib.metadata import PackageNotFoundError, version
from strands_mem0.client import Mem0ServiceClient
from strands_mem0.store import Mem0MemoryStore
__all__ = [
"Mem0MemoryStore",
"Mem0ServiceClient",
]
try:
__version__ = version("strands-mem0")
except PackageNotFoundError: # pragma: no cover - only when running from a source tree
__version__ = "0.0.0+unknown"

Some files were not shown because too many files have changed in this diff Show More