Compare commits
243 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c05e889c73 | |||
| 9b7001e44c | |||
| fa80bfdb3f | |||
| 57b0846784 | |||
| c9a0c19ac2 | |||
| 19cb89aff4 | |||
| fdfb763d6e | |||
| 0070e08e01 | |||
| 39bc023305 | |||
| b1342a3408 | |||
| b717e38785 | |||
| dc82354e14 | |||
| 4ddee9c51d | |||
| 7e09615571 | |||
| d18e751dec | |||
| 8d5b7865bd | |||
| 9b565da8e3 | |||
| 48d0d0cd9c | |||
| feb12852c0 | |||
| 5af797834c | |||
| 4fa4839077 | |||
| 3599aa75ed | |||
| 1de6499b8a | |||
| ed38ddf873 | |||
| 530d802b55 | |||
| d3334fa5f1 | |||
| 52b02c7cc1 | |||
| 001c235229 | |||
| bf2d591b27 | |||
| b4c50550bf | |||
| 290de24bb8 | |||
| ef6f51d977 | |||
| a10c0cd030 | |||
| 956bf4f88e | |||
| 0f172c2890 | |||
| 696455fd62 | |||
| 9e99eaadbc | |||
| 02ff6c5595 | |||
| a0329f047b | |||
| c50a2bfb8f | |||
| bfb51c6b93 | |||
| a133287015 | |||
| c883f52130 | |||
| 96d45b78c7 | |||
| ba2fb9f4c3 | |||
| 14c431735b | |||
| d70cc00ab3 | |||
| c427a453a8 | |||
| f5b4300449 | |||
| 71f2ebefa3 | |||
| 35a125585e | |||
| 4debc58a83 | |||
| b42cfdd5c8 | |||
| 6fe6140dba | |||
| b05dc2740f | |||
| 4a0a9a92a6 | |||
| beea626f0a | |||
| 3f39fba28f | |||
| 12c47f5249 | |||
| 3f717e5459 | |||
| 18021dd106 | |||
| fad0e0e415 | |||
| 1112be3e5e | |||
| 6052252250 | |||
| deca4bd3a5 | |||
| 4cfcd0241a | |||
| b54710a3c3 | |||
| 4cfa98f626 | |||
| b830b99abe | |||
| 45208feebb | |||
| 3ac9ba4b50 | |||
| 965140eb19 | |||
| 6e6f5b8d59 | |||
| dd54e387de | |||
| fd32b980b4 | |||
| 21aae599be | |||
| ea6fd3b457 | |||
| 8ca9a0f2c0 | |||
| 5f77d86caf | |||
| 9ef409222f | |||
| c90bdbdce0 | |||
| 50bdaaea0c | |||
| 38e47ac261 | |||
| d06ea1875c | |||
| c2c3a12838 | |||
| 07e58c54ae | |||
| cf3355e2ca | |||
| 54328ffd97 | |||
| 29fa41558c | |||
| 8d45fb3c9a | |||
| 760dca6f39 | |||
| 74f6dc6f0d | |||
| bcca72e3f0 | |||
| 9c2d6222ce | |||
| 790e190486 | |||
| d4869d24ec | |||
| 1ac3aa7256 | |||
| e168d48e04 | |||
| ea2ee07586 | |||
| 540d23d610 | |||
| 3274390f82 | |||
| b357a5a1b0 | |||
| d653b63fac | |||
| cc4671579f | |||
| 01afdde3e7 | |||
| d6d89c987b | |||
| c2150e8f1a | |||
| 19c7bb84a2 | |||
| e6281ab724 | |||
| a71d7bdbe3 | |||
| 56ec7d20f1 | |||
| ca2abca2b8 | |||
| 0e582adc6c | |||
| 9caffeaa7b | |||
| a58e0586ad | |||
| a9cb4bb644 | |||
| 7bf84b8d38 | |||
| 5e7adc4d12 | |||
| 14393b5962 | |||
| f590c9596a | |||
| dd5f7e39a8 | |||
| 70ab76a053 | |||
| 2af3a72f73 | |||
| 8005e18aec | |||
| 39551145b8 | |||
| c2bc28e589 | |||
| fec2fe6a2c | |||
| d0c23a5950 | |||
| b05cce581b | |||
| 8a57967c8a | |||
| 756b0b1b6d | |||
| 726bcc80b2 | |||
| 9383e9a255 | |||
| ddaa655edf | |||
| 739534c0a3 | |||
| 633b035342 | |||
| ccbe5861a1 | |||
| 50c3cf44f1 | |||
| d6d2588ef5 | |||
| 6c1741e3a4 | |||
| 42cf18c4e6 | |||
| d89793b666 | |||
| 17836748d7 | |||
| c9af55986e | |||
| f69f8dcc7b | |||
| 28e4d819f8 | |||
| 1d383c4ee2 | |||
| 8488abe603 | |||
| 49863e9a7a | |||
| 770ce97bd9 | |||
| 44bcfbe1f3 | |||
| 33a0ed7559 | |||
| ba9054e0c8 | |||
| df9d5cc4b1 | |||
| 3b2357bfe0 | |||
| 573b20cec8 | |||
| a781800d3f | |||
| 4470803fe5 | |||
| 6a801bfe2f | |||
| 2a4aa232b2 | |||
| 99206f0c64 | |||
| 5dbf071356 | |||
| b26469e006 | |||
| e72ae96ad4 | |||
| fbdbab805d | |||
| 22f70d50e1 | |||
| f89edb45dc | |||
| 846f25bd39 | |||
| 9b04509433 | |||
| 803ff13bb8 | |||
| 94e46526bc | |||
| d122479687 | |||
| cc52f0e367 | |||
| 87276ef968 | |||
| 002fe46ab3 | |||
| 9d36b2c94d | |||
| c944bed460 | |||
| 2cc060fd76 | |||
| 2bc2f763d9 | |||
| 7fb3feb5cd | |||
| fec7cdf118 | |||
| 03b41ab00f | |||
| 4c974c8fa8 | |||
| b0bee551cb | |||
| b8141aaea8 | |||
| a7ecf781cd | |||
| 6dc4606dcf | |||
| 580d390e4d | |||
| 2bd3ff1eff | |||
| cd79fa8914 | |||
| 59484f066f | |||
| 8a5c0729e5 | |||
| 3b9aed866a | |||
| fb2593e10d | |||
| 207f65deda | |||
| f2532f072f | |||
| 41c8f00851 | |||
| a36a392cd3 | |||
| 152d1e66f7 | |||
| bc05fd9623 | |||
| ad7e09851c | |||
| c325bd3b8e | |||
| 2add7fd57d | |||
| b2ff3aeda5 | |||
| 754034abbc | |||
| bedf862d64 | |||
| cc59d122db | |||
| 3619fd77ae | |||
| 2dcb3542f8 | |||
| 31cec11a79 | |||
| 4c0ea22d31 | |||
| f59320df65 | |||
| ad57cbb8d6 | |||
| ee0c38e081 | |||
| d5b64ccec9 | |||
| 8d6b7c1d67 | |||
| b44ce4dcc3 | |||
| e9c930c430 | |||
| 4b39d01ccb | |||
| 49061718bd | |||
| 7e7682a06d | |||
| f38608fb50 | |||
| fb11cdffbb | |||
| ee600705c2 | |||
| f4ccef5157 | |||
| 08da741a31 | |||
| e4efdd2e29 | |||
| a87c9ce367 | |||
| d258b638ef | |||
| bbbfcfea07 | |||
| fbef369b91 | |||
| a7ed68e697 | |||
| 8a92cf0306 | |||
| 0fbbb2f525 | |||
| 818c2981b7 | |||
| 1f66aadfa3 | |||
| b91c745fbc | |||
| af70668308 | |||
| 890473f891 | |||
| d2ff83cf72 | |||
| 0e02effaf7 | |||
| 9269a0ad6e | |||
| 3d06006f36 |
@@ -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.11"
|
||||
"version": "0.2.15"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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.11"
|
||||
"version": "0.2.15"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
# CI/CD and repository automation (`.github/`)
|
||||
|
||||
> **Do not modify any workflow without explicit approval from a maintainer.** Publishing
|
||||
> credentials are bound to workflow filenames, and the gate workflows decide whether
|
||||
> contributions are accepted. Read this file before proposing any change here.
|
||||
|
||||
## CI: one gate, many pipelines
|
||||
|
||||
`ci-gate.yml` (**CI Gate**) is the single entry point. It runs on every PR, detects which packages changed, and calls only the relevant package workflows as reusable workflows (`workflow_call`). Its final `CI Gate` job aggregates the results: skipped pipelines pass, failed or cancelled ones fail. It is the **only CI status check that needs to be required** in branch protection.
|
||||
|
||||
Package workflows keep their own push-to-main and manual triggers. Their `pull_request` triggers live in the gate's path filters instead.
|
||||
|
||||
| Workflow | File | Standalone triggers | Runs |
|
||||
|----------|------|---------------------|------|
|
||||
| CI Gate | `ci-gate.yml` | All PRs | Routes to and aggregates everything below |
|
||||
| Python SDK | `ci.yml` | Push to main | Ruff + pytest on Python 3.10, 3.11, 3.12 |
|
||||
| TypeScript SDK | `ts-sdk-ci.yml` | Push to main (`mem0-ts/`) | Prettier + build + jest on Node 20, 22 |
|
||||
| Python CLI | `cli-python-ci.yml` | Push to main (`cli/python/`), manual | Ruff + pytest + hatch build on Python 3.10, 3.11, 3.12 |
|
||||
| Node CLI | `cli-node-ci.yml` | Push to main (`cli/node/`), manual | Biome + tsc + vitest + tsup on Node 20, 22 |
|
||||
| OpenClaw | `openclaw-checks.yml` | Push to main (`integrations/openclaw/`), manual | tsc + vitest (Codecov) + tsup on Node 20, 22 |
|
||||
| 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 | `deepseek-plugin-checks.yml` | Push to main (`integrations/deepseek-plugin/`), 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 |
|
||||
| mem0-strands | `mem0-strands-checks.yml` | Push to main (`integrations/mem0-strands/`), 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:
|
||||
|
||||
| Context | Posted by | Why |
|
||||
|---------|-----------|-----|
|
||||
| `CI Gate` | `ci-gate.yml` | Aggregates every package pipeline |
|
||||
| `license/cla` | CLA Assistant | Proves the CLA is signed, not merely requested |
|
||||
|
||||
Editing the ruleset requires repo **admin**. `maintain` is not enough, and the API returns 404 rather than 403 in that case. Until `license/cla` is required, the claim in `CONTRIBUTING.md` that unsigned PRs are blocked from merging holds by convention only.
|
||||
|
||||
Requiring `CI Gate` also means fork PRs from first-time contributors cannot merge until a maintainer approves the workflow run. Those sit at `action_required`, which is intended behavior.
|
||||
|
||||
## CD: one router, many publishers
|
||||
|
||||
`release.yml` (**Release Router**) is the only workflow listening to `release: published`. It matches the tag prefix and dispatches the matching package workflow through `workflow_dispatch`, so one release produces exactly one routed run.
|
||||
|
||||
| Workflow | File | Tag prefix | Target |
|
||||
|----------|------|------------|--------|
|
||||
| Release Router | `release.yml` | all releases | dispatches the rows below |
|
||||
| Python SDK | `cd.yml` | `v*` | PyPI (`mem0ai`) |
|
||||
| TypeScript SDK | `ts-sdk-cd.yml` | `ts-v*` | npm (`mem0ai`) |
|
||||
| Python CLI | `cli-python-cd.yml` | `cli-v*` | PyPI (`mem0-cli`) |
|
||||
| Node CLI | `cli-node-cd.yml` | `cli-node-v*` | npm (`@mem0/cli`) |
|
||||
| Vercel AI SDK | `vercel-ai-cd.yml` | `vercel-ai-v*` | npm (`@mem0/vercel-ai-provider`) |
|
||||
| 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 | `deepseek-plugin-cd.yml` | `deepseek-plugin-v*` | npm (`@mem0/deepseek-plugin`) |
|
||||
| n8n Node | `n8n-nodes-mem0-cd.yml` | `n8n-nodes-mem0-v*` | npm (`@mem0/n8n-nodes-mem0`) |
|
||||
| mem0-strands | `mem0-strands-cd.yml` | `mem0-strands-v*` | PyPI (`mem0-strands`) |
|
||||
|
||||
- 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.
|
||||
- Registry trusted-publisher settings are pinned to each package's own workflow **filename**. Renaming a CD workflow breaks publishing for that package.
|
||||
- First publish of a new npm package must be done manually. OIDC works from the second version onward.
|
||||
- To re-publish a release, do **not** delete and recreate the GitHub release. Dispatch the workflow directly: `gh workflow run <package>-cd.yml --ref refs/tags/<tag> -f tag=<tag>`.
|
||||
- The Zapier app deploys to Zapier's platform, not npm, so it is not in the router. Deploy with `gh workflow run zapier-mem0-cd.yml --ref main`.
|
||||
- Adding a package: add its CD workflow, then register its tag prefix in the `case` block in `release.yml`, keeping the bare `v*` arm last.
|
||||
|
||||
## Contribution gates
|
||||
|
||||
| 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. |
|
||||
| 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 |
|
||||
| llms.txt Check | `docs-llms-txt-check.yml` | Blocks PRs touching `docs/**/*.mdx` when `docs/llms.txt` is out of sync |
|
||||
|
||||
`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.
|
||||
|
||||
## Issue forms and templates
|
||||
|
||||
`ISSUE_TEMPLATE/*.yml` are GitHub issue **forms**, not markdown templates. Only forms support `required: true` and machine-parseable field ids. Blank issues are disabled in `config.yml`.
|
||||
|
||||
`issue-labeler.yml` reads only the `component` field id through `stefanbuck/github-issue-parser` and `redhat-plumbers-in-action/advanced-issue-labeler`, so adding new field ids is safe. Renaming `component` is not.
|
||||
|
||||
Current field ids:
|
||||
|
||||
| Form | Ids |
|
||||
|------|-----|
|
||||
| `bug_report.yml` | `component`, `description`, `verification`, `ai_assistance` |
|
||||
| `feature_request.yml` | `component`, `description`, `ai_assistance` |
|
||||
| `documentation_issue.yml` | `description`, `ai_assistance` |
|
||||
|
||||
## Trust list
|
||||
|
||||
`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.
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
AGENTS.md
|
||||
@@ -9,12 +9,10 @@ body:
|
||||
label: Component
|
||||
description: Which part of mem0 is affected?
|
||||
options:
|
||||
- Core / Python SDK
|
||||
- Python SDK
|
||||
- TypeScript SDK
|
||||
- Vector Store (Qdrant, PGVector, Redis, Chroma, etc.)
|
||||
- Graph Memory (Neo4j, Memgraph, etc.)
|
||||
- Ollama / Local Models
|
||||
- OpenClaw
|
||||
- Vector Store
|
||||
- Plugin
|
||||
- REST API
|
||||
- Other
|
||||
validations:
|
||||
@@ -53,3 +51,43 @@ body:
|
||||
- OS:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: verification
|
||||
attributes:
|
||||
label: How You Verified This
|
||||
description: We only take on bugs someone has actually reproduced. Show your work.
|
||||
value: |
|
||||
### What I Ran
|
||||
|
||||
The exact command or script, and where it ran.
|
||||
|
||||
### What I Saw
|
||||
|
||||
The real output, log line, or traceback. Paste it, do not describe it.
|
||||
|
||||
### Why This Is a Bug
|
||||
|
||||
What should have happened instead, and what says so: a docs link, a
|
||||
docstring, a test, or the code itself.
|
||||
|
||||
### What I Ruled Out
|
||||
|
||||
Anything you checked that turned out not to be the cause.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: ai_assistance
|
||||
attributes:
|
||||
label: AI Assistance
|
||||
description: >-
|
||||
This asks how the bug was found and confirmed, not how the text was
|
||||
written. Drafting the write-up with AI is fine. We ask because it tells
|
||||
us how much to trust the reproduction, not because it counts against you.
|
||||
options:
|
||||
- No AI involved
|
||||
- AI helped me find it, and I reproduced it myself afterwards
|
||||
- AI found and wrote this, and I have not reproduced it myself
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
blank_issues_enabled: true
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Discord Community
|
||||
- name: Question or general help
|
||||
url: https://discord.gg/6PzXDgEjG5
|
||||
about: Ask questions and discuss with the community
|
||||
about: Ask on Discord. The issue tracker is for bugs and accepted work only.
|
||||
- name: Documentation
|
||||
url: https://docs.mem0.ai
|
||||
about: Read the official mem0 documentation
|
||||
- name: Report a security vulnerability
|
||||
url: https://github.com/mem0ai/mem0/security/advisories/new
|
||||
about: Report privately through a security advisory. Never open a public issue.
|
||||
|
||||
@@ -21,3 +21,17 @@ body:
|
||||
How should the docs be improved?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: ai_assistance
|
||||
attributes:
|
||||
label: AI Assistance
|
||||
description: >-
|
||||
This asks how the problem was found, not how the text was written.
|
||||
Drafting the write-up with AI is fine.
|
||||
options:
|
||||
- No AI involved, I hit this reading the docs
|
||||
- AI-assisted, but I checked the page myself
|
||||
- AI found this, and I have not opened the page
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -9,14 +9,11 @@ body:
|
||||
label: Component
|
||||
description: Which part of mem0 does this relate to?
|
||||
options:
|
||||
- Core / Python SDK
|
||||
- Python SDK
|
||||
- TypeScript SDK
|
||||
- Vector Store (Qdrant, PGVector, Redis, Chroma, etc.)
|
||||
- Graph Memory (Neo4j, Memgraph, etc.)
|
||||
- Ollama / Local Models
|
||||
- OpenClaw
|
||||
- Vector Store
|
||||
- Plugin
|
||||
- REST API
|
||||
- Benchmarks / Evals
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
@@ -39,3 +36,18 @@ body:
|
||||
Any workarounds you've tried or other approaches considered.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: ai_assistance
|
||||
attributes:
|
||||
label: AI Assistance
|
||||
description: >-
|
||||
This asks where the idea came from, not how the text was written.
|
||||
Drafting the write-up with AI is fine. A request you hit yourself while
|
||||
building something carries more weight than one a model suggested.
|
||||
options:
|
||||
- No AI involved, this is a need I hit myself
|
||||
- AI-assisted, but the need is mine
|
||||
- AI suggested this feature
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -14,6 +14,18 @@ Closes #<!-- issue number -->
|
||||
- [ ] Refactor (no functional changes)
|
||||
- [ ] Documentation update
|
||||
|
||||
## AI Assistance
|
||||
|
||||
<!-- This is about the code, not this description. Writing the description with AI is fine. -->
|
||||
|
||||
- [ ] No AI assistance
|
||||
- [ ] AI-assisted (autocomplete, or I asked a model questions while writing this)
|
||||
- [ ] AI-generated (an agent wrote most or all of this diff)
|
||||
|
||||
<!-- If you ticked either AI box, name the tool and what you checked yourself. -->
|
||||
|
||||
- [ ] **I can explain every line of this diff and how it interacts with the rest of the codebase, without asking an AI tool.**
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
<!-- If this is a breaking change, describe what breaks and the migration path. Delete this section if not applicable. -->
|
||||
|
||||
@@ -0,0 +1,427 @@
|
||||
# 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.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# Syntax:
|
||||
# - One handle per line (without @), sorted alphabetically.
|
||||
# - Optionally specify platform: `platform:username` (e.g. `github:mitchellh`).
|
||||
# - To denounce a user, prefix with minus: `-username`.
|
||||
# - Optionally add a note after a space following the handle.
|
||||
#
|
||||
# Maintainers vouch by commenting "!vouch @username" on any issue, and denounce
|
||||
# with "!denounce @username". The bot commits the change back to this file.
|
||||
#
|
||||
# Seeded on 2026-08-12 from every author with at least one merged pull request,
|
||||
# then filtered: accounts with a merge rate at or below 16% across six or more
|
||||
# attempts were dropped, since landing one change out of many is the signature
|
||||
# of automated submission rather than contribution. Removal is not a ban. Any
|
||||
# maintainer can !vouch these accounts back in.
|
||||
1MikeMakuch
|
||||
aaishikdutta
|
||||
Aarkin7
|
||||
abdullahirfann
|
||||
AbdurNawaz
|
||||
abhay-codes07
|
||||
Abhineshhh
|
||||
ac12644
|
||||
acarbonetto
|
||||
adh-wonolo
|
||||
Aditya-Tripuraneni
|
||||
aesher9o1
|
||||
agumpandey
|
||||
ahnedeee
|
||||
ajmalmohad
|
||||
AkisAya
|
||||
akshat1423
|
||||
akshseh
|
||||
alessandropanzieri
|
||||
alohays
|
||||
aloktripathi1
|
||||
amahuli03
|
||||
amanagarwal042
|
||||
Ameysr
|
||||
amjadraza
|
||||
anantoj
|
||||
anchit-nishant
|
||||
andrewghlee
|
||||
andy-k-improving
|
||||
anifort
|
||||
anishesg
|
||||
AnkushMalaker
|
||||
AnnaSuSu
|
||||
anujshandillya
|
||||
ArchishmanSengupta
|
||||
Arsh-mem0
|
||||
ArthurHoward1
|
||||
aryankhanna475
|
||||
Ashu463
|
||||
atahanyild
|
||||
AtharvaJaiswal005
|
||||
atkinsh
|
||||
avp1598
|
||||
axelray-dev
|
||||
ayaangazali
|
||||
aymenkrifa
|
||||
barry166
|
||||
being-abhi
|
||||
berwinjoule
|
||||
BillionClaw
|
||||
bioshazard
|
||||
bisla
|
||||
bkidd1
|
||||
blino
|
||||
bmsvinci1729
|
||||
boss-mao
|
||||
Br1an67
|
||||
brucewkz
|
||||
cachho
|
||||
caifeizhi
|
||||
candidosales
|
||||
cclauss
|
||||
chaithanyak42
|
||||
chinnuabey
|
||||
ChiragArora31
|
||||
ChrisFloofyKitsune
|
||||
chrisqu777
|
||||
clementantonyk
|
||||
codexvn
|
||||
Colsrch
|
||||
CrepuscularIRIS
|
||||
ctxlong
|
||||
danielsiwiec
|
||||
darkhaniop
|
||||
davidatorres
|
||||
deshraj
|
||||
Dev-Khant
|
||||
Devan019
|
||||
deven298
|
||||
devYRPauli
|
||||
DhanushNehru
|
||||
DhilipBinny
|
||||
Dhravya
|
||||
dimigerontaki
|
||||
Diveyam-Mishra
|
||||
divyansh-1009
|
||||
Divyanshu9822
|
||||
dog-last
|
||||
DrJsPBs
|
||||
dtee1
|
||||
DumoeDss
|
||||
e-biswas
|
||||
Echo3ToEcho7
|
||||
eldar702
|
||||
eltociear
|
||||
EnzoFanAccount
|
||||
Esparon1
|
||||
Fahmid-Arman
|
||||
Failfail2603
|
||||
FarukhS52
|
||||
farzad528
|
||||
felipeavilis
|
||||
femto
|
||||
fengjikui
|
||||
fenilfaldu
|
||||
fileames
|
||||
Flyfoxs
|
||||
fmercurio
|
||||
FoliageOwO
|
||||
fran3cc
|
||||
frank-zsy
|
||||
frederikb96
|
||||
Freshield
|
||||
freya0926
|
||||
G26karthik
|
||||
gabe-l-hart
|
||||
gabrielstein-mem0
|
||||
gajazlikovac
|
||||
gasolin
|
||||
gaurav0107
|
||||
gauravagerwala
|
||||
Genarojrsanchez
|
||||
ghdcksgml1
|
||||
GingerMoon
|
||||
gmdorf
|
||||
golemus
|
||||
GongRzhe
|
||||
GopalGB
|
||||
Gyubin
|
||||
haarishmk26
|
||||
hackice20
|
||||
halanm
|
||||
hardik1408
|
||||
Harin329
|
||||
harshgupta-mem0
|
||||
harshpandit007
|
||||
hayescode
|
||||
hcsum
|
||||
he-yufeng
|
||||
heng-ah
|
||||
Hexecu
|
||||
Himanshu-Sangshetti
|
||||
hjlarry
|
||||
HowieG
|
||||
HrushiYadav
|
||||
HScarb
|
||||
huveewomg
|
||||
Hybirdss
|
||||
ianupamsingh
|
||||
IgnazioDS
|
||||
immuhammadfurqan
|
||||
into-the-night
|
||||
invincible04
|
||||
Itz-Antaripa
|
||||
ixchio
|
||||
Jaco-Ren
|
||||
Jai0401
|
||||
JainamShah-22
|
||||
Jainish-S
|
||||
jarediaz
|
||||
jeanibarz
|
||||
Jerry-Terrasse
|
||||
jessai2026
|
||||
jesse-c
|
||||
jfeng18
|
||||
jferrettiboke
|
||||
jjjojoj
|
||||
joaomdmoura
|
||||
JoeSL
|
||||
johnwlockwood
|
||||
jonasiwnl
|
||||
josephchancey
|
||||
juananpe
|
||||
juaneloDev
|
||||
junmo1215
|
||||
Jupiter363
|
||||
KapilM26
|
||||
karthik-indla
|
||||
KarthikeyaKollu
|
||||
kartik-mem0
|
||||
katarinasupe
|
||||
ketangangal
|
||||
kimnamu
|
||||
kindertheo
|
||||
kirex0
|
||||
kirklin
|
||||
kk2211
|
||||
kmitul
|
||||
koi646
|
||||
kratos0718
|
||||
krescent
|
||||
Krishnachaitanyakc
|
||||
kriszlazar
|
||||
KushagraB424
|
||||
l1anch1
|
||||
lamost423
|
||||
lan17
|
||||
LeonieFreisinger
|
||||
lh0x00
|
||||
limboinf
|
||||
liviaellen
|
||||
longway-code
|
||||
lsvishaal
|
||||
LuciAkirami
|
||||
lucifertrj
|
||||
lvpx
|
||||
ly-wang19
|
||||
maamalama
|
||||
maccuryj
|
||||
mae5357
|
||||
mahone3297
|
||||
Malhis
|
||||
maljazaery
|
||||
manganeseheptoxide
|
||||
manthanguptaa
|
||||
mark-watson
|
||||
Mark-Zeng
|
||||
markmbain
|
||||
matanco1
|
||||
mauricioalarcon
|
||||
maxvonhippel
|
||||
me-tusharchandra
|
||||
mezotv
|
||||
MgeeeeK
|
||||
mggger
|
||||
mgoulart
|
||||
microbluey
|
||||
mikejgray
|
||||
Mingxiangyu
|
||||
Mini256
|
||||
misrasaurabh1
|
||||
mjzcng
|
||||
mogith-pn
|
||||
morgoth9808
|
||||
moyueheng
|
||||
mrbusche
|
||||
Mrinank-Bhowmick
|
||||
muhammed-mamun
|
||||
MUZAMMILPERVAIZ
|
||||
mvanhorn
|
||||
naman09
|
||||
NavyaAlapati13
|
||||
neilbhutada
|
||||
NightClover-code
|
||||
nikhilsharma26500
|
||||
NILAY1556
|
||||
niv-hertz
|
||||
NoahStapp
|
||||
norrishuang
|
||||
OfficialAbhinavSingh
|
||||
officialasishkumar
|
||||
OjusWiZard
|
||||
okaditya84
|
||||
omahs
|
||||
OsamaNabih
|
||||
oskarrough
|
||||
p-tirth
|
||||
Padarn
|
||||
paipeline
|
||||
ParseDark
|
||||
parshvadaftari
|
||||
Parteeksachdeva
|
||||
parthshr370
|
||||
parzival418
|
||||
Paulie-Aditya
|
||||
paurushmittal
|
||||
pc9
|
||||
Pecunia201
|
||||
peterj
|
||||
pragnyanramtha
|
||||
PranavPuranik
|
||||
PrashantDixit0
|
||||
prateekchhikara
|
||||
prathameshagrawal
|
||||
pratikgajjar
|
||||
PratikRai0101
|
||||
Prikshit7766
|
||||
Prithvi1994
|
||||
QunBB
|
||||
rafid001
|
||||
raghavtyagii
|
||||
rahulsharmavishwakarma
|
||||
rajib76
|
||||
rakheesingh
|
||||
ranjithkumar8352
|
||||
Rayhanpatel
|
||||
reachAnushaKondam
|
||||
Real5K
|
||||
Rhythm-08
|
||||
richawo
|
||||
Rishiraj2594
|
||||
RitwijParmar
|
||||
RobinALG87
|
||||
rocke2020
|
||||
rodboev
|
||||
rohitgr7
|
||||
ron-42
|
||||
roshan-shaik-ml
|
||||
rst0070
|
||||
rudra717
|
||||
rudrajmehta-mem0
|
||||
rupamoraczen
|
||||
rupeshbansal
|
||||
ryanrozich
|
||||
SaharshPatel24
|
||||
sahilyadav902
|
||||
sahithreddy05
|
||||
SakshiSrivastava2024
|
||||
SamuelDevdas
|
||||
sarkarsaurabh27
|
||||
sdht0
|
||||
seetharam-rajagopal
|
||||
sergio-toro
|
||||
SerSamgy
|
||||
shafdev
|
||||
shashank42
|
||||
ShauryaaSharma
|
||||
Sheharyar570
|
||||
shenxiangzhuang
|
||||
ShivamMenda
|
||||
shlokkhemani
|
||||
shraderdm
|
||||
shrivastavanolo
|
||||
shubhampal123
|
||||
shuoli84
|
||||
sidmohanty11
|
||||
siroa
|
||||
slobodaapl
|
||||
soapun
|
||||
soumil-rathi
|
||||
spike-spiegel-21
|
||||
srishti-git1110
|
||||
SSDWGG
|
||||
sssserrano
|
||||
subhadip001
|
||||
subhajit20
|
||||
SudoAnirudh
|
||||
sukkritsharmaofficial
|
||||
sw8fbar
|
||||
swarnaprakash
|
||||
sxu75374
|
||||
SZemse
|
||||
taranjeet
|
||||
techcontributor
|
||||
tgabi333
|
||||
theagenticguy
|
||||
thomasgtaylor
|
||||
tomasonjo
|
||||
TommyZihao
|
||||
TruptiAgrawal
|
||||
turtletongue
|
||||
Tushar-kalsi
|
||||
Ukong0324
|
||||
umran666
|
||||
utkarsh240799
|
||||
UzairNaeem3
|
||||
V-Silpin
|
||||
vatsalrathod16
|
||||
vedant381
|
||||
veeceey
|
||||
vgvoleg
|
||||
VictorECDSA
|
||||
VikramIyer125
|
||||
Vir-8
|
||||
vsatyamuralikrishna
|
||||
vuonghuuhung
|
||||
WayneCao
|
||||
whysosaket
|
||||
wobushixiaoj
|
||||
xiangpingjiang
|
||||
XiaojuCH
|
||||
xu-xiang
|
||||
xyb
|
||||
yashikabadaya
|
||||
yashs33244
|
||||
ygorth
|
||||
youneshima
|
||||
ytkimirti
|
||||
YuriyTW
|
||||
YusukeJustinNakajima
|
||||
zaiddkhan
|
||||
zegerhoogeboom
|
||||
zinyando
|
||||
Zlo7
|
||||
Zncl2222
|
||||
zzaym
|
||||
@@ -1,18 +1,15 @@
|
||||
# Maps dropdown selections to GitHub labels
|
||||
# Used by the advanced-issue-labeler GitHub Action
|
||||
|
||||
component:
|
||||
- label: "sdk-python"
|
||||
matcher: "Core / Python SDK"
|
||||
- label: "sdk-typescript"
|
||||
matcher: "TypeScript SDK"
|
||||
- label: "vector-store"
|
||||
matcher: "Vector Store"
|
||||
- label: "graph-memory"
|
||||
matcher: "Graph Memory"
|
||||
- label: "ollama"
|
||||
matcher: "Ollama"
|
||||
- label: "openclaw"
|
||||
matcher: "OpenClaw"
|
||||
- label: "rest-api"
|
||||
matcher: "REST API"
|
||||
policy:
|
||||
- section:
|
||||
- id: ['component']
|
||||
block-list: ['Other']
|
||||
label:
|
||||
- name: 'sdk-python'
|
||||
keys: ['Python SDK']
|
||||
- name: 'sdk-typescript'
|
||||
keys: ['TypeScript SDK']
|
||||
- name: 'vector-store'
|
||||
keys: ['Vector Store']
|
||||
- name: 'plugin'
|
||||
keys: ['Plugin']
|
||||
- name: 'rest-api'
|
||||
keys: ['REST API']
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"language": {
|
||||
"sdk-python": [
|
||||
"python", "pip install", "pypi", "pyproject", "requirements.txt",
|
||||
"from mem0", "import mem0", "traceback", "pydantic", "asyncmemory",
|
||||
"poetry", "virtualenv", "venv", "conda", "pytest", "async def"
|
||||
],
|
||||
"sdk-typescript": [
|
||||
"typescript", "javascript", "pnpm", "yarn", "node.js", "nodejs",
|
||||
"mem0-ts", "mem0ai/oss", "tsconfig", "await import",
|
||||
"=> {", "undefined is not"
|
||||
]
|
||||
},
|
||||
"area": {
|
||||
"plugin": [
|
||||
"openclaw", "openclaw-mem0", "openclaw.json", "openclaw plugin",
|
||||
"claude code", "opencode", "pi agent", "mem0-plugin",
|
||||
"cursor plugin", "codex plugin", "editor plugin"
|
||||
],
|
||||
"cli": ["mem0-cli", "@mem0/cli", "npx mem0", "command line"],
|
||||
"vector-store": [
|
||||
"pgvector", "pinecone", "chroma", "chromadb", "weaviate",
|
||||
"milvus", "faiss", "vector store", "vectorstore",
|
||||
"elasticsearch", "supabase", "azure ai search",
|
||||
"s3 vectors", "mongodb"
|
||||
],
|
||||
"integrations": [
|
||||
"vercel ai", "vercel-ai-sdk", "@mem0/vercel-ai-provider",
|
||||
"llamaindex", "crewai", "autogen", "langgraph"
|
||||
],
|
||||
"rest-api": [
|
||||
"rest api", "fastapi", "docker-compose", "/v1/memories",
|
||||
"localhost:8000", "localhost:8888", "curl -x", "http endpoint"
|
||||
],
|
||||
"documentation": [
|
||||
"docs.mem0.ai", "documentation", "typo", "readme", "docstring", "broken link",
|
||||
"issue on docs", "docs:", "link to the docs page", "issue with current documentation"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
sdk-python:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- 'mem0/**'
|
||||
- 'tests/**'
|
||||
- 'cli/python/**'
|
||||
- 'pyproject.toml'
|
||||
- 'poetry.lock'
|
||||
|
||||
sdk-typescript:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- 'mem0-ts/**'
|
||||
- 'cli/node/**'
|
||||
|
||||
vector-store:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- 'mem0/vector_stores/**'
|
||||
- 'mem0-ts/src/oss/src/vector_stores/**'
|
||||
|
||||
rest-api:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'server/**'
|
||||
|
||||
integrations:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'integrations/**'
|
||||
|
||||
plugin:
|
||||
- changed-files:
|
||||
- all-globs-to-any-file:
|
||||
- 'integrations/**'
|
||||
- '!integrations/vercel-ai-sdk/**'
|
||||
- any-glob-to-any-file:
|
||||
- 'skills/**'
|
||||
- '.agents/**'
|
||||
- '.claude-plugin/**'
|
||||
- '.codex-plugin/**'
|
||||
- '.cursor-plugin/**'
|
||||
- 'marketplace.json'
|
||||
|
||||
cli:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'cli/**'
|
||||
|
||||
documentation:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- 'docs/**'
|
||||
- 'examples/**'
|
||||
- '*.md'
|
||||
|
||||
ci:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- '.github/**'
|
||||
- 'scripts/**'
|
||||
- '.pre-commit-config.yaml'
|
||||
@@ -0,0 +1,44 @@
|
||||
const fs = require('fs');
|
||||
|
||||
function componentLabels(keywords) {
|
||||
return Object.values(keywords).flatMap(Object.keys);
|
||||
}
|
||||
|
||||
function toMatcher(term) {
|
||||
const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const prefix = /^[a-z0-9]/i.test(term) ? '\\b' : '';
|
||||
return new RegExp(prefix + escaped, 'i');
|
||||
}
|
||||
|
||||
function scoreGroup(text, group) {
|
||||
let winner = null;
|
||||
let best = 0;
|
||||
for (const [label, terms] of Object.entries(group)) {
|
||||
const score = terms.reduce((n, term) => n + (toMatcher(term).test(text) ? 1 : 0), 0);
|
||||
if (score > best) {
|
||||
winner = label;
|
||||
best = score;
|
||||
}
|
||||
}
|
||||
return winner;
|
||||
}
|
||||
|
||||
const UMBRELLA = { plugin: 'integrations' };
|
||||
|
||||
function inferComponentLabels(text, keywords) {
|
||||
if (!text) return [];
|
||||
const labels = [scoreGroup(text, keywords.language), scoreGroup(text, keywords.area)].filter(
|
||||
Boolean,
|
||||
);
|
||||
for (const label of labels.slice()) {
|
||||
const parent = UMBRELLA[label];
|
||||
if (parent && !labels.includes(parent)) labels.push(parent);
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
|
||||
function loadKeywords(file) {
|
||||
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
}
|
||||
|
||||
module.exports = { componentLabels, inferComponentLabels, loadKeywords };
|
||||
@@ -0,0 +1,102 @@
|
||||
const assert = require('assert');
|
||||
const path = require('path');
|
||||
const { inferComponentLabels, loadKeywords } = require('./infer-component-labels.js');
|
||||
|
||||
const keywords = loadKeywords(path.join(__dirname, '..', 'component-keywords.json'));
|
||||
|
||||
const cases = [
|
||||
{
|
||||
number: 6210,
|
||||
title: "but(anthropic): sampling parameters returns 400 error for new model",
|
||||
body: "### Component\n\nCore / Python SDK\n\n### Description\n\n### Summary\n\nWhen using Anthropic latest models such as `claude-opus-4-7`, `claude-opus-4-8`, or `claude-sonnet-5`, Mem0 still sends sampling parameters like `temperature` / `top_p`. These models do not support those parameters, causing Anthropic API requests to fail.\n\nSee https://platform.claude.com/docs/en/about-claude/models/migration-guide\n\n### Steps to Reproduce\n\n```python\n from mem0 import Memory\n\n m = Memory.from_config({\n \"llm\": {\n \"provider\": \"anthropic\",\n \"config\": {\n \"model\": \"claude-opus-4-8\",\n \"api_key\": \"your-anthropic-api-key\"\n },\n },\n ...\n })\n```\n\n### Expected Behavior\n\nMem0 should detect Anthropic models that do not support sampling parameters and omit temperature and top_p from the request.\n\nFor models that still support sampling parameters, such as claude-opus-4-6, claude-sonnet-4-6, and claude-haiku-4-5, Mem0 should continue sending supported sampling parameters till they're deprecated.\n\n### Actual Behavior\n\nMem0 includes temperature by default for Anthropic requests. With newer Anthropic models that do not support sampling parameters, the API request fails because unsupported parameters are sent.\n\n### Environment\n\n - mem0 version: 2.0.11\n - Python/Node version: Python 3.11\n - OS: macOS\n",
|
||||
expected: ["sdk-python"],
|
||||
},
|
||||
{
|
||||
number: 5770,
|
||||
title: "feat(ts-sdk): add FastEmbed embedding provider",
|
||||
body: "## Summary\n\nThe Python SDK supports **FastEmbed** as an embedding provider, but the TypeScript OSS SDK (`mem0ai/oss`) does not. Add it to bring the TS SDK to parity.\n\n| | |\n|---|---|\n| Python reference | `mem0/embeddings/fastembed.py` |\n| Registered in (Python) | `mem0/utils/factory.py` (EmbedderFactory) |\n| Target file (TypeScript) | `mem0-ts/src/oss/src/embeddings/fastembed.ts` |\n| Suggested implementation | Use the `fastembed` npm package (ONNX local embeddings). |\n\n## Requirements\n\n- [ ] Implement `FastEmbedEmbedder` in `mem0-ts/src/oss/src/embeddings/fastembed.ts`, extending `Embedder` (`mem0-ts/src/oss/src/embeddings/base.ts`) and mirroring the Python provider's behavior (embed / embedBatch).\n- [ ] Register the `\"fastembed\"` provider in `mem0-ts/src/oss/src/utils/factory.ts` (EmbedderFactory).\n- [ ] Add config typing in `mem0-ts/src/oss/src/types/`.\n- [ ] Add a unit test under `mem0-ts/src/oss/src/tests/`.\n- [ ] Add `fastembed` to `mem0-ts/package.json` (optional/peer dependency, lazy-imported like other providers).\n- [ ] Update docs under `docs/` if this provider is user-facing.\n\n## Reference pattern\n\nMirror an existing TS provider: `embeddings/openai.ts`.\n\n## Notes\n\n`fastembed` (v2.x) is the JS port of Qdrant's FastEmbed — local/offline embeddings. Mirror the default model in `mem0/embeddings/fastembed.py`.\n\n---\n_Part of the TypeScript ↔ Python SDK provider-parity effort. One provider per issue (atomic)._\n",
|
||||
expected: ["sdk-typescript"],
|
||||
},
|
||||
{
|
||||
number: 3940,
|
||||
title: "Milvus database will return distance not similarity score",
|
||||
body: "### 🐛 Describe the bug\n\nMilvus database will return distance not similarity score\n\n## in milvus.py\n\ndef _parse_output(self, data: list):\n \"\"\"\n Parse the output data.\n\n Args:\n data (Dict): Output data.\n\n Returns:\n List[OutputData]: Parsed output data.\n \"\"\"\n memory = []\n\n for value in data:\n uid, score, metadata = (\n value.get(\"id\"),\n value.get(\"distance\"), # here\n value.get(\"entity\", {}).get(\"metadata\"),\n )\n\n memory_obj = OutputData(id=uid, score=score, payload=metadata)\n memory.append(memory_obj)\n\n return memory\n",
|
||||
expected: ["vector-store"],
|
||||
},
|
||||
{
|
||||
number: 5290,
|
||||
title: "Recall search failed: Bad Request Using OpenAI Embedding Model",
|
||||
body: "### Component\n\nOpenClaw\n\n### Description\n\n### Summary\nuse openclaw.json config:\n\n```json\n...\n\"embedder\": {\n \"provider\": \"openai\",\n \"config\": {\n \"model\": \"bge-base-zh-v1.5\",\n \"embedding_dims\": 1024,\n \"embeddingDims\": 1024,\n \"url\": \"https://xxxxxxxxx/v1\",\n \"apiKey\": \"xxxxxxxxxxxx\"\n }\n },\n\"vectorStore\": {\n \"provider\": \"qdrant\",\n \"config\": {\n \"url\": \"http://qdrant:6333\",\n \"apiKey\": \"${QDRANT_API_KEY}\",\n \"collectionName\": \"mem0\",\n \"embeddingModelDims\": 1024\n }\n }\n```\n```\n\nopenclaw log info is:\n\n```\n23:14:20 Api key is used with unsecure connection.\n23:14:21 [mem0] Recall search failed: Bad Request\n23:14:21 [plugins] openclaw-mem0: skills-mode recall (strategy=smart) injecting 0 memories (~20 tokens)\n23:14:22 [ws] ⇄ res ✓ sessions.list 256ms conn=d1eb9bc4…17da id=201b8113…c9dc\n23:14:22 [ws] ⇄ res ✓ sessions.list 264ms conn=d1eb9bc4…17da id=4939f962…2f16\n23:14:34 [ws] ⇄ res ✓ sessions.list 250ms conn=d1eb9bc4…17da id=f7ad503f…baa6\n23:15:12 [mem0] **Recall search failed: Bad Request**\n23:15:12 [plugins] openclaw-mem0: skills-mode recall (strategy=smart) injecting 0 memories (~20 tokens)\n23:15:12 [ws] ⇄ res ✓ sessions.list 288ms conn=d1eb9bc4…17da id=9e20bb86…371e\n23:15:13 [ws] ⇄ res ✓ sessions.list 268ms conn=d1eb9bc4…17da id=3b49a2ad…7ada\n23:15:20 [ws] ⇄ res ✓ sessions.list 235ms conn=d1eb9bc4…17da id=a192da30…069f\n```\n\n### Actual Behavior\n\nembedding model response ok,response message has 1024 vectors,but the vectors are submitted to vector-db:qdrant with all zero vectors,and vectors has only 256 size.\n\n```http\nPOST /collections/mem0/points/search HTTP/1.1\nhost: qdrant:6333\nconnection: keep-alive\nuser-agent: qdrant-js/1.13.0\napi-key: xxxxxxxxxxxxxxxxxxxxxxxxxxxx\nContent-Type: application/json\nAccept: application/json\naccept-language: *\nsec-fetch-mode: cors\naccept-encoding: gzip, deflate\ncontent-length: 651\n\n{\"vector\":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],\"limit\":120,\"offset\":0,\"filter\":{\"must\":[{\"key\":\"user_id\",\"match\":{\"value\":\"agent\"}}]},\"with_payload\":true,\"with_vector\":false}\n\n**HTTP/1.1 400 Bad Request**\ntransfer-encoding: chunked\ncontent-type: application/json\nvary: accept-encoding, Origin, Access-Control-Request-Method, Access-Control-Request-Headers\ncontent-encoding: gzip\n\n```\n\n### Expected Behavior\n\nembedding model response ok by tcpdump, response message has 1024 vectors,and this vectors are submitted to vector-db:qdrant with the same vectors,and vectors has also 1024 size.\n\n\n### Environment\n\n- openclaw-mem0 version: 1.0.11\n- qdrant: 1.13.6\n",
|
||||
expected: ["plugin", "integrations"],
|
||||
},
|
||||
{
|
||||
number: 3696,
|
||||
title: "Cannot set expiration_date for memory in REST API server (Docker Compose)",
|
||||
body: "### 🐛 Describe the bug\n\nI'm using docker compose to deploy a REST API server. When adding memory, I'm unable to set the expiration_date. Is this feature not supported?",
|
||||
expected: ["rest-api"],
|
||||
},
|
||||
{
|
||||
number: 6252,
|
||||
title: "cursor: on_file_read_cursor.sh ignores auto_search / MEM0_AUTO_SEARCH",
|
||||
body: "### Component\n\nCursor / mem0-plugin\n\n### Description\n\n`on_file_read_cursor.sh` never checks `MEM0_AUTO_SEARCH`. In Claude Code, #6065/#6071 added a guard on `on_file_read.sh`, but the Cursor PreToolUse variant still always calls `file_context.py` (and thus Platform search) once `MEM0_API_KEY` is set.\n\n### Expected\n\nWhen `auto_search: false` / `MEM0_AUTO_SEARCH=false`, `on_file_read_cursor.sh` should exit 0 without searching.\n\n### Actual\n\nTimeline search still runs.\n\n### Related\n\n#6065, #6071, #6250\n",
|
||||
expected: ["plugin", "integrations"],
|
||||
},
|
||||
{
|
||||
number: 6032,
|
||||
title: "docs: fix typos and punctuation errors across docs",
|
||||
body: "### Description\n\n### Page\nMultiple pages — see list below.\n\n### What's Wrong or Missing\n1. https://docs.mem0.ai/components/llms/overview — \"a llm\" should be \"an LLM\"\n2. https://docs.mem0.ai/components/vectordbs/dbs/azure — 2 comma splices + \"setup\" used as a verb (should be \"set up\")\n3. https://docs.mem0.ai/components/embedders/models/azure_openai — \"from the Azure.\" is an incomplete sentence\n4. https://docs.mem0.ai/components/llms/models/azure_openai — same incomplete \"from the Azure\" phrasing\n5. https://docs.mem0.ai/cookbooks/companions/voice-companion-openai — \"an important information\" (uncountable noun)\n6. https://docs.mem0.ai/cookbooks/essentials/exporting-memories — comma splice\n7. https://docs.mem0.ai/cookbooks/integrations/tavily-search — \"usecase\" should be \"use case\"\n8. https://docs.mem0.ai/cookbooks/overview — broken parallelism in bullet list\n9. README.md — \"Github App\" should be \"GitHub App\"\n10. https://docs.mem0.ai/platform/overview — table cell not capitalized like other rows\n\n### Suggested Fix\nApply the corrections listed above for each page. I will submit a PR soon addressing all of the issues mentioned.",
|
||||
expected: ["documentation"],
|
||||
},
|
||||
];
|
||||
|
||||
const cliRegressionCase = {
|
||||
number: 3144,
|
||||
title: "Bug Report: Memory Score Does Not Match Expected Relevance in Local Search",
|
||||
body: "### 🐛 Describe the bug\n\n#### Description\n\nWhen using the locally deployed `mem0` server, the returned memory `score` from the `search` interface does not align with the expected semantic relevance. In particular, irrelevant or less relevant memories sometimes receive higher scores than directly related ones.\n\n#### Reproduction Steps\n\n```python\nmem0 = mem0_client(mode=\"local\")\nprint(\"Mem0 client initialized successfully.\")\n\nprint(\"Adding memories...\")\nresult = mem0.add(messages=[\n {\"role\": \"user\", \"content\": \"I like drinking coffee in the morning\"},\n {\"role\": \"user\", \"content\": \"I enjoy reading books at night\"}\n], user_id=\"alice\")\nprint(\"Memory added:\", result)\n\nprint(\"Searching memories...\")\nsearch_result = mem0.search(query=\"coffee\", user_id=\"alice\", top_k=2)\nprint(\"Search results:\", search_result)\n```\n\n#### Actual Output\n\n```json\n{\n \"results\": [\n {\n \"id\": \"5099b5be-c673-4f09-99de-a196f43b6476\",\n \"memory\": \"Likes drinking coffee in the morning\",\n \"score\": 0.5115111920687857\n },\n {\n \"id\": \"08df5c51-c52b-4c45-a5b6-b3f864ea149a\",\n \"memory\": \"Enjoys reading books at night\",\n \"score\": 0.7755568273863331\n }\n ],\n \"relations\": [\n {\"source\": \"coffee\", \"relationship\": \"consumed_in\", \"destination\": \"morning\"},\n {\"source\": \"user_id:_alice\", \"relationship\": \"likes\", \"destination\": \"coffee\"},\n {\"source\": \"user_id:_alice\", \"relationship\": \"likes_drinking\", \"destination\": \"coffee\"},\n {\"source\": \"user_id:_alice\", \"relationship\": \"in_time\", \"destination\": \"morning\"},\n {\"source\": \"user_id:_alice\", \"relationship\": \"drinks_in\", \"destination\": \"morning\"}\n ]\n}\n```\n\n#### Expected Behavior\n\nThe memory `\"Likes drinking coffee in the morning\"` should have a **higher score** than `\"Enjoys reading books at night\"` when querying for `\"coffee\"`, since it is directly semantically related.",
|
||||
};
|
||||
|
||||
let failures = 0;
|
||||
|
||||
function run(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(`PASS ${name}`);
|
||||
} catch (err) {
|
||||
failures++;
|
||||
console.error(`FAIL ${name}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const { number, title, body, expected } of cases) {
|
||||
const text = `${title}
|
||||
|
||||
${body}`;
|
||||
run(`#${number}`, () => {
|
||||
assert.deepStrictEqual(inferComponentLabels(text, keywords), expected);
|
||||
});
|
||||
}
|
||||
|
||||
run('#3144 cliKeywordPrefixSubstringRegression', () => {
|
||||
const text = `${cliRegressionCase.title}
|
||||
|
||||
${cliRegressionCase.body}`;
|
||||
const inferred = inferComponentLabels(text, keywords);
|
||||
assert.ok(!inferred.includes('cli'), `expected 'cli' absent (body contains 'Mem0 client', a substring of the removed 'mem0 cli' term), got ${JSON.stringify(inferred)}`);
|
||||
});
|
||||
|
||||
run('noKeywordMatchReturnsEmptyArray', () => {
|
||||
const text = 'The weather today is sunny and I went for a walk in the park with my dog.';
|
||||
assert.deepStrictEqual(inferComponentLabels(text, keywords), []);
|
||||
});
|
||||
|
||||
run('emptyStringReturnsEmptyArray', () => {
|
||||
assert.deepStrictEqual(inferComponentLabels('', keywords), []);
|
||||
});
|
||||
|
||||
if (failures > 0) {
|
||||
console.error(`
|
||||
${failures} test(s) failed.`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`
|
||||
All ${cases.length + 3} tests passed.`);
|
||||
@@ -0,0 +1,60 @@
|
||||
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);
|
||||
@@ -0,0 +1,122 @@
|
||||
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);
|
||||
@@ -38,9 +38,15 @@ jobs:
|
||||
cli_python: ${{ steps.filter.outputs.cli_python }}
|
||||
cli_node: ${{ steps.filter.outputs.cli_node }}
|
||||
openclaw: ${{ steps.filter.outputs.openclaw }}
|
||||
mem0_plugin: ${{ steps.filter.outputs.mem0_plugin }}
|
||||
opencode_plugin: ${{ steps.filter.outputs.opencode_plugin }}
|
||||
pi_agent_plugin: ${{ steps.filter.outputs.pi_agent_plugin }}
|
||||
deepseek_plugin: ${{ steps.filter.outputs.deepseek_plugin }}
|
||||
n8n_nodes_mem0: ${{ steps.filter.outputs.n8n_nodes_mem0 }}
|
||||
zapier_mem0: ${{ steps.filter.outputs.zapier_mem0 }}
|
||||
mem0_strands: ${{ steps.filter.outputs.mem0_strands }}
|
||||
docs_llms_txt: ${{ steps.filter.outputs.docs_llms_txt }}
|
||||
github_scripts: ${{ steps.filter.outputs.github_scripts }}
|
||||
steps:
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
@@ -71,6 +77,11 @@ jobs:
|
||||
- 'integrations/openclaw/**'
|
||||
- '.github/workflows/openclaw-checks.yml'
|
||||
- '.github/workflows/ci-gate.yml'
|
||||
mem0_plugin:
|
||||
- 'integrations/mem0-plugin/**'
|
||||
- '!integrations/mem0-plugin/.opencode-plugin/**'
|
||||
- '.github/workflows/mem0-plugin-checks.yml'
|
||||
- '.github/workflows/ci-gate.yml'
|
||||
opencode_plugin:
|
||||
- 'integrations/mem0-plugin/.opencode-plugin/**'
|
||||
- '.github/workflows/opencode-plugin-checks.yml'
|
||||
@@ -79,6 +90,21 @@ jobs:
|
||||
- 'integrations/pi-agent-plugin/**'
|
||||
- '.github/workflows/pi-agent-plugin-checks.yml'
|
||||
- '.github/workflows/ci-gate.yml'
|
||||
deepseek_plugin:
|
||||
- 'integrations/deepseek-plugin/**'
|
||||
- '.github/workflows/deepseek-plugin-checks.yml'
|
||||
- '.github/workflows/ci-gate.yml'
|
||||
n8n_nodes_mem0:
|
||||
- 'integrations/n8n-nodes-mem0/**'
|
||||
- '.github/workflows/n8n-nodes-mem0-checks.yml'
|
||||
zapier_mem0:
|
||||
- 'integrations/zapier-mem0/**'
|
||||
- '.github/workflows/zapier-mem0-checks.yml'
|
||||
- '.github/workflows/ci-gate.yml'
|
||||
mem0_strands:
|
||||
- 'integrations/mem0-strands/**'
|
||||
- '.github/workflows/mem0-strands-checks.yml'
|
||||
- '.github/workflows/ci-gate.yml'
|
||||
docs_llms_txt:
|
||||
- 'docs/**/*.mdx'
|
||||
- 'docs/llms.txt'
|
||||
@@ -86,6 +112,13 @@ 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
|
||||
@@ -122,6 +155,13 @@ jobs:
|
||||
uses: ./.github/workflows/openclaw-checks.yml
|
||||
secrets: inherit
|
||||
|
||||
mem0-plugin:
|
||||
name: Mem0 Plugin
|
||||
needs: changes
|
||||
if: needs.changes.outputs.mem0_plugin == 'true'
|
||||
uses: ./.github/workflows/mem0-plugin-checks.yml
|
||||
secrets: inherit
|
||||
|
||||
opencode-plugin:
|
||||
name: OpenCode Plugin
|
||||
needs: changes
|
||||
@@ -136,6 +176,32 @@ jobs:
|
||||
uses: ./.github/workflows/pi-agent-plugin-checks.yml
|
||||
secrets: inherit
|
||||
|
||||
deepseek-plugin:
|
||||
name: DeepSeek Harness Plugin
|
||||
needs: changes
|
||||
if: needs.changes.outputs.deepseek_plugin == 'true'
|
||||
uses: ./.github/workflows/deepseek-plugin-checks.yml
|
||||
secrets: inherit
|
||||
|
||||
n8n-nodes-mem0:
|
||||
name: n8n Node
|
||||
needs: changes
|
||||
if: needs.changes.outputs.n8n_nodes_mem0 == 'true'
|
||||
uses: ./.github/workflows/n8n-nodes-mem0-checks.yml
|
||||
zapier-mem0:
|
||||
name: Zapier App
|
||||
needs: changes
|
||||
if: needs.changes.outputs.zapier_mem0 == 'true'
|
||||
uses: ./.github/workflows/zapier-mem0-checks.yml
|
||||
secrets: inherit
|
||||
|
||||
mem0-strands:
|
||||
name: mem0-strands
|
||||
needs: changes
|
||||
if: needs.changes.outputs.mem0_strands == 'true'
|
||||
uses: ./.github/workflows/mem0-strands-checks.yml
|
||||
secrets: inherit
|
||||
|
||||
docs-llms-txt:
|
||||
name: docs llms.txt
|
||||
needs: changes
|
||||
@@ -143,6 +209,24 @@ 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:
|
||||
@@ -152,9 +236,15 @@ jobs:
|
||||
- cli-python
|
||||
- cli-node
|
||||
- openclaw
|
||||
- mem0-plugin
|
||||
- opencode-plugin
|
||||
- pi-agent-plugin
|
||||
- deepseek-plugin
|
||||
- n8n-nodes-mem0
|
||||
- zapier-mem0
|
||||
- mem0-strands
|
||||
- docs-llms-txt
|
||||
- github-scripts
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
}
|
||||
|
||||
base_version=$(git show "$BASE_SHA:pyproject.toml" 2>/dev/null | extract_version || echo "")
|
||||
head_version=$(extract_version < pyproject.toml)
|
||||
head_version=$(git show "$HEAD_SHA:pyproject.toml" | extract_version)
|
||||
|
||||
echo "Base version: ${base_version:-<unknown>}"
|
||||
echo "Head version: $head_version"
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
pip install -e ".[test,graph,vector_stores,llms,extras]"
|
||||
pip install ruff
|
||||
pip install ruff==0.16.0
|
||||
- name: Run Linting
|
||||
if: needs.check_changes.outputs.mem0_changed == 'true'
|
||||
run: make lint
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
name: Publish @mem0/deepseek-plugin 📦 to npm
|
||||
|
||||
# Dispatched by release.yml (Release Router) when a release tagged
|
||||
# deepseek-plugin-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. deepseek-plugin-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/deepseek-plugin 📦 to npm
|
||||
if: startsWith(inputs.tag, 'deepseek-plugin-v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: integrations/deepseek-plugin
|
||||
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/deepseek-plugin/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
|
||||
@@ -0,0 +1,90 @@
|
||||
name: deepseek-plugin 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/deepseek-plugin/**'
|
||||
- '.github/workflows/deepseek-plugin-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/deepseek-plugin/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd integrations/deepseek-plugin && pnpm install --frozen-lockfile
|
||||
|
||||
- name: Type check
|
||||
run: cd integrations/deepseek-plugin && 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/deepseek-plugin/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd integrations/deepseek-plugin && pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run tests
|
||||
run: cd integrations/deepseek-plugin && 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/deepseek-plugin/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd integrations/deepseek-plugin && pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: cd integrations/deepseek-plugin && pnpm build
|
||||
|
||||
- name: Verify dist output exists
|
||||
run: |
|
||||
test -f integrations/deepseek-plugin/dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
|
||||
test -f integrations/deepseek-plugin/dist/index.d.ts || (echo "Build output missing: dist/index.d.ts" && exit 1)
|
||||
@@ -12,28 +12,56 @@ jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: stefanbuck/github-issue-parser@v3
|
||||
id: issue-parser
|
||||
continue-on-error: true
|
||||
with:
|
||||
template-path: .github/ISSUE_TEMPLATE/bug_report.yml
|
||||
|
||||
- uses: redhat-plumbers-in-action/advanced-issue-labeler@v3
|
||||
continue-on-error: true
|
||||
with:
|
||||
issue-form: ${{ steps.issue-parser.outputs.jsonString }}
|
||||
section: component
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
config-path: .github/advanced-issue-labeler.yml
|
||||
|
||||
- uses: stefanbuck/github-issue-parser@v3
|
||||
id: feature-parser
|
||||
if: contains(github.event.issue.labels.*.name, 'enhancement')
|
||||
- name: Infer component from text when the form was not used
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
template-path: .github/ISSUE_TEMPLATE/feature_request.yml
|
||||
script: |
|
||||
const {
|
||||
componentLabels,
|
||||
inferComponentLabels,
|
||||
loadKeywords,
|
||||
} = require(`${process.env.GITHUB_WORKSPACE}/.github/scripts/infer-component-labels.js`);
|
||||
|
||||
- uses: redhat-plumbers-in-action/advanced-issue-labeler@v3
|
||||
if: contains(github.event.issue.labels.*.name, 'enhancement')
|
||||
with:
|
||||
issue-form: ${{ steps.feature-parser.outputs.jsonString }}
|
||||
section: component
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
config-path: .github/advanced-issue-labeler.yml
|
||||
const { data: issue } = await github.rest.issues.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
});
|
||||
|
||||
const keywords = loadKeywords(`${process.env.GITHUB_WORKSPACE}/.github/component-keywords.json`);
|
||||
const known = componentLabels(keywords);
|
||||
const existing = issue.labels.map((label) => label.name || label);
|
||||
if (existing.some((name) => known.includes(name))) {
|
||||
core.info(`Component label already present: ${existing.join(', ')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const labels = inferComponentLabels(`${issue.title}\n\n${issue.body || ''}`, keywords);
|
||||
|
||||
if (labels.length === 0) {
|
||||
core.info('No component could be inferred from the issue text');
|
||||
return;
|
||||
}
|
||||
|
||||
core.info(`Inferred: ${labels.join(', ')}`);
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
name: Mem0 Plugin Checks
|
||||
|
||||
# On PRs this is invoked by ci-gate.yml (the single required check);
|
||||
# push-to-main and manual runs remain standalone.
|
||||
#
|
||||
# Covers the Python plugin (scripts/ + tests/). The nested .opencode-plugin/
|
||||
# is a separate package with its own workflow (opencode-plugin-checks.yml).
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'integrations/mem0-plugin/**'
|
||||
- '!integrations/mem0-plugin/.opencode-plugin/**'
|
||||
- '.github/workflows/mem0-plugin-checks.yml'
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
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 dependencies
|
||||
working-directory: integrations/mem0-plugin
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
pip install pytest
|
||||
|
||||
- name: Verify hook entry points are executable
|
||||
working-directory: integrations/mem0-plugin
|
||||
run: |
|
||||
missing=$(find scripts -name '*.sh' ! -name '_*' ! -perm -u+x -print)
|
||||
if [ -n "$missing" ]; then
|
||||
echo "Hook entry points must be executable:"
|
||||
echo "$missing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Check hook manifests are valid JSON
|
||||
working-directory: integrations/mem0-plugin
|
||||
run: |
|
||||
for f in plugin.json mcp_config.json hooks.json hooks/*.json; do
|
||||
jq empty "$f" || (echo "Invalid JSON: $f" && exit 1)
|
||||
done
|
||||
|
||||
- name: Run tests
|
||||
working-directory: integrations/mem0-plugin
|
||||
run: pytest -q
|
||||
@@ -0,0 +1,50 @@
|
||||
name: Publish mem0-strands 🐍 distribution 📦 to PyPI
|
||||
|
||||
# Dispatched by release.yml (Release Router) when a release tagged
|
||||
# mem0-strands-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 `mem0-strands` 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. mem0-strands-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 mem0-strands 📦 to PyPI
|
||||
if: startsWith(inputs.tag, 'mem0-strands-v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: integrations/mem0-strands/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/mem0-strands/python/dist/
|
||||
@@ -0,0 +1,82 @@
|
||||
name: mem0-strands 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/mem0-strands/**'
|
||||
- '.github/workflows/mem0-strands-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/mem0-strands/python
|
||||
run: pip install -e ".[dev]"
|
||||
|
||||
- name: Lint with ruff
|
||||
working-directory: integrations/mem0-strands/python
|
||||
run: ruff check .
|
||||
|
||||
- name: Check formatting
|
||||
working-directory: integrations/mem0-strands/python
|
||||
run: ruff format --check .
|
||||
|
||||
- name: Type-check with mypy
|
||||
working-directory: integrations/mem0-strands/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/mem0-strands/python
|
||||
run: pip install -e ".[dev]"
|
||||
|
||||
- name: Run tests
|
||||
working-directory: integrations/mem0-strands/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/mem0-strands/python
|
||||
run: hatch build --clean
|
||||
|
||||
- name: Verify dist output
|
||||
run: |
|
||||
ls integrations/mem0-strands/python/dist/*.whl || (echo "Wheel file missing" && exit 1)
|
||||
ls integrations/mem0-strands/python/dist/*.tar.gz || (echo "Source dist missing" && exit 1)
|
||||
@@ -0,0 +1,60 @@
|
||||
name: Publish n8n-nodes-mem0 📦 to npm
|
||||
|
||||
# Dispatched by release.yml (Release Router) when a release tagged
|
||||
# n8n-nodes-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. n8n-nodes-mem0-v0.1.0)'
|
||||
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 n8n-nodes-mem0 📦 to npm
|
||||
if: startsWith(inputs.tag, 'n8n-nodes-mem0-v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: integrations/n8n-nodes-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: '20'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: integrations/n8n-nodes-mem0/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile --ignore-scripts
|
||||
|
||||
- name: Build
|
||||
run: pnpm run 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
|
||||
@@ -0,0 +1,88 @@
|
||||
name: n8n-nodes-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/n8n-nodes-mem0/**'
|
||||
- '.github/workflows/n8n-nodes-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/n8n-nodes-mem0/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd integrations/n8n-nodes-mem0 && pnpm install --frozen-lockfile --ignore-scripts
|
||||
|
||||
- name: Lint
|
||||
run: cd integrations/n8n-nodes-mem0 && pnpm run lint
|
||||
|
||||
test:
|
||||
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/n8n-nodes-mem0/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd integrations/n8n-nodes-mem0 && pnpm install --frozen-lockfile --ignore-scripts
|
||||
|
||||
- name: Run tests
|
||||
run: cd integrations/n8n-nodes-mem0 && pnpm test
|
||||
|
||||
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/n8n-nodes-mem0/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd integrations/n8n-nodes-mem0 && pnpm install --frozen-lockfile --ignore-scripts
|
||||
|
||||
- name: Build
|
||||
run: cd integrations/n8n-nodes-mem0 && pnpm run build
|
||||
|
||||
- name: Verify dist output exists
|
||||
run: |
|
||||
test -f integrations/n8n-nodes-mem0/dist/nodes/Mem0/Mem0.node.js || (echo "Build output missing: dist/nodes/Mem0/Mem0.node.js" && exit 1)
|
||||
test -f integrations/n8n-nodes-mem0/dist/credentials/Mem0Api.credentials.js || (echo "Build output missing: dist/credentials/Mem0Api.credentials.js" && exit 1)
|
||||
test -f integrations/n8n-nodes-mem0/dist/nodes/Mem0/mem0.svg || (echo "Build output missing: dist/nodes/Mem0/mem0.svg" && exit 1)
|
||||
@@ -0,0 +1,214 @@
|
||||
name: PR Gate
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, ready_for_review, edited]
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
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' }}
|
||||
|
||||
env:
|
||||
GATE_EFFECTIVE_FROM: '2026-08-12T00:00:00Z'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: read
|
||||
|
||||
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:
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const pr = context.payload.pull_request;
|
||||
const { owner, repo } = context.repo;
|
||||
|
||||
const effectiveFrom = process.env.GATE_EFFECTIVE_FROM;
|
||||
if (effectiveFrom && Date.parse(pr.created_at) < Date.parse(effectiveFrom)) {
|
||||
core.info(`Opened ${pr.created_at}, before the gate took effect ${effectiveFrom}. Skipped.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const { data: current } = await github.rest.pulls.get({
|
||||
owner, repo, pull_number: pr.number,
|
||||
});
|
||||
if (current.state !== 'open') {
|
||||
core.info(`#${pr.number} is already ${current.state}. Skipped.`);
|
||||
return;
|
||||
}
|
||||
|
||||
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))) {
|
||||
core.info('Docs-only PR, gate skipped');
|
||||
return;
|
||||
}
|
||||
|
||||
const { repository } = await github.graphql(
|
||||
`query ($owner: String!, $repo: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $number) {
|
||||
closingIssuesReferences(first: 20) {
|
||||
nodes { number labels(first: 50) { nodes { name } } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{ owner, repo, number: pr.number },
|
||||
);
|
||||
|
||||
const accepted = repository.pullRequest.closingIssuesReferences.nodes
|
||||
.filter((issue) => issue.labels.nodes.some((label) => label.name === 'accepted'))
|
||||
.map((issue) => issue.number);
|
||||
|
||||
if (accepted.length > 0) {
|
||||
core.info(`Accepted issue linked: #${accepted.join(', #')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
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.',
|
||||
'**Closed does not mean rejected.** It means it is not in the queue yet, and reopening takes about a minute.',
|
||||
'',
|
||||
'To get it reviewed:',
|
||||
'',
|
||||
'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.',
|
||||
'',
|
||||
'Issue already labeled `accepted`? Just add `Closes #<number>` to the description. That reopens this too.',
|
||||
'',
|
||||
'Documentation-only changes skip this gate entirely.',
|
||||
'',
|
||||
'See [CONTRIBUTING.md](https://github.com/mem0ai/mem0/blob/main/CONTRIBUTING.md) for the full policy.',
|
||||
].join('\n');
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner, repo, issue_number: pr.number, body,
|
||||
});
|
||||
await github.rest.pulls.update({
|
||||
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}`);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
name: PR Labeler
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened, edited]
|
||||
|
||||
concurrency:
|
||||
group: pr-labeler-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: read
|
||||
|
||||
jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v5
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Propagate labels from linked issues
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const allowed = new Set([
|
||||
'sdk-python', 'sdk-typescript', 'vector-store', 'plugin',
|
||||
'rest-api', 'documentation', 'ci', 'cli', 'integrations',
|
||||
]);
|
||||
const umbrella = { plugin: 'integrations' };
|
||||
const { repository } = await github.graphql(
|
||||
`query ($owner: String!, $repo: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $number) {
|
||||
closingIssuesReferences(first: 20) {
|
||||
nodes { labels(first: 50) { nodes { name } } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{ owner: context.repo.owner, repo: context.repo.repo, number: context.issue.number },
|
||||
);
|
||||
|
||||
const labels = new Set();
|
||||
for (const issue of repository.pullRequest.closingIssuesReferences.nodes) {
|
||||
for (const label of issue.labels.nodes) {
|
||||
if (allowed.has(label.name)) labels.add(label.name);
|
||||
}
|
||||
}
|
||||
|
||||
for (const label of [...labels]) {
|
||||
if (umbrella[label]) labels.add(umbrella[label]);
|
||||
}
|
||||
|
||||
if (labels.size > 0) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: [...labels],
|
||||
});
|
||||
}
|
||||
@@ -45,6 +45,9 @@ jobs:
|
||||
openclaw-v*) workflow="openclaw-cd.yml" ;;
|
||||
opencode-v*) workflow="opencode-plugin-cd.yml" ;;
|
||||
pi-agent-v*) workflow="pi-agent-plugin-cd.yml" ;;
|
||||
deepseek-plugin-v*) workflow="deepseek-plugin-cd.yml" ;;
|
||||
n8n-nodes-mem0-v*) workflow="n8n-nodes-mem0-cd.yml" ;;
|
||||
mem0-strands-v*) workflow="mem0-strands-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."
|
||||
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
set -euo pipefail
|
||||
|
||||
base_version=$(git show "$BASE_SHA:mem0-ts/package.json" 2>/dev/null | jq -r .version || echo "")
|
||||
head_version=$(jq -r .version mem0-ts/package.json)
|
||||
head_version=$(git show "$HEAD_SHA:mem0-ts/package.json" | jq -r .version)
|
||||
|
||||
echo "Base version: ${base_version:-<unknown>}"
|
||||
echo "Head version: $head_version"
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
name: Vouch - Check PR
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened]
|
||||
|
||||
concurrency:
|
||||
group: vouch-check-pr-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
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
|
||||
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,
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
name: Vouch - Manage by Issue
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
concurrency:
|
||||
group: vouch-manage
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
manage:
|
||||
if: contains(github.event.comment.body, '!vouch') || contains(github.event.comment.body, '!denounce') || contains(github.event.comment.body, '!unvouch')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/create-github-app-token@v3
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.VOUCH_APP_ID }}
|
||||
private-key: ${{ secrets.VOUCH_APP_PRIVATE_KEY }}
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- uses: mitchellh/vouch/action/manage-by-issue@d66fa29a64600490892131ad87597c30c91fcac4 # v1.5.0
|
||||
with:
|
||||
repo: ${{ github.repository }}
|
||||
issue-id: ${{ github.event.issue.number }}
|
||||
comment-id: ${{ github.event.comment.id }}
|
||||
vouch-keyword: "!vouch"
|
||||
denounce-keyword: "!denounce"
|
||||
unvouch-keyword: "!unvouch"
|
||||
pull-request: "true"
|
||||
merge-immediately: "false"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
@@ -0,0 +1,42 @@
|
||||
name: Deploy zapier-mem0 to Zapier
|
||||
|
||||
# Zapier apps deploy to Zapier's own platform (not npm), so this is NOT wired
|
||||
# into the npm release router (release.yml). It is manual workflow_dispatch
|
||||
# only and requires the ZAPIER_DEPLOY_KEY repo secret.
|
||||
#
|
||||
# gh workflow run zapier-mem0-cd.yml --ref main
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
push:
|
||||
name: Push zapier-mem0 to Zapier
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: integrations/zapier-mem0
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- 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
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: integrations/zapier-mem0/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build TypeScript
|
||||
run: pnpm build
|
||||
|
||||
- name: Push to Zapier
|
||||
env:
|
||||
ZAPIER_DEPLOY_KEY: ${{ secrets.ZAPIER_DEPLOY_KEY }}
|
||||
run: npx zapier-platform-cli@19 push
|
||||
@@ -0,0 +1,47 @@
|
||||
name: zapier-mem0 checks
|
||||
|
||||
# On PRs this is invoked by ci-gate.yml (the single required check);
|
||||
# push-to-main and manual runs remain standalone.
|
||||
#
|
||||
# CI compiles the TypeScript app, runs `zapier validate` (offline schema + style
|
||||
# checks) against the build, plus the offline jest unit suite (test/unit.test.ts —
|
||||
# mocked z.request, no network). The end-to-end jest suite is skipped here because
|
||||
# it hits the live Mem0 API — it runs locally with MEM0_API_KEY set (see README).
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'integrations/zapier-mem0/**'
|
||||
- '.github/workflows/zapier-mem0-checks.yml'
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
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: 22
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: integrations/zapier-mem0/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd integrations/zapier-mem0 && pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build TypeScript
|
||||
run: cd integrations/zapier-mem0 && pnpm build
|
||||
|
||||
- name: Validate Zapier app definition
|
||||
run: cd integrations/zapier-mem0 && npx zapier-platform-cli@19 validate
|
||||
|
||||
- name: Run offline unit tests
|
||||
run: cd integrations/zapier-mem0 && pnpm test:unit
|
||||
@@ -191,3 +191,7 @@ qdrant_storage/
|
||||
testing.ipynb
|
||||
.weave/
|
||||
|
||||
|
||||
# TypeScript incremental build info and local, uncommitted e2e scripts (used by the integrations, e.g. integrations/deepseek-plugin)
|
||||
*.tsbuildinfo
|
||||
*.local.mjs
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "mem0-plugins",
|
||||
"version": "1",
|
||||
"plugins": [
|
||||
{
|
||||
"id": "mem0",
|
||||
"displayName": "Mem0",
|
||||
"version": "0.1.0",
|
||||
"description": "Persistent memory for Kimi Code. Remembers decisions, patterns, and preferences across sessions.",
|
||||
"homepage": "https://mem0.ai",
|
||||
"keywords": ["memory", "personalization", "mcp", "semantic-search"],
|
||||
"source": "https://github.com/mem0ai/mem0/tree/main/integrations/mem0-plugin"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,611 +1,175 @@
|
||||
# AGENTS.md
|
||||
|
||||
This file provides context for AI coding assistants (Claude Code, Cursor, GitHub Copilot, Codex, etc.) working with the Mem0 repository.
|
||||
Context for AI coding assistants (Claude Code, Cursor, Copilot, Codex) working in the Mem0 repository.
|
||||
|
||||
## Project Overview
|
||||
**Mem0** ("mem-zero") is a memory layer for AI agents: persistent, personalized memory through a hosted platform API and self-hosted open-source SDKs. Apache-2.0.
|
||||
[Repository](https://github.com/mem0ai/mem0) · [Documentation](https://docs.mem0.ai)
|
||||
|
||||
**Mem0** ("mem-zero") is an intelligent memory layer for AI agents and assistants. It provides persistent, personalized memory via both a hosted platform API and self-hosted open-source SDKs.
|
||||
This is a polyglot monorepo and **every package sets its own rules**. Read the `AGENTS.md` nearest the files you are editing before running any command. The linters, formatters, test runners, and line lengths genuinely differ per package, and using the wrong one fails CI or produces a diff full of noise.
|
||||
|
||||
- **Repository**: https://github.com/mem0ai/mem0
|
||||
- **Documentation**: https://docs.mem0.ai
|
||||
- **License**: Apache-2.0
|
||||
## Do NOT
|
||||
|
||||
## Repository Structure
|
||||
- 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.
|
||||
- Use npm or yarn in TypeScript packages. This repo is pnpm-only (Bun in `.opencode-plugin/`).
|
||||
- Use `require()` in TypeScript. ES module `import` syntax only.
|
||||
- Mix up linter configs. Root Python is ruff at line length **120**, `cli/python/` is ruff at **100**, `cli/node/` is Biome, `mem0-ts/` is Prettier, `integrations/vercel-ai-sdk/` is ESLint.
|
||||
- Add Python dependencies to the core `dependencies` list in `pyproject.toml`. Use an optional group.
|
||||
- Change a public API without updating `docs/` in the same pull request.
|
||||
- Introduce a new framework or abstraction without discussion. Follow the patterns already in the file you are editing.
|
||||
|
||||
This is a **polyglot monorepo** containing Python and TypeScript packages, CLIs, servers, plugins, and documentation.
|
||||
## Where to look
|
||||
|
||||
### Key Directories
|
||||
| Editing | Read | Toolchain |
|
||||
|---------|------|-----------|
|
||||
| `mem0/` | [`mem0/AGENTS.md`](mem0/AGENTS.md) | hatch, ruff 120, pytest |
|
||||
| `tests/` | [`tests/AGENTS.md`](tests/AGENTS.md) | pytest |
|
||||
| `mem0-ts/` | [`mem0-ts/AGENTS.md`](mem0-ts/AGENTS.md) | pnpm, tsup, Prettier, jest |
|
||||
| `cli/python/` | [`cli/python/AGENTS.md`](cli/python/AGENTS.md) | ruff **100**, pytest |
|
||||
| `cli/node/` | [`cli/node/AGENTS.md`](cli/node/AGENTS.md) | pnpm, tsup, Biome, vitest |
|
||||
| `integrations/` | [`integrations/AGENTS.md`](integrations/AGENTS.md) | varies per integration |
|
||||
| `server/` | [`server/AGENTS.md`](server/AGENTS.md) | Docker Compose, FastAPI |
|
||||
| `docs/` | [`docs/AGENTS.md`](docs/AGENTS.md) | Mintlify |
|
||||
| `skills/` | [`skills/AGENTS.md`](skills/AGENTS.md) | markdown, size-budgeted |
|
||||
| `.github/` | [`.github/AGENTS.md`](.github/AGENTS.md) | GitHub Actions |
|
||||
|
||||
| Directory | Description |
|
||||
|-----------|-------------|
|
||||
| `mem0/` | Core Python SDK (`mem0ai` on PyPI) — memory, LLMs, embeddings, vector stores, graphs, rerankers |
|
||||
| `mem0-ts/` | TypeScript SDK (`mem0ai` on npm) — client + OSS memory |
|
||||
| `cli/python/` | Python CLI (`mem0-cli` on PyPI) — Typer-based, entry point `mem0` |
|
||||
| `cli/node/` | Node CLI (`@mem0/cli` on npm) — Commander-based, entry point `mem0` |
|
||||
| `integrations/` | **Agent & editor integrations**, one directory per integration (see "Adding a New Integration") |
|
||||
| `integrations/mem0-plugin/` | AI editor plugins (Claude Code, Cursor, Codex) — MCP server connection, lifecycle hooks, skills. Contains nested `.opencode-plugin/` (`@mem0/opencode-plugin`) |
|
||||
| `integrations/openclaw/` | `@mem0/openclaw-mem0` — OpenClaw plugin for Claude Code / AI editors |
|
||||
| `integrations/pi-agent-plugin/` | `@mem0/pi-agent-plugin` — Pi Agent plugin |
|
||||
| `integrations/vercel-ai-sdk/` | `@mem0/vercel-ai-provider` — Vercel AI SDK memory provider |
|
||||
| `server/` | FastAPI REST server for self-hosted Mem0 (Docker: FastAPI + PostgreSQL/pgvector + Neo4j) |
|
||||
| `openmemory/` | Self-hosted memory platform — `api/` (FastAPI + Alembic + MCP server) and `ui/` (Next.js 15 + React 19) |
|
||||
| `skills/` | Claude Code skill definitions. Reference skills (SDK knowledge, always-on): `mem0/`, `mem0-cli/`, `mem0-vercel-ai-sdk/`. Pipeline skills (run on demand): `mem0-integrate/`, `mem0-test-integration/`, `mem0-oss-to-platform/` |
|
||||
## Repository map
|
||||
|
||||
| Directory | What it is |
|
||||
|-----------|------------|
|
||||
| `mem0/` | Core Python SDK (`mem0ai` on PyPI): memory, LLMs, embeddings, vector stores, graphs, rerankers |
|
||||
| `mem0-ts/` | TypeScript SDK (`mem0ai` on npm): hosted client + OSS memory |
|
||||
| `cli/python/` | Python CLI (`mem0-cli` on PyPI), Typer-based, entry point `mem0` |
|
||||
| `cli/node/` | Node CLI (`@mem0/cli` on npm), Commander-based, entry point `mem0` |
|
||||
| `integrations/` | Agent and editor integrations, one self-contained directory each |
|
||||
| `server/` | FastAPI REST server for self-hosted Mem0 (Docker: FastAPI + pgvector + Neo4j) |
|
||||
| `skills/` | Claude Code skill definitions, published by raw URL |
|
||||
| `docs/` | Documentation site (Mintlify) |
|
||||
| `tests/` | Python SDK tests (pytest) |
|
||||
| `evaluation/` | Submodule → [`mem0ai/memory-benchmarks`](https://github.com/mem0ai/memory-benchmarks) — benchmarking (LOCOMO, LongMemEval, BEAM) lives in that repo |
|
||||
| `examples/` | Sample projects & runnable demos — apps, Chrome extension, multi-agent patterns, and Jupyter notebooks (`notebooks/`) |
|
||||
| `examples/` | Sample apps, Chrome extension, multi-agent patterns, notebooks |
|
||||
| `scripts/` | Repo-wide utilities, e.g. `check-llms-txt-coverage.py` |
|
||||
| `evaluation/` | Submodule pinned to [`mem0ai/memory-benchmarks`](https://github.com/mem0ai/memory-benchmarks) |
|
||||
| `pr-reviews/` | Pull request review materials |
|
||||
| `scripts/` | Repo-wide utility scripts (e.g., `check-llms-txt-coverage.py` for docs/llms.txt sync) |
|
||||
|
||||
### Core Package Dependencies
|
||||
|
||||
```
|
||||
mem0 (Python SDK) mem0-ts (TypeScript SDK)
|
||||
├── mem0/memory/ ├── src/client/ (MemoryClient — hosted)
|
||||
├── mem0/llms/ └── src/oss/ (Memory — self-hosted)
|
||||
├── mem0/memory/ ├── src/client/ MemoryClient (hosted)
|
||||
├── mem0/llms/ └── src/oss/ Memory (self-hosted)
|
||||
├── mem0/embeddings/ ├── src/llms/
|
||||
├── mem0/vector_stores/ ├── src/embeddings/
|
||||
├── mem0/graphs/ ├── src/vector_stores/
|
||||
└── mem0/reranker/ └── src/graphs/
|
||||
|
||||
cli/python/ ──▶ mem0ai (optional, for OSS mode)
|
||||
cli/node/ ──▶ mem0ai (npm, for API calls)
|
||||
integrations/vercel-ai-sdk/ ──▶ ai, @ai-sdk/* providers
|
||||
integrations/openclaw/ ──▶ mem0ai (npm)
|
||||
cli/python/ ──▶ mem0ai (optional, OSS mode)
|
||||
cli/node/ ──▶ mem0ai (npm)
|
||||
integrations/vercel-ai-sdk/ ──▶ ai, @ai-sdk/*
|
||||
integrations/openclaw/ ──▶ mem0ai (npm)
|
||||
```
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Requirements
|
||||
|
||||
- **Python**: 3.9+ (3.10+ for CLI)
|
||||
- **Node.js**: v18+ (v20 or v22 recommended)
|
||||
- **pnpm**: v10+ (`npm install -g pnpm@10`) — used for all TypeScript packages
|
||||
- **Hatch**: Python build/environment tool (`pip install hatch`)
|
||||
- **Docker**: Required for `server/` and `openmemory/` development
|
||||
|
||||
### Initial Setup
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
# Python SDK
|
||||
hatch shell dev_py_3_11 # creates environment with all deps
|
||||
pre-commit install # install git hooks
|
||||
hatch shell dev_py_3_11 # Python: creates the env with all deps
|
||||
pre-commit install # ruff + isort on commit
|
||||
|
||||
# TypeScript packages
|
||||
cd mem0-ts && pnpm install # TS SDK
|
||||
cd cli/node && pnpm install # Node CLI
|
||||
cd integrations/vercel-ai-sdk && pnpm install # Vercel AI provider
|
||||
cd integrations/openclaw && pnpm install # OpenClaw plugin
|
||||
cd <ts-package> && pnpm install
|
||||
```
|
||||
|
||||
## Build, Lint, and Test Commands
|
||||
Requirements: Python 3.9+ (3.10+ for the CLI), Node 18+ (20 or 22 preferred), pnpm 10+, hatch, Docker for `server/`.
|
||||
|
||||
### Python SDK (`mem0/`)
|
||||
## Conventions everywhere
|
||||
|
||||
- **Naming:** `snake_case.py`, `test_<module>.py`, `snake_case.ts`, `<module>.test.ts`, `kebab-case` for config and manifest files.
|
||||
- **Python:** Pydantic v2 for models and config. Providers inherit a `base.py` abstract class; config lives in `configs.py`.
|
||||
- **TypeScript:** strict mode, tsup builds, ES module imports.
|
||||
- **Commits:** [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, `docs:`, `refactor:`, `test:`).
|
||||
- **Versions:** bump in `pyproject.toml` or `package.json`. Releases are cut by tag prefix; see [`.github/AGENTS.md`](.github/AGENTS.md).
|
||||
|
||||
## Benchmarking
|
||||
|
||||
Benchmarks (LOCOMO, LongMemEval, BEAM) live in [`mem0ai/memory-benchmarks`](https://github.com/mem0ai/memory-benchmarks). The in-repo `evaluation/` path is a submodule pinned to that repo's `main`:
|
||||
|
||||
```bash
|
||||
# Environment setup (uses Hatch)
|
||||
hatch shell dev_py_3_11 # or dev_py_3_9, dev_py_3_10, dev_py_3_12
|
||||
|
||||
# Linting and formatting
|
||||
make lint # ruff check
|
||||
make format # ruff format
|
||||
make sort # isort mem0/
|
||||
|
||||
# Tests
|
||||
make test # pytest tests/
|
||||
make test-py-3.9 # test specific Python version (3.9–3.12)
|
||||
|
||||
# Build and publish
|
||||
make build # hatch build
|
||||
make publish # hatch publish
|
||||
git submodule update --init evaluation
|
||||
```
|
||||
|
||||
- **Python:** 3.9, 3.10, 3.11, 3.12
|
||||
- **Linter/formatter:** Ruff (line length **120**)
|
||||
- **Import sorting:** isort (`profile = "black"`)
|
||||
- **Test framework:** pytest (with pytest-mock, pytest-asyncio)
|
||||
- **Pre-commit hooks:** ruff + isort — run `pre-commit install` before committing
|
||||
## What to ship with a change
|
||||
|
||||
### TypeScript SDK (`mem0-ts/`)
|
||||
Guidelines, not rules. Trivial fixes need less; anything user-facing needs more.
|
||||
|
||||
```bash
|
||||
cd mem0-ts
|
||||
pnpm install
|
||||
pnpm run build # tsup
|
||||
pnpm run test # jest (all tests)
|
||||
pnpm run test:unit # jest --coverage (unit tests only)
|
||||
pnpm run test:integration # jest (integration tests, needs MEM0_API_KEY)
|
||||
pnpm run test:ci # jest --coverage --ci (CI mode)
|
||||
pnpm run test:watch # jest watch mode
|
||||
```
|
||||
| Change | Expect |
|
||||
|--------|--------|
|
||||
| **Bug fix** | A regression test that fails without the fix, written first. The fix. The relevant suite passing. The package's linter run. |
|
||||
| **New feature** | Implementation following existing patterns, test coverage, `docs/` updates for public APIs, an example if the behavior is user-facing, and an `llms.txt` entry for any new `.mdx` page. |
|
||||
| **New provider** | See [Adding a provider](mem0/AGENTS.md#adding-a-provider). |
|
||||
| **New integration** | See [Adding an integration](integrations/AGENTS.md#adding-an-integration). |
|
||||
| **Refactor** | Tests for changed behavior, existing tests still green. No docs needed for internal-only changes. |
|
||||
|
||||
- **Node:** 20, 22 (CI-tested)
|
||||
- **Build:** tsup (CJS + ESM)
|
||||
- **Test:** jest
|
||||
- **Formatter:** prettier
|
||||
Fix bugs at the root, not at the symptom. If a guard belongs in a shared function, put it there rather than in each caller.
|
||||
|
||||
### Python CLI (`cli/python/`)
|
||||
## Contributing
|
||||
|
||||
```bash
|
||||
cd cli/python
|
||||
pip install -e ".[dev]" # dev install with ruff + pytest
|
||||
ruff check . # lint
|
||||
ruff format . # format
|
||||
pytest # test
|
||||
hatch build # build
|
||||
```
|
||||
Full guide: [`CONTRIBUTING.md`](CONTRIBUTING.md). Conduct: [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md).
|
||||
|
||||
- **Python:** 3.10+ (not 3.9)
|
||||
- **Linter/formatter:** Ruff (line length **100** — different from root SDK)
|
||||
- **Ruff rules:** E, F, I, W, UP, B, SIM, RUF (ignores E501, B008 for Typer patterns, SIM108)
|
||||
- **Framework:** Typer + Rich + httpx
|
||||
- **Entry point:** `mem0 = "mem0_cli.app:main"`
|
||||
- **Source layout:** `src/mem0_cli/`
|
||||
- **Optional dependency:** `mem0ai` (for OSS mode, via `[oss]` extra)
|
||||
1. Open an issue **first** and wait for a maintainer to apply the `accepted` label. Every PR must link it with `Closes #<number>`. PRs without an accepted linked issue are closed automatically by the [PR Gate](.github/workflows/pr-gate.yml), with a reopen path. Documentation-only changes are exempt.
|
||||
2. Fork, then branch from `main` (`feature/...`, `fix/...`).
|
||||
3. Make the change: code, tests, docs, examples.
|
||||
4. Run lint and tests for **every** package you touched.
|
||||
5. Commit with Conventional Commits.
|
||||
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.**
|
||||
|
||||
### Node CLI (`cli/node/`)
|
||||
### Two gates decide whether your pull request stays open
|
||||
|
||||
```bash
|
||||
cd cli/node
|
||||
pnpm install
|
||||
pnpm run build # tsup
|
||||
pnpm run lint # biome check src/
|
||||
pnpm run lint:fix # biome check --write src/
|
||||
pnpm run typecheck # tsc --noEmit
|
||||
pnpm run test # vitest run
|
||||
pnpm run test:watch # vitest (watch mode)
|
||||
pnpm run dev # tsx src/index.ts (development)
|
||||
```
|
||||
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.
|
||||
|
||||
- **Node:** 18+ required
|
||||
- **Build:** tsup (ESM)
|
||||
- **Linter:** Biome (not ESLint, not Ruff)
|
||||
- **Test:** vitest (not jest)
|
||||
- **Framework:** Commander + Chalk + ora + cli-table3
|
||||
**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.
|
||||
|
||||
### Vercel AI SDK Provider (`integrations/vercel-ai-sdk/`)
|
||||
**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:
|
||||
|
||||
```bash
|
||||
cd integrations/vercel-ai-sdk
|
||||
pnpm install
|
||||
pnpm run build # tsup
|
||||
pnpm run lint # eslint
|
||||
pnpm run type-check # tsc --noEmit
|
||||
pnpm run prettier-check # prettier --check
|
||||
pnpm run test # jest
|
||||
pnpm run test:edge # vitest (edge runtime)
|
||||
pnpm run test:node # vitest (node runtime)
|
||||
```
|
||||
| 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 |
|
||||
|
||||
- **Build:** tsup (CJS + ESM)
|
||||
- **Lint:** ESLint + Prettier
|
||||
- **Test:** jest + vitest (edge/node configs)
|
||||
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.
|
||||
|
||||
### OpenClaw Plugin (`integrations/openclaw/`)
|
||||
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.
|
||||
|
||||
```bash
|
||||
cd integrations/openclaw
|
||||
pnpm install
|
||||
pnpm run build # tsup
|
||||
pnpm run test # vitest run
|
||||
```
|
||||
### The CLA is not optional
|
||||
|
||||
- **Build:** tsup (ESM)
|
||||
- **Test:** vitest (with Codecov in CI)
|
||||
- **Plugin manifest:** `openclaw.plugin.json`
|
||||
**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.
|
||||
|
||||
### Server (`server/`)
|
||||
The `CLAassistant` bot comments on your first pull request with a link. Signing takes under a minute, is done once per GitHub account, and covers every contribution you make afterwards. Until it is signed the `license/cla` check stays red.
|
||||
|
||||
```bash
|
||||
# Docker production build
|
||||
cd server
|
||||
make build # docker build -t mem0-api-server .
|
||||
make run_local # docker run -p 8000:8000 with .env
|
||||
If you are an agent opening a pull request on someone's behalf, tell them they must sign it themselves. Nobody else can sign for them, and the pull request goes nowhere until they do.
|
||||
|
||||
# Docker Compose development (FastAPI + PostgreSQL/pgvector + Neo4j)
|
||||
cd server
|
||||
docker-compose up # starts all 3 services
|
||||
# mem0 API: localhost:8888
|
||||
# PostgreSQL: localhost:8432
|
||||
# Neo4j HTTP: localhost:8474, Bolt: localhost:8687
|
||||
```
|
||||
### What gets a pull request closed
|
||||
|
||||
- **Framework:** FastAPI with uvicorn (auto-reload in dev)
|
||||
- **Services:** PostgreSQL with pgvector, Neo4j 5.x with APOC plugin
|
||||
- **Hot reload:** Dev Dockerfile mounts `server/` and `mem0/` for live changes
|
||||
Beyond the CLA and the accepted-issue gate, the [Contribution Conduct](CODE_OF_CONDUCT.md#contribution-conduct) section of the code of conduct is the enforceable form of this repo's anti-slop policy:
|
||||
|
||||
### OpenMemory (`openmemory/`)
|
||||
- **Disclose AI use.** The PR template asks how the *code* was written; drafting the description with a model is fine. The disclosure is never held against you, it tells a reviewer where to look. Silence followed by a review comment you cannot answer is what costs everyone the afternoon.
|
||||
- **Do not submit work you have not run.** A bug report means you reproduced it. A PR means you ran the tests.
|
||||
- **Do not fabricate evidence.** Invented tracebacks, unmeasured benchmarks, tests that assert the implementation back at itself, descriptions that describe a different change than the diff makes.
|
||||
- **Match your volume to your engagement.** Open changes at the rate you can discuss them.
|
||||
- **Do not press for merges.** One polite follow-up after a reasonable wait is fine.
|
||||
- **You must be able to explain every line of your diff** and how it interacts with the rest of the codebase, without asking an AI tool. This is the one rule that does not bend.
|
||||
|
||||
```bash
|
||||
# Full stack via Docker Compose
|
||||
cd openmemory
|
||||
docker-compose up
|
||||
# Qdrant: localhost:6333
|
||||
# API (MCP): localhost:8765
|
||||
# UI: localhost:3000
|
||||
### Reference
|
||||
|
||||
# Individual development
|
||||
cd openmemory/api && uvicorn main:app --reload # FastAPI backend
|
||||
cd openmemory/ui && npm run dev # Next.js frontend
|
||||
|
||||
# Tests
|
||||
cd openmemory/api && pytest tests/ # API tests (e.g., test_mcp_server.py)
|
||||
```
|
||||
|
||||
- **API:** FastAPI + Alembic (DB migrations) + MCP server (Model Context Protocol)
|
||||
- **UI:** Next.js 15, React 19, Radix UI, Redux Toolkit, TailwindCSS, Recharts
|
||||
- **Vector store:** Qdrant
|
||||
|
||||
### Documentation (`docs/`)
|
||||
|
||||
```bash
|
||||
make docs # or: cd docs && mintlify dev
|
||||
```
|
||||
|
||||
- **Framework:** Mintlify
|
||||
- **API spec:** `docs/openapi.json`
|
||||
- **Structure:** `api-reference/`, `open-source/`, `platform/`, `integrations/`, `cookbooks/`, `core-concepts/`
|
||||
|
||||
### Evaluation / Benchmarking
|
||||
|
||||
Benchmarking lives in the external [`mem0ai/memory-benchmarks`](https://github.com/mem0ai/memory-benchmarks) repo (LOCOMO + LongMemEval + BEAM). The in-repo `evaluation/` path is a **git submodule** pinned to that repo's `main` — populate it with `git submodule update --init evaluation` (or clone mem0 with `--recurse-submodules`), or clone the benchmarks repo standalone:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/mem0ai/memory-benchmarks.git
|
||||
cd memory-benchmarks
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Run a benchmark (Mem0 Cloud; use docker compose for OSS)
|
||||
python -m benchmarks.locomo.run --project-name my-test --backend cloud --mem0-api-key $MEM0_API_KEY
|
||||
python -m benchmarks.longmemeval.run --project-name my-test --backend cloud --mem0-api-key $MEM0_API_KEY --all-questions
|
||||
python -m benchmarks.beam.run --project-name my-test --backend cloud --mem0-api-key $MEM0_API_KEY --chat-sizes 100K --conversations 0-9
|
||||
```
|
||||
|
||||
## Core APIs
|
||||
|
||||
### Python
|
||||
|
||||
| Function / Class | Purpose | Import |
|
||||
|-----------------|---------|--------|
|
||||
| `Memory` | Self-hosted memory (sync) | `from mem0 import Memory` |
|
||||
| `AsyncMemory` | Self-hosted memory (async) | `from mem0 import AsyncMemory` |
|
||||
| `MemoryClient` | Hosted platform client (sync) | `from mem0 import MemoryClient` |
|
||||
| `AsyncMemoryClient` | Hosted platform client (async) | `from mem0 import AsyncMemoryClient` |
|
||||
|
||||
**Key `Memory` / `MemoryClient` methods:**
|
||||
|
||||
| Method | Purpose |
|
||||
|--------|---------|
|
||||
| `add(messages, *, user_id, agent_id, run_id, metadata)` | Store a new memory |
|
||||
| `search(query, *, user_id, agent_id, run_id, limit, filters)` | Search memories |
|
||||
| `get(memory_id)` | Retrieve a single memory by ID |
|
||||
| `get_all(*, user_id, agent_id, run_id, limit)` | List all memories |
|
||||
| `update(memory_id, data)` | Update a memory |
|
||||
| `delete(memory_id)` | Delete a memory |
|
||||
| `delete_all(*, user_id, agent_id, run_id)` | Delete all memories |
|
||||
| `history(memory_id)` | Get change history for a memory |
|
||||
|
||||
### TypeScript
|
||||
|
||||
| Export | Purpose | Import |
|
||||
|--------|---------|--------|
|
||||
| `MemoryClient` | Hosted platform client | `import { MemoryClient } from 'mem0ai'` |
|
||||
| `Memory` | Self-hosted OSS memory | `import { Memory } from 'mem0ai/oss'` |
|
||||
|
||||
## Import Patterns
|
||||
|
||||
### Python
|
||||
|
||||
| What | Import |
|
||||
|------|--------|
|
||||
| Core memory classes | `from mem0 import Memory, AsyncMemory` |
|
||||
| Platform client | `from mem0 import MemoryClient, AsyncMemoryClient` |
|
||||
| Configuration | `from mem0.configs.base import MemoryConfig` |
|
||||
| LLM providers | `from mem0.llms.<provider> import <ProviderLLM>` |
|
||||
| Embedding providers | `from mem0.embeddings.<provider> import <ProviderEmbedding>` |
|
||||
| Vector store providers | `from mem0.vector_stores.<provider> import <ProviderVectorStore>` |
|
||||
|
||||
### TypeScript
|
||||
|
||||
| What | Import |
|
||||
|------|--------|
|
||||
| Hosted client | `import { MemoryClient } from 'mem0ai'` |
|
||||
| OSS memory | `import { Memory } from 'mem0ai/oss'` |
|
||||
| Specific providers (OSS) | `import { OpenAIEmbedding } from 'mem0ai/oss'` |
|
||||
|
||||
## Coding Standards
|
||||
|
||||
### File Naming Conventions
|
||||
|
||||
- **Python source files:** `snake_case.py` (e.g., `azure_openai.py`, `cohere_reranker.py`)
|
||||
- **Python test files:** `test_<module>.py` (e.g., `test_memory.py`, `test_main.py`)
|
||||
- **TypeScript source files:** `snake_case.ts` (e.g., `azure_ai_search.ts`)
|
||||
- **TypeScript test files:** `<module>.test.ts` (e.g., `memory.test.ts`)
|
||||
- **Config/manifest files:** `kebab-case` (e.g., `openclaw.plugin.json`, `jest.config.js`)
|
||||
|
||||
### Python Conventions
|
||||
|
||||
- **Provider pattern:** All providers (LLMs, embeddings, vector stores, graphs, rerankers) inherit from a `base.py` abstract class in their directory. Config classes live in `configs.py`.
|
||||
- **Pydantic v2** for all data models and configuration.
|
||||
- **Ruff** is the single linting and formatting tool — no black, no flake8.
|
||||
- Root SDK: line length **120**
|
||||
- Python CLI: line length **100** with extended rule set (UP, B, SIM, RUF)
|
||||
- **isort** with `profile = "black"` for import sorting.
|
||||
- Ruff excludes `openmemory/` from root config.
|
||||
|
||||
### TypeScript Conventions
|
||||
|
||||
- **Build:** tsup across all packages.
|
||||
- **Package manager:** pnpm everywhere (no npm, no yarn).
|
||||
- **TypeScript strict mode** across all packages.
|
||||
- **Linting varies by package:**
|
||||
|
||||
| Package | Linter | Formatter | Test Framework |
|
||||
|---------|--------|-----------|---------------|
|
||||
| `mem0-ts/` | — | Prettier | jest |
|
||||
| `cli/node/` | Biome | Biome | vitest |
|
||||
| `integrations/vercel-ai-sdk/` | ESLint | Prettier | jest + vitest |
|
||||
| `integrations/openclaw/` | — | — | vitest |
|
||||
|
||||
### Type Checking
|
||||
|
||||
Always run type checking after modifying TypeScript code:
|
||||
|
||||
```bash
|
||||
cd <package> && pnpm run typecheck # or: tsc --noEmit
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Provider Pattern
|
||||
|
||||
The SDK uses a consistent plugin architecture across 5 categories. Each category has a `base.py` abstract class and concrete provider implementations:
|
||||
|
||||
| Category | Count | Examples |
|
||||
|----------|-------|---------|
|
||||
| **LLMs** | 24 | OpenAI, Anthropic, AWS Bedrock, Azure OpenAI, Gemini, Groq, Ollama, Together, DeepSeek, vLLM, LiteLLM, LM Studio, xAI |
|
||||
| **Vector Stores** | 30 | Qdrant, Pinecone, Chroma, Weaviate, Milvus, MongoDB, Redis, Elasticsearch, pgvector, Supabase, Faiss, S3 Vectors |
|
||||
| **Embeddings** | 15 | OpenAI, Azure OpenAI, Gemini, HuggingFace, FastEmbed, Together, AWS Bedrock, Ollama, Vertex AI |
|
||||
| **Graph Stores** | 4 | Neo4j, Memgraph, Kuzu, Apache AGE |
|
||||
| **Rerankers** | 5 | Cohere, HuggingFace, LLM-based, Sentence Transformer, Zero Entropy |
|
||||
|
||||
### Two Usage Modes
|
||||
|
||||
Self-hosted `Memory` / `AsyncMemory` classes and hosted-platform `MemoryClient` — both in Python and TypeScript.
|
||||
|
||||
### Graph Memory
|
||||
|
||||
Optional layer on top of vector memory for relationship-aware retrieval. Configured via the `graph` section of `MemoryConfig`.
|
||||
|
||||
### MCP Integration
|
||||
|
||||
Model Context Protocol support in multiple places:
|
||||
|
||||
- **Remote:** MCP server at `mcp.mem0.ai`
|
||||
- **Local:** MCP server in `openmemory/api/` (FastAPI-based)
|
||||
- **Plugin:** MCP tools in `integrations/mem0-plugin/` — 9 tools: `add_memory`, `search_memories`, `get_memories`, `get_memory`, `update_memory`, `delete_memory`, `delete_all_memories`, `delete_entities`, `list_entities`
|
||||
|
||||
### Plugin & Skills System
|
||||
|
||||
- `integrations/mem0-plugin/` provides integrations for Claude Code, Cursor, and Codex via MCP server connections and lifecycle hooks for automatic memory capture.
|
||||
- `skills/` contains structured skill definitions for AI agents, split into two categories:
|
||||
- **Reference skills** (always-on SDK knowledge): `mem0` (Python + TS SDKs, framework integrations), `mem0-cli` (terminal workflows), `mem0-vercel-ai-sdk` (Vercel AI provider).
|
||||
- **Pipeline skills** (run on demand): `mem0-integrate` wires Mem0 into an existing repo via a TDD pipeline; `mem0-test-integration` verifies what the integrator produced on the same branch (the two are loosely coupled via `.mem0-integration/` artifacts); `mem0-oss-to-platform` migrates an existing project from Mem0 OSS to the hosted Platform SDK (plan, then execute on approval).
|
||||
|
||||
### Adding a New Provider
|
||||
|
||||
To add a new LLM, embedding, vector store, or reranker provider:
|
||||
|
||||
1. Create `mem0/<category>/<provider_name>.py`
|
||||
2. Inherit from the abstract base class in `mem0/<category>/base.py`
|
||||
3. Add configuration to `mem0/<category>/configs.py` (if the category uses one)
|
||||
4. Register the provider in `mem0/<category>/__init__.py`
|
||||
5. Add tests in `tests/<category>/<provider_name>/`
|
||||
6. Add any new dependencies to the appropriate optional group in `pyproject.toml` (never to core `dependencies`)
|
||||
7. Follow the exact pattern of existing providers in the same category — match method signatures, error handling, and config structure
|
||||
|
||||
### Adding a New Integration
|
||||
|
||||
Agent/editor integrations live under `integrations/`. Each is a self-contained directory (its own `package.json`/lockfile, build, and tests). To add one:
|
||||
|
||||
1. Create `integrations/<name>/` and build the integration there.
|
||||
2. If it publishes to a registry, set `repository.directory: "integrations/<name>"` in its `package.json` so npm provenance links to the correct subdirectory.
|
||||
3. Add CI/CD under `.github/workflows/` (`<name>-checks.yml`, `<name>-cd.yml`). Use `integrations/<name>` in `paths:` triggers, `working-directory`, and `cache-dependency-path`. Register the release tag prefix in the `case` block in `release.yml` (keep the bare `v*` arm last). Keep workflow **filenames** stable — npm OIDC trusted publishing is pinned to repo + workflow filename.
|
||||
4. If it is a Claude Code / editor marketplace plugin, register its path in the five `marketplace.json` files (root + `.claude-plugin/`, `.cursor-plugin/`, `.codex-plugin/`, `.agents/plugins/`).
|
||||
5. Document it under `docs/integrations/` and add the page to `docs/docs.json` and `docs/llms.txt`.
|
||||
6. Add rows to the "Key Directories" table and the CI/CD tables in this file.
|
||||
|
||||
## CI/CD
|
||||
|
||||
### CI Workflows (automated testing)
|
||||
|
||||
PR testing is orchestrated by a single entry point: **`ci-gate.yml` (CI Gate)** runs on every PR, detects which packages changed, and invokes only the relevant package workflows below as reusable workflows (`workflow_call`). Its final **`CI Gate`** job aggregates the results (skipped pipelines pass; failed or cancelled ones fail) and is the **only status check that needs to be required** in branch protection. Package workflows keep their own push-to-main and manual triggers; their `pull_request` triggers moved into the gate's path filters.
|
||||
|
||||
| Workflow | File | Standalone Triggers | Tests |
|
||||
|----------|------|---------------------|-------|
|
||||
| CI Gate | `ci-gate.yml` | All PRs | Routes to and aggregates the workflows below |
|
||||
| Python SDK | `ci.yml` | Push to main | Ruff lint + pytest on Python 3.10, 3.11, 3.12 |
|
||||
| TypeScript SDK | `ts-sdk-ci.yml` | Push to main (on `mem0-ts/`) | Prettier + build + jest on Node 20, 22 |
|
||||
| Python CLI | `cli-python-ci.yml` | Push to main (on `cli/python/`), manual | Ruff lint + pytest + hatch build on Python 3.10, 3.11, 3.12 |
|
||||
| Node CLI | `cli-node-ci.yml` | Push to main (on `cli/node/`), manual | Biome lint + tsc + vitest + tsup build on Node 20, 22 |
|
||||
| OpenClaw | `openclaw-checks.yml` | Push to main (on `integrations/openclaw/`), manual | tsc + vitest (with Codecov) + tsup build on Node 20, 22 |
|
||||
| OpenCode Plugin | `opencode-plugin-checks.yml` | Push to main (on `integrations/mem0-plugin/.opencode-plugin/`), manual | Bun: tsc type-check + build + dist artifact check |
|
||||
| Pi Agent Plugin | `pi-agent-plugin-checks.yml` | Push to main (on `integrations/pi-agent-plugin/`), manual | tsc + vitest + tsup build (dist artifact check) on Node 20, 22 |
|
||||
| docs llms.txt | `docs-llms-txt-check.yml` | Manual | `docs/llms.txt` coverage check |
|
||||
|
||||
When adding a new 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` — a path filter under the `changes` job, a call job, and an entry in the gate job's `needs` list.
|
||||
|
||||
### CD Workflows (automated publishing)
|
||||
|
||||
Publishing is routed through a single entry point: **`release.yml` (Release Router)** is the only workflow that listens to `release: published` events. It matches the release tag prefix and dispatches the corresponding package workflow via `workflow_dispatch`, so each release produces exactly one routed run (no skipped runs from the other pipelines).
|
||||
|
||||
| Workflow | File | Tag Prefix | Target |
|
||||
|----------|------|------------|--------|
|
||||
| Release Router | `release.yml` | (all releases) | dispatches the matching workflow below |
|
||||
| Python SDK | `cd.yml` | `v*` | PyPI (`mem0ai`) |
|
||||
| TypeScript SDK | `ts-sdk-cd.yml` | `ts-v*` | npm (`mem0ai`) |
|
||||
| Python CLI | `cli-python-cd.yml` | `cli-v*` | PyPI (`mem0-cli`) |
|
||||
| Node CLI | `cli-node-cd.yml` | `cli-node-v*` | npm (`@mem0/cli`) |
|
||||
| Vercel AI SDK | `vercel-ai-cd.yml` | `vercel-ai-v*` | npm (`@mem0/vercel-ai-provider`) |
|
||||
| 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`) |
|
||||
|
||||
- Package CD workflows are `workflow_dispatch`-only (inputs: `tag`, `prerelease`); they check out and build the given tag. Registry trusted-publisher settings stay pinned to each package's own workflow filename.
|
||||
- All publishing uses **OIDC trusted publishing** — no tokens or secrets required.
|
||||
- First publish of a new npm package must be done manually; OIDC works for subsequent versions.
|
||||
- To re-publish a release (e.g. after a registry settings fix), do **not** delete/recreate the GitHub release — manually dispatch the package workflow instead: `gh workflow run <package>-cd.yml --ref refs/tags/<tag> -f tag=<tag>`.
|
||||
- When adding a new package: add its CD workflow (`workflow_dispatch` with `tag`/`prerelease` inputs), then register its tag prefix in the `case` block in `release.yml`. Keep the bare `v*` arm last.
|
||||
|
||||
### Utility Workflows
|
||||
|
||||
| Workflow | File | Purpose |
|
||||
|----------|------|---------|
|
||||
| Issue Labeler | `issue-labeler.yml` | Automatic issue labeling |
|
||||
| Stale Bot | `stale.yml` | Marks stale issues and PRs |
|
||||
| llms.txt Check | `docs-llms-txt-check.yml` | Blocks PRs touching `docs/**/*.mdx` when `docs/llms.txt` is out of sync. Fix locally with `python scripts/check-llms-txt-coverage.py --write`. |
|
||||
|
||||
## Task Completion Guidelines
|
||||
|
||||
These guidelines outline typical artifacts for different task types. Use judgment to adapt based on scope and context.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
1. **Unit tests**: Add tests that would fail without the fix (regression tests)
|
||||
2. **Implementation**: Fix the bug
|
||||
3. **Manual verification**: Run the relevant test suite to confirm the fix
|
||||
4. **Lint**: Run the appropriate linter for the package you modified
|
||||
|
||||
### New Features
|
||||
|
||||
1. **Implementation**: Build the feature following existing patterns
|
||||
2. **Unit tests**: Comprehensive test coverage for new functionality
|
||||
3. **Documentation**: Update relevant docs in `docs/` for public APIs
|
||||
4. **Examples**: Add usage examples if the feature introduces new user-facing behavior
|
||||
5. **llms.txt**: Any new `.mdx` page under `docs/` must be linked in `docs/llms.txt` with a scope tag (`[Platform]` / `[OSS]` / `[Both]`) and a `Use when ...` description. The `docs-llms-txt-check.yml` workflow runs on every PR that touches docs and **fails the check** if the index is out of sync. To fix: run `python scripts/check-llms-txt-coverage.py --write` locally to scaffold placeholders under `## Unclassified - needs triage`, then replace the `[TODO: ...]` tags, rewrite descriptions as `Use when ...`, move entries into the right section, and delete the triage heading when empty.
|
||||
|
||||
### New Provider (LLM / Embedding / Vector Store / Reranker)
|
||||
|
||||
1. **Implementation**: Follow the "Adding a New Provider" steps above
|
||||
2. **Tests**: Add unit tests matching the pattern of existing providers
|
||||
3. **Configuration**: Add to the appropriate `configs.py` and `__init__.py`
|
||||
4. **Dependencies**: Add to the correct optional group in `pyproject.toml`
|
||||
5. **Documentation**: Add an integration guide in `docs/integrations/`
|
||||
|
||||
### Refactoring / Internal Changes
|
||||
|
||||
- Unit tests for any changed behavior
|
||||
- No documentation needed for internal-only changes
|
||||
- Ensure all existing tests still pass
|
||||
|
||||
### When to Deviate
|
||||
|
||||
These are guidelines, not rigid rules. Adjust based on:
|
||||
|
||||
- **Scope**: Trivial fixes (typos, comments) may not need tests
|
||||
- **Visibility**: Internal changes may not need documentation
|
||||
- **Context**: Some changes span multiple categories — use judgment
|
||||
|
||||
When uncertain about expected artifacts, ask for clarification.
|
||||
|
||||
## Contributing Guidelines
|
||||
|
||||
### Workflow
|
||||
|
||||
1. Fork and clone the repository.
|
||||
2. Create a feature branch from `main` (e.g., `feature/my-new-feature`).
|
||||
3. Make your changes — add tests, docs, and examples as appropriate.
|
||||
4. Run linting and tests for every package you modified (see commands above).
|
||||
5. Run `pre-commit install` on first setup — hooks run ruff + isort automatically.
|
||||
6. Commit with a clear message following [Conventional Commits](https://www.conventionalcommits.org/) (e.g., `feat:`, `fix:`, `docs:`, `refactor:`).
|
||||
7. Push and open a Pull Request against `main`.
|
||||
|
||||
### Pull Request Requirements
|
||||
|
||||
Every PR must follow the repo's PR template (`.github/PULL_REQUEST_TEMPLATE.md`):
|
||||
|
||||
1. **Linked Issue** — Reference the issue with `Closes #<number>`. If no issue exists, create one first or explain why in the description.
|
||||
2. **Description** — Explain what the PR does and why it's needed.
|
||||
3. **Type of Change** — Check the appropriate box:
|
||||
- Bug fix / New feature / Breaking change / Refactor / Documentation update
|
||||
4. **Breaking Changes** — If applicable, describe what breaks and the migration path.
|
||||
5. **Test Coverage** — Check what applies:
|
||||
- Added/updated unit tests
|
||||
- Added/updated integration tests
|
||||
- Tested manually (describe how)
|
||||
- No tests needed (explain why)
|
||||
6. **Checklist** — All must be checked before merge:
|
||||
- [ ] Code follows the project's style guidelines
|
||||
- [ ] Self-review performed
|
||||
- [ ] Tests added that prove the fix/feature works
|
||||
- [ ] New and existing tests pass locally
|
||||
- [ ] Documentation updated if needed
|
||||
|
||||
### PR Description Template
|
||||
|
||||
```markdown
|
||||
## Linked Issue
|
||||
|
||||
Closes #<!-- issue number -->
|
||||
|
||||
## Description
|
||||
|
||||
<!-- What does this PR do? Why is it needed? -->
|
||||
|
||||
## Type of Change
|
||||
|
||||
- [ ] Bug fix (non-breaking change that fixes an issue)
|
||||
- [ ] New feature (non-breaking change that adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
|
||||
- [ ] Refactor (no functional changes)
|
||||
- [ ] Documentation update
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
N/A
|
||||
|
||||
## Test Coverage
|
||||
|
||||
- [ ] I added/updated unit tests
|
||||
- [ ] I added/updated integration tests
|
||||
- [ ] I tested manually (describe below)
|
||||
- [ ] No tests needed (explain why)
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] My code follows the project's style guidelines
|
||||
- [ ] I have performed a self-review of my code
|
||||
- [ ] I have added tests that prove my fix/feature works
|
||||
- [ ] New and existing tests pass locally
|
||||
- [ ] I have updated documentation if needed
|
||||
```
|
||||
|
||||
### General Rules
|
||||
|
||||
- Follow existing code patterns — don't introduce new frameworks or abstractions without discussion.
|
||||
- Version bumps go in `pyproject.toml` (Python) or `package.json` (TypeScript).
|
||||
- For `server/` and `openmemory/` work, use Docker Compose for local development.
|
||||
- Do NOT use `pip` or `conda` for dependency management — use `hatch` (see `docs/contributing/development.mdx`).
|
||||
|
||||
### Contributing Guides
|
||||
|
||||
| Task | Guide |
|
||||
|------|-------|
|
||||
| Code contributions | `docs/contributing/development.mdx` |
|
||||
| Topic | File |
|
||||
|-------|------|
|
||||
| Contributor guide | [`CONTRIBUTING.md`](CONTRIBUTING.md) |
|
||||
| Code of conduct | [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) |
|
||||
| Security reports | [`SECURITY.md`](SECURITY.md) |
|
||||
| Development setup | `docs/contributing/development.mdx` |
|
||||
| Documentation contributions | `docs/contributing/documentation.mdx` |
|
||||
| PR template | `.github/PULL_REQUEST_TEMPLATE.md` |
|
||||
| Bug reports | `.github/ISSUE_TEMPLATE/bug_report.yml` |
|
||||
| Feature requests | `.github/ISSUE_TEMPLATE/feature_request.yml` |
|
||||
| Documentation issues | `.github/ISSUE_TEMPLATE/documentation_issue.yml` |
|
||||
|
||||
## Do NOT
|
||||
|
||||
- Modify CI/CD workflows without explicit approval.
|
||||
- Add new Python dependencies to the core `dependencies` list in `pyproject.toml` without discussion — use optional dependency groups instead.
|
||||
- Commit `.env` files, API keys, or credentials.
|
||||
- Skip pre-commit hooks.
|
||||
- Use npm or yarn in TypeScript packages — this repo uses pnpm exclusively.
|
||||
- Use `require()` for imports in TypeScript — use ES module `import` syntax.
|
||||
- Mix up linter configs: root Python SDK uses line-length 120, Python CLI uses 100, Node CLI uses Biome (not ESLint/Ruff).
|
||||
- Modify `openmemory/` database migrations without understanding the Alembic migration chain.
|
||||
- Change public APIs without updating documentation in `docs/`.
|
||||
| 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) |
|
||||
| CI/CD, gates, rulesets | [`.github/AGENTS.md`](.github/AGENTS.md) |
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
- Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
- The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Contribution Conduct
|
||||
|
||||
Mem0 receives more contributions than any maintainer can read line by line. The
|
||||
rules below exist so that the time we do have goes to people who are actually
|
||||
trying to improve the project. They apply to issues, pull requests, discussions,
|
||||
and reviews.
|
||||
|
||||
**Be honest about how the work was produced.** Using an AI tool to find a bug,
|
||||
write a patch, or draft a description is fine and welcome. Not saying so is not.
|
||||
Every issue form and the pull request template ask about AI, and the answer is
|
||||
never held against you. It tells a reviewer where to look. An unanswered review
|
||||
comment on code the author cannot explain is what costs us the afternoon.
|
||||
|
||||
**Do not submit work you have not verified.** A reported bug means you ran it and
|
||||
saw it. A pull request means you ran the tests. Pasting a model's output, a
|
||||
scanner result, or a plausible-looking patch and letting maintainers find out
|
||||
whether it is real moves your work onto someone else's desk. Reports and patches
|
||||
that turn out to be unverified are closed without a detailed response.
|
||||
|
||||
**Do not fabricate evidence.** Invented tracebacks, benchmark numbers you did not
|
||||
measure, reproductions that were never run, tests that assert the implementation
|
||||
back at itself, and descriptions that describe a different change than the diff
|
||||
makes are all treated the same way, regardless of whether a person or a tool
|
||||
produced them.
|
||||
|
||||
**Match your volume to your engagement.** Open changes at the rate you can
|
||||
discuss them. A queue of open pull requests from one author, none of them
|
||||
answered when questioned, is treated as automated submission and handled under
|
||||
enforcement below, whatever the individual diffs look like.
|
||||
|
||||
**Do not press for merges.** Bumping a thread, tagging maintainers repeatedly,
|
||||
asking in Discord or by direct message for a review, and reopening a closed pull
|
||||
request without addressing why it was closed all take attention away from the
|
||||
queue rather than moving your change through it. One polite follow-up after a
|
||||
reasonable wait is fine.
|
||||
|
||||
**Do not contribute for a badge.** Changes made to raise a contribution count,
|
||||
qualify for an event, or pad a profile, whitespace edits, README churn, and
|
||||
mechanical reformatting bundled with nothing else, are closed on sight.
|
||||
|
||||
**Disagreement is fine, and closing is not a verdict.** Our pull request gate
|
||||
closes changes that do not yet link an accepted issue. That is a queue decision,
|
||||
not a judgment of you or your code, and reopening takes about a minute. Argue for
|
||||
your change on its merits; that is a normal and welcome part of contributing.
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, including this
|
||||
repository, our Discord, and our documentation, and also applies when an
|
||||
individual is officially representing the community in public spaces. Examples of
|
||||
representing our community include using an official email address, posting via
|
||||
an official social media account, or acting as an appointed representative at an
|
||||
online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the maintainers at **support@mem0.ai**. All complaints will be
|
||||
reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community, or a first contribution that
|
||||
breaches the Contribution Conduct rules above.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of
|
||||
actions, including a repeated pattern of unverified or automated submissions
|
||||
after a first warning.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban. At this stage the account may be denounced in `.github/VOUCHED.td`, which
|
||||
means new pull requests are flagged automatically.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||
community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
The Contribution Conduct section is specific to this repository.
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][mozilla].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][faq]. Translations are available at
|
||||
[https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[mozilla]: https://github.com/mozilla/inclusion
|
||||
[faq]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
+212
-47
@@ -1,72 +1,235 @@
|
||||
# Contributing to mem0
|
||||
# Contributing to Mem0
|
||||
|
||||
Let us make contribution easy, collaborative and fun.
|
||||
First off, thank you for taking the time to contribute! 🎉 Mem0 is a
|
||||
community-driven project and we welcome contributions of all kinds — bug fixes,
|
||||
new features, documentation, examples, and integrations.
|
||||
|
||||
## Submit your Contribution through PR
|
||||
Mem0 is a polyglot monorepo, and this guide covers contributing to both the
|
||||
**Python SDK** and the **TypeScript SDK** (and the rest of the repository).
|
||||
|
||||
To make a contribution, follow these steps:
|
||||
By participating you agree to our [Code of Conduct](./CODE_OF_CONDUCT.md). Its
|
||||
**Contribution Conduct** section is the enforceable form of the rules on this
|
||||
page: disclose AI use, don't submit work you haven't run, don't fabricate
|
||||
reproductions or benchmarks, keep your volume matched to your engagement, and
|
||||
don't press for merges.
|
||||
|
||||
1. Fork and clone this repository
|
||||
2. Do the changes on your fork with dedicated feature branch `feature/f1`
|
||||
3. If you modified the code (new feature or bug-fix), please add tests for it
|
||||
4. Include proper documentation / docstring and examples to run the feature
|
||||
5. Ensure that all tests pass
|
||||
6. Submit a pull request
|
||||
## Before You Start
|
||||
|
||||
For more details about pull requests, please read [GitHub's guides](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request).
|
||||
### 1. Open an Issue First
|
||||
|
||||
**Always open an issue before opening a pull request.** This lets us discuss the
|
||||
change, avoid duplicate effort, and agree on the approach before you invest time
|
||||
in code.
|
||||
|
||||
### 📦 Development Environment
|
||||
- Search [existing issues](https://github.com/mem0ai/mem0/issues) first to see if
|
||||
your bug or idea already exists.
|
||||
- If it doesn't, open a
|
||||
[bug report](https://github.com/mem0ai/mem0/issues/new?template=bug_report.yml) or
|
||||
[feature request](https://github.com/mem0ai/mem0/issues/new?template=feature_request.yml).
|
||||
- For anything beyond a trivial fix, wait for a maintainer to confirm the approach
|
||||
before starting significant work.
|
||||
|
||||
We use `hatch` for managing development environments. To set up:
|
||||
A bug report needs a reproduction we can run, the version you are on, and the
|
||||
real output or traceback you saw. Reports without those cannot be acted on and
|
||||
get closed. A feature request needs the problem you hit and the workaround you
|
||||
are living with, not just the API you would like.
|
||||
|
||||
Every pull request must link to an issue using `Closes #<issue-number>`, and that
|
||||
issue must carry the `accepted` label. A maintainer applies `accepted` once we
|
||||
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.
|
||||
|
||||
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
|
||||
a private advisory and a private fork so the vulnerability isn't disclosed before
|
||||
the fix ships.
|
||||
|
||||
### 2. Understand Your Code
|
||||
|
||||
**You must be able to explain what your changes do and how they interact with
|
||||
the rest of the codebase without the help of an AI tool.** This is the one rule
|
||||
we will not bend on.
|
||||
|
||||
Using AI to write code is fine. Most of us do. You can build real understanding
|
||||
by interrogating an agent about this codebase until you grasp the edge cases and
|
||||
the blast radius of your change. What is not fine is opening a pull request for
|
||||
a diff you cannot defend in review.
|
||||
|
||||
Disclose it in the pull request template and say what you checked yourself.
|
||||
We ask about the code, not the write-up: using AI to draft the pull request
|
||||
description is fine. We ask because it tells reviewers where to look, not
|
||||
because it counts against you. An honest "an agent wrote this, here is what I
|
||||
verified" is welcome. Silence, followed by a review comment you cannot answer,
|
||||
is what wastes everyone's time.
|
||||
|
||||
Signs your pull request will be closed:
|
||||
|
||||
- Invented APIs, config keys, or providers that don't exist in this repo.
|
||||
- Tests that assert the implementation back at itself rather than the behaviour.
|
||||
- A description that describes a different change than the diff makes.
|
||||
- Sweeping unrelated reformatting bundled with a small fix.
|
||||
- You cannot answer a direct question about your own diff.
|
||||
|
||||
### 3. Sign the Contributor License Agreement (CLA)
|
||||
|
||||
**We cannot accept or merge any pull request until you have signed our Contributor
|
||||
License Agreement (CLA).**
|
||||
|
||||
When you open your first PR, the CLA bot will automatically comment with a link to
|
||||
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:
|
||||
|
||||
| Package | Path | Language | Package manager |
|
||||
| --------------------- | ---------- | ------------ | --------------- |
|
||||
| Python SDK (`mem0ai`) | `mem0/` | Python 3.9+ | `hatch` |
|
||||
| TypeScript SDK (`mem0ai`) | `mem0-ts/` | TypeScript | `pnpm` |
|
||||
|
||||
Other packages include the CLIs (`cli/python/`, `cli/node/`), integrations
|
||||
(`integrations/`), the self-hosted `server/`, and the docs site
|
||||
(`docs/`). See [AGENTS.md](./AGENTS.md) for a full map of the repository.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **Fork** the repository and **clone** your fork.
|
||||
2. Create a **feature branch** from `main` (e.g. `feature/my-new-feature` or
|
||||
`fix/issue-1234`).
|
||||
3. Make your changes — add **tests**, **documentation**, and **examples** as
|
||||
appropriate.
|
||||
4. Run **linting and tests** for every package you touched (see below).
|
||||
5. Commit using [Conventional Commits](https://www.conventionalcommits.org/)
|
||||
(e.g. `feat:`, `fix:`, `docs:`, `refactor:`, `test:`).
|
||||
6. Push and open a **pull request** against `main`, linking the issue with
|
||||
`Closes #<number>` and filling out the
|
||||
[PR template](./.github/PULL_REQUEST_TEMPLATE.md).
|
||||
|
||||
### Contributing to the Python SDK (`mem0/`)
|
||||
|
||||
We use [`hatch`](https://hatch.pypa.io/latest/install/) to manage environments.
|
||||
**Do not use `pip` or `conda` for dependency management.**
|
||||
|
||||
```bash
|
||||
# Activate environment for specific Python version:
|
||||
hatch shell dev_py_3_9 # Python 3.9
|
||||
hatch shell dev_py_3_10 # Python 3.10
|
||||
hatch shell dev_py_3_11 # Python 3.11
|
||||
hatch shell dev_py_3_12 # Python 3.12
|
||||
# Activate a dev environment (3.9 / 3.10 / 3.11 / 3.12)
|
||||
hatch shell dev_py_3_11
|
||||
|
||||
# The environment will automatically install all dev dependencies
|
||||
# Run tests within the activated shell:
|
||||
make test
|
||||
```
|
||||
|
||||
### 📌 Pre-commit
|
||||
|
||||
To ensure our standards, make sure to install pre-commit before starting to contribute.
|
||||
|
||||
```bash
|
||||
# Install pre-commit hooks (runs ruff + isort on commit)
|
||||
pre-commit install
|
||||
|
||||
# Lint, format, and sort imports
|
||||
make lint
|
||||
make format
|
||||
make sort
|
||||
|
||||
# Run the test suite (run `make install_all` first if deps are missing)
|
||||
make test
|
||||
```
|
||||
|
||||
### 🧪 Testing
|
||||
- **Linter / formatter:** Ruff (line length **120**)
|
||||
- **Import sorting:** isort (`profile = "black"`)
|
||||
- **Tests:** pytest (in `tests/`)
|
||||
|
||||
We use `pytest` to test our code across multiple Python versions. You can run tests using:
|
||||
See the full [Development guide](https://docs.mem0.ai/contributing/development) for
|
||||
environment details.
|
||||
|
||||
### Contributing to the TypeScript SDK (`mem0-ts/`)
|
||||
|
||||
We use [`pnpm`](https://pnpm.io/) (v10+) for all TypeScript packages. **Do not use
|
||||
`npm` or `yarn`.**
|
||||
|
||||
```bash
|
||||
# Run tests with default Python version
|
||||
make test
|
||||
cd mem0-ts
|
||||
pnpm install
|
||||
|
||||
# Test specific Python versions:
|
||||
make test-py-3.9 # Python 3.9 environment
|
||||
make test-py-3.10 # Python 3.10 environment
|
||||
make test-py-3.11 # Python 3.11 environment
|
||||
make test-py-3.12 # Python 3.12 environment
|
||||
|
||||
# When using hatch shells, run tests with:
|
||||
make test # After activating a shell with hatch shell test_XX
|
||||
pnpm run build # tsup (CJS + ESM)
|
||||
pnpm run test # jest (all tests)
|
||||
pnpm run test:unit # unit tests with coverage
|
||||
```
|
||||
|
||||
Make sure that all tests pass across all supported Python versions before submitting a pull request.
|
||||
- **Build:** tsup
|
||||
- **Formatter:** Prettier
|
||||
- **Tests:** jest
|
||||
- Always run type checking after changes: `pnpm run typecheck` (or `tsc --noEmit`).
|
||||
- Use ES module `import` syntax — never `require()`.
|
||||
|
||||
We look forward to your pull requests and can't wait to see your contributions!
|
||||
## Good Contribution Practices
|
||||
|
||||
### 🚀 Releasing
|
||||
- **Keep PRs small and focused.** One logical change per PR is easier to review and
|
||||
merge.
|
||||
- **Follow existing patterns.** Match the style, structure, and conventions of the
|
||||
code around you. Don't introduce new frameworks or abstractions without
|
||||
discussion.
|
||||
- **Write tests** that would fail without your change — regression tests for bugs,
|
||||
coverage for new features.
|
||||
- **Update documentation** in `docs/` for any user-facing change. New `.mdx` pages
|
||||
must be added to `docs/llms.txt` (run
|
||||
`python scripts/check-llms-txt-coverage.py --write` to scaffold entries).
|
||||
- **Add examples** when introducing new user-facing behavior.
|
||||
- **Run linters and tests locally** before pushing — CI re-runs them on every PR
|
||||
via the CI Gate.
|
||||
- **Never commit secrets** — no `.env` files, API keys, or credentials.
|
||||
- **Don't add core dependencies lightly.** New Python dependencies belong in an
|
||||
optional group in `pyproject.toml`, not the core `dependencies` list.
|
||||
- **Be responsive** to review feedback and keep your branch up to date with `main`.
|
||||
|
||||
All packages are published automatically via GitHub Actions when a GitHub Release is created with the correct tag prefix.
|
||||
## Pull Request Checklist
|
||||
|
||||
#### Tag Prefixes
|
||||
Before requesting review, make sure:
|
||||
|
||||
- [ ] An issue exists and is linked with `Closes #<number>`
|
||||
- [ ] You have signed the CLA
|
||||
- [ ] Your code follows the project's style guidelines (lint passes)
|
||||
- [ ] You performed a self-review of your changes
|
||||
- [ ] Tests are added/updated and pass locally
|
||||
- [ ] Documentation is updated if needed
|
||||
|
||||
## Reporting Security Issues
|
||||
|
||||
**Do not report security vulnerabilities through public issues or pull requests.**
|
||||
Please follow our [Security Policy](./SECURITY.md) to report them privately.
|
||||
|
||||
## Releasing
|
||||
|
||||
All packages are published automatically via GitHub Actions when a GitHub Release
|
||||
is created with the correct tag prefix.
|
||||
|
||||
### Tag Prefixes
|
||||
|
||||
| Package | Registry | Tag Prefix | Example |
|
||||
|---------|----------|------------|---------|
|
||||
@@ -77,15 +240,17 @@ All packages are published automatically via GitHub Actions when a GitHub Releas
|
||||
| `@mem0/vercel-ai-provider` | npm | `vercel-ai-v*` | `vercel-ai-v2.0.6` |
|
||||
| `@mem0/openclaw-mem0` | npm | `openclaw-v*` | `openclaw-v1.0.1` |
|
||||
|
||||
#### How to Release
|
||||
### How to Release
|
||||
|
||||
1. Bump the version in `pyproject.toml` (Python) or `package.json` (Node)
|
||||
2. Create a [GitHub Release](https://github.com/mem0ai/mem0/releases/new) with the matching tag prefix
|
||||
3. The correct workflow will trigger automatically — verify in the [Actions tab](https://github.com/mem0ai/mem0/actions)
|
||||
|
||||
#### Publishing Details
|
||||
### Publishing Details
|
||||
|
||||
- **PyPI packages** use OIDC trusted publishing via `pypa/gh-action-pypi-publish`
|
||||
- **npm packages** use OIDC trusted publishing via npm CLI (>= 11.5.1) — no tokens or secrets required
|
||||
- All workflows require `permissions: id-token: write` for OIDC authentication
|
||||
- First publish of a new npm package must be done manually; OIDC works for subsequent versions
|
||||
|
||||
We look forward to your pull requests and can't wait to see your contributions!
|
||||
|
||||
@@ -11,7 +11,7 @@ install:
|
||||
hatch env create
|
||||
|
||||
install_all:
|
||||
pip install ruff==0.6.9 groq together boto3 litellm ollama chromadb weaviate weaviate-client sentence_transformers vertexai \
|
||||
pip install ruff==0.16.0 groq together boto3 'litellm>=1.83.7,<1.98.0' 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
|
||||
|
||||
|
||||
@@ -46,12 +46,12 @@
|
||||
|
||||
| Benchmark | Old | New | Tokens | Latency p50 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| **LoCoMo** | 71.4 | **91.6** | 7.0K | 0.88s |
|
||||
| **LongMemEval** | 67.8 | **94.8** | 6.8K | 1.09s |
|
||||
| **LoCoMo** | 71.4 | **92.5** | 7.0K | 0.88s |
|
||||
| **LongMemEval** | 67.8 | **94.4** | 6.8K | 1.09s |
|
||||
| **BEAM (1M)** | — | **64.1** | 6.7K | 1.00s |
|
||||
| **BEAM (10M)** | — | **48.6** | 6.9K | 1.05s |
|
||||
|
||||
All benchmarks run on the same production-representative model stack. Single-pass retrieval (one call, no agentic loops).
|
||||
All benchmarks run on the same production-representative model stack. Single-pass retrieval (one call, no agentic loops) at a top_200 retrieval budget. Scores reflect Mem0's managed platform, which includes proprietary optimizations not available in the open-source SDK; open-source users should expect directionally similar gains but not identical numbers.
|
||||
|
||||
**What changed:**
|
||||
- **Single-pass ADD-only extraction** -- one LLM call, no UPDATE/DELETE. Memories accumulate; nothing is overwritten.
|
||||
@@ -63,8 +63,8 @@ All benchmarks run on the same production-representative model stack. Single-pas
|
||||
See the [migration guide](https://docs.mem0.ai/migration/oss-v2-to-v3) for upgrade instructions. The [evaluation framework](https://github.com/mem0ai/memory-benchmarks) is open-sourced so anyone can reproduce the numbers.
|
||||
|
||||
## Research Highlights
|
||||
- **91.6 on LoCoMo** -- +20 points over the previous algorithm
|
||||
- **94.8 on LongMemEval** -- +27 points, with +53.6 on assistant memory recall
|
||||
- **92.5 on LoCoMo** -- +21 points over the previous algorithm
|
||||
- **94.4 on LongMemEval** -- +27 points, with 98.2 on assistant memory recall
|
||||
- **64.1 on BEAM (1M)** -- production-scale memory evaluation at 1M tokens
|
||||
- [Read the full paper](https://mem0.ai/research)
|
||||
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
# Security Policy
|
||||
|
||||
We take the security of Mem0 and our community seriously. Thank you for helping
|
||||
keep Mem0 and its users safe by disclosing vulnerabilities responsibly.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please **do not** report security vulnerabilities through public GitHub issues,
|
||||
pull requests, or discussions.
|
||||
|
||||
If you believe you have found a security vulnerability in Mem0, please report it
|
||||
privately through one of the following channels:
|
||||
|
||||
1. **GitHub Private Vulnerability Reporting** — open a
|
||||
[private security advisory](https://github.com/mem0ai/mem0/security/advisories/new)
|
||||
directly on this repository.
|
||||
2. **Email** the maintainers at **support@mem0.ai** with the subject line:
|
||||
|
||||
`SECURITY: Mem0 vulnerability report`
|
||||
|
||||
To help us triage and resolve the issue quickly, please include as much of the
|
||||
following as you can:
|
||||
|
||||
- Affected component or package (e.g. Python SDK, TypeScript SDK, server, CLI)
|
||||
- Affected version, tag, or commit
|
||||
- Clear, step-by-step reproduction instructions
|
||||
- The security impact and a proof of concept, if available
|
||||
- Any suggested fix or mitigation
|
||||
- Whether an AI tool was involved in finding or writing up the report
|
||||
|
||||
Reports generated by an AI tool are welcome, but only once you have run the
|
||||
reproduction yourself and confirmed the impact is real. A scan result or model
|
||||
output pasted in without that step is not a vulnerability report, and we close
|
||||
those without a detailed response so we can spend the time on real ones.
|
||||
|
||||
## Response Process
|
||||
|
||||
- We will acknowledge receipt of your report within **72 hours**.
|
||||
- We will work with you privately to confirm the issue and assess its impact.
|
||||
- Once a fix or mitigation is ready, we will coordinate a disclosure timeline
|
||||
with you and credit you for the discovery, unless you prefer to remain anonymous.
|
||||
|
||||
## Public Disclosure
|
||||
|
||||
Please avoid sharing technical details of the vulnerability publicly until the
|
||||
maintainers have reviewed the issue and a fix or mitigation has been released. We
|
||||
are committed to resolving valid reports promptly and keeping you informed
|
||||
throughout the process.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We release security fixes against the latest published version of each package.
|
||||
Whenever possible, please reproduce the issue on the most recent release before
|
||||
reporting.
|
||||
+1
-2
@@ -60,9 +60,8 @@ mem0 delete <memory-id>
|
||||
| `mem0 entity` | List or delete entities (users, agents, apps, runs) |
|
||||
| `mem0 event` | Inspect background processing events (bulk deletes, large add jobs) |
|
||||
| `mem0 status` | Verify API connection and display current project |
|
||||
| `mem0 version` | Print the CLI version |
|
||||
|
||||
Run `mem0 <command> --help` for detailed usage on any command.
|
||||
Run `mem0 <command> --help` for detailed usage on any command, or `mem0 --version` to print the CLI version.
|
||||
|
||||
## Agent mode
|
||||
|
||||
|
||||
+13
-1
@@ -207,7 +207,11 @@
|
||||
{ "name": "immutable", "flags": ["--immutable"], "type": "boolean", "default": false, "help": "Prevent future updates." },
|
||||
{ "name": "no_infer", "flags": ["--no-infer"], "type": "boolean", "default": false, "help": "Skip inference, store raw." },
|
||||
{ "name": "expires", "flags": ["--expires"], "type": "string", "help": "Expiration date (YYYY-MM-DD)." },
|
||||
{ "name": "categories", "flags": ["--categories"], "type": "string", "help": "Categories (JSON array or comma-separated)." },
|
||||
{ "name": "categories", "flags": ["--categories"], "type": "string", "help": "Not supported on add, use --custom-categories instead." },
|
||||
{ "name": "custom_instructions", "flags": ["--custom-instructions"], "type": "string", "help": "Custom instructions for fact extraction." },
|
||||
{ "name": "custom_categories", "flags": ["--custom-categories"], "type": "string", "help": "Custom categories as a JSON array of {name: description} objects." },
|
||||
{ "name": "structured_data_schema", "flags": ["--structured-data-schema"], "type": "string", "help": "Schema for structured data extraction, as JSON." },
|
||||
{ "name": "timestamp", "flags": ["--timestamp"], "type": "integer", "help": "Unix timestamp for the memory." },
|
||||
{ "name": "graph", "flags": ["--graph"], "type": "boolean", "default": false, "help": "Enable graph memory extraction.", "panel": "Scope" },
|
||||
{ "name": "no_graph", "flags": ["--no-graph"], "type": "boolean", "default": false, "help": "Disable graph memory extraction.", "panel": "Scope" },
|
||||
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output format: text, json, quiet.", "panel": "Output" }
|
||||
@@ -244,6 +248,9 @@
|
||||
{ "name": "keyword", "flags": ["--keyword"], "type": "boolean", "default": false, "help": "Use keyword search.", "panel": "Search" },
|
||||
{ "name": "filter_json", "flags": ["--filter"], "type": "string", "help": "Advanced filter expression (JSON).", "panel": "Search" },
|
||||
{ "name": "fields", "flags": ["--fields"], "type": "string", "help": "Specific fields to return (comma-separated).", "panel": "Search" },
|
||||
{ "name": "show_expired", "flags": ["--show-expired"], "type": "boolean", "default": false, "help": "Include expired memories.", "panel": "Search" },
|
||||
{ "name": "reference_date", "flags": ["--reference-date"], "type": "string", "help": "Reference date for relative queries (YYYY-MM-DD or unix timestamp).", "panel": "Search" },
|
||||
{ "name": "latest_only", "flags": ["--latest-only"], "type": "boolean", "default": false, "help": "Only return the latest version of each memory.", "panel": "Search" },
|
||||
{ "name": "graph", "flags": ["--graph"], "type": "boolean", "default": false, "help": "Enable graph in search.", "panel": "Search" },
|
||||
{ "name": "no_graph", "flags": ["--no-graph"], "type": "boolean", "default": false, "help": "Disable graph in search.", "panel": "Search" },
|
||||
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output: text, json, table.", "panel": "Output" }
|
||||
@@ -296,6 +303,8 @@
|
||||
{ "name": "category", "flags": ["--category"], "type": "string", "help": "Filter by category.", "panel": "Filters" },
|
||||
{ "name": "after", "flags": ["--after"], "type": "string", "help": "Created after (YYYY-MM-DD).", "panel": "Filters" },
|
||||
{ "name": "before", "flags": ["--before"], "type": "string", "help": "Created before (YYYY-MM-DD).", "panel": "Filters" },
|
||||
{ "name": "show_expired", "flags": ["--show-expired"], "type": "boolean", "default": false, "help": "Include expired memories.", "panel": "Filters" },
|
||||
{ "name": "latest_only", "flags": ["--latest-only"], "type": "boolean", "default": false, "help": "Only return the latest version of each memory.", "panel": "Filters" },
|
||||
{ "name": "graph", "flags": ["--graph"], "type": "boolean", "default": false, "help": "Enable graph in listing.", "panel": "Filters" },
|
||||
{ "name": "no_graph", "flags": ["--no-graph"], "type": "boolean", "default": false, "help": "Disable graph in listing.", "panel": "Filters" },
|
||||
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "table", "help": "Output: text, json, table.", "panel": "Output" }
|
||||
@@ -329,6 +338,8 @@
|
||||
],
|
||||
"options": [
|
||||
{ "name": "metadata", "flags": ["--metadata", "-m"], "type": "string", "help": "Update metadata (JSON)." },
|
||||
{ "name": "expires", "flags": ["--expires"], "type": "string", "help": "Expiration date (YYYY-MM-DD)." },
|
||||
{ "name": "timestamp", "flags": ["--timestamp"], "type": "integer", "help": "Unix timestamp for the memory." },
|
||||
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output: text, json, quiet.", "panel": "Output" }
|
||||
],
|
||||
"apiEndpoint": "update"
|
||||
@@ -356,6 +367,7 @@
|
||||
{ "name": "all", "flags": ["--all"], "type": "boolean", "default": false, "help": "Delete all memories matching scope filters." },
|
||||
{ "name": "entity", "flags": ["--entity"], "type": "boolean", "default": false, "help": "Delete the entity itself and all its memories (cascade)." },
|
||||
{ "name": "project", "flags": ["--project"], "type": "boolean", "default": false, "help": "With --all: delete ALL memories project-wide." },
|
||||
{ "name": "delete_linked", "flags": ["--delete-linked"], "type": "boolean", "default": false, "help": "Also delete memories linked to this memory." },
|
||||
{ "name": "dry_run", "flags": ["--dry-run"], "type": "boolean", "default": false, "help": "Show what would be deleted without deleting." },
|
||||
{ "name": "force", "flags": ["--force"], "type": "boolean", "default": false, "help": "Skip confirmation." },
|
||||
{ "name": "user_id", "flags": ["--user-id", "-u"], "type": "string", "help": "Scope to user.", "panel": "Scope" },
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# Node CLI (`cli/node/`)
|
||||
|
||||
The `@mem0/cli` package on npm. Commander-based, entry point `mem0`.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm run build # tsup (ESM)
|
||||
pnpm run lint # biome check src/
|
||||
pnpm run lint:fix # biome check --write src/
|
||||
pnpm run typecheck # tsc --noEmit
|
||||
pnpm run test # vitest run
|
||||
pnpm run test:watch
|
||||
pnpm run dev # tsx src/index.ts
|
||||
```
|
||||
|
||||
pnpm only. Never npm, never yarn.
|
||||
|
||||
## Conventions
|
||||
|
||||
> **Biome, not ESLint. vitest, not jest.** `mem0-ts/` uses Prettier + jest and
|
||||
> `integrations/vercel-ai-sdk/` uses ESLint + jest. Running those tools here produces
|
||||
> spurious diffs. Every toolchain in this repo is per-package.
|
||||
|
||||
- **Node 18+** required.
|
||||
- **Build:** tsup, ESM output only.
|
||||
- **Linter and formatter:** Biome, configured in `biome.json`.
|
||||
- **Tests:** vitest.
|
||||
- **TypeScript strict mode.** ES module `import` syntax only, never `require()`.
|
||||
|
||||
Run `pnpm run typecheck` after every change.
|
||||
|
||||
## Dependencies
|
||||
|
||||
Commander + Chalk + ora + cli-table3, and `mem0ai` (npm) for API calls.
|
||||
|
||||
## CI and release
|
||||
|
||||
- CI: `cli-node-ci.yml`, Biome + tsc + vitest + tsup build on Node 20 and 22.
|
||||
- Release: tag prefix `cli-node-v*` dispatches `cli-node-cd.yml`, publishing to npm over OIDC.
|
||||
@@ -1,60 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to `@mem0/cli` are documented here.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.2.9] — 2026-06-19
|
||||
|
||||
### Security
|
||||
|
||||
- Telemetry no longer passes the Mem0 API key to its child process via
|
||||
command-line arguments. The context is now sent over stdin, so the key is no
|
||||
longer visible in the process list (`ps`, `/proc/<pid>/cmdline`, Activity
|
||||
Monitor). Fixes #4862.
|
||||
|
||||
## [0.2.8] — 2026-06-01
|
||||
|
||||
### Security
|
||||
|
||||
- Pinned transitive dependencies via pnpm overrides to remediate high-severity CVEs:
|
||||
- `jws` → 4.0.1 (CVE-2025-65945)
|
||||
- `langsmith` → ^0.6.0 (CVE-2026-45134)
|
||||
- `tar-fs` → ^2.1.4 (CVE-2025-48387, CVE-2025-59343)
|
||||
- `picomatch` → ^2.3.2 (CVE-2026-33671)
|
||||
- `minimatch` → ^3.1.3 / ^5.1.8 / ^9.0.7 (CVE-2026-27903, CVE-2026-27904, CVE-2026-26996)
|
||||
- `path-to-regexp` → ^8.4.0 (CVE-2026-4926)
|
||||
- `rollup` → ^4.59.0 (CVE-2026-27606)
|
||||
- `glob` → ^10.5.0 (CVE-2025-64756)
|
||||
- `@modelcontextprotocol/sdk` → ^1.25.4 (CVE-2025-66414, CVE-2026-0621)
|
||||
|
||||
## [0.2.7] — 2026-05-20
|
||||
|
||||
### Added
|
||||
|
||||
- `mem0 whoami` — print the active agent's `default_user_id` (the AGENTRUSH
|
||||
leaderboard identifier). Reads from local config, no network call.
|
||||
- `mem0 agent-rush <add | search>` — subcommand group that wraps the new
|
||||
`/v1/agent-rush/` platform endpoints for the 7-day AGENTRUSH game. Project
|
||||
routing is implicit (resolved server-side); no flags exposed. Pretty-prints
|
||||
platform error codes into actionable hints (e.g. `agentrush_search_first`
|
||||
→ "Run 3 'mem0 agent-rush search' commands before adding.").
|
||||
- PII safety prompt on first `mem0 agent-rush add`. Interactive runs require
|
||||
explicit `y` to acknowledge that AGENTRUSH memories are public; the
|
||||
acknowledgement is persisted in `~/.mem0/config.json` under
|
||||
`agent_rush.acknowledged_at` so the prompt only appears once per machine.
|
||||
Non-interactive (agent) invocations surface the warning to stderr without
|
||||
blocking.
|
||||
- New config schema field: `agent_rush.acknowledged_at` (ISO timestamp,
|
||||
empty until first interactive acknowledgement).
|
||||
|
||||
### Changed
|
||||
|
||||
- HTTP requests from the new agent-rush commands send `X-Mem0-Mode: agent-rush`
|
||||
in addition to the existing source headers, so platform telemetry can split
|
||||
game traffic from regular CLI usage.
|
||||
|
||||
## [0.2.6] and earlier
|
||||
|
||||
Unlogged historical releases. See git history under `cli/node/`.
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
AGENTS.md
|
||||
+2
-8
@@ -232,14 +232,6 @@ Verify your API connection and display the current project.
|
||||
mem0 status
|
||||
```
|
||||
|
||||
### `mem0 version`
|
||||
|
||||
Print the CLI version.
|
||||
|
||||
```bash
|
||||
mem0 version
|
||||
```
|
||||
|
||||
## Agent mode
|
||||
|
||||
Pass `--agent` (or its alias `--json`) as a **global flag** on any command to get output designed for AI agent tool loops:
|
||||
@@ -298,6 +290,8 @@ These flags are available on all commands:
|
||||
| `--base-url` | Override the configured API base URL for this request |
|
||||
| `-o, --output` | Set the output format |
|
||||
|
||||
`mem0 --version` prints the CLI version. It is only valid before a subcommand, not after one.
|
||||
|
||||
## Environment variables
|
||||
|
||||
| Variable | Description |
|
||||
|
||||
@@ -51,7 +51,7 @@ pnpm link --global
|
||||
|
||||
# Now use it like a normal CLI
|
||||
mem0 --help
|
||||
mem0 version
|
||||
mem0 --version
|
||||
```
|
||||
|
||||
> **Warning:** If you also have the Python CLI installed, both register the `mem0` command. The last one linked/installed wins. Unlink with `pnpm unlink --global`.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mem0/cli",
|
||||
"version": "0.2.9",
|
||||
"version": "0.2.13",
|
||||
"description": "The official CLI for mem0 — the memory layer for AI agents",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
@@ -51,8 +51,8 @@
|
||||
"langsmith@<0.6.0": "^0.6.0",
|
||||
"tar-fs@>=2.0.0 <2.1.4": "^2.1.4",
|
||||
"picomatch@<2.3.2": "^2.3.2",
|
||||
"postcss@<8.5.10": ">=8.5.10",
|
||||
"esbuild": ">=0.28.1"
|
||||
"esbuild": ">=0.28.1",
|
||||
"postcss@<8.5.18": ">=8.5.18 <9.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+34
-17
@@ -9,8 +9,8 @@ overrides:
|
||||
langsmith@<0.6.0: ^0.6.0
|
||||
tar-fs@>=2.0.0 <2.1.4: ^2.1.4
|
||||
picomatch@<2.3.2: ^2.3.2
|
||||
postcss@<8.5.10: '>=8.5.10'
|
||||
esbuild: '>=0.28.1'
|
||||
postcss@<8.5.18: '>=8.5.18 <9.0.0'
|
||||
|
||||
importers:
|
||||
|
||||
@@ -40,7 +40,7 @@ importers:
|
||||
version: 20.19.37
|
||||
tsup:
|
||||
specifier: ^8.0.0
|
||||
version: 8.5.1(postcss@8.5.15)(tsx@4.21.0)(typescript@5.9.3)
|
||||
version: 8.5.1(postcss@8.5.23)(tsx@4.21.0)(typescript@5.9.3)
|
||||
tsx:
|
||||
specifier: ^4.7.0
|
||||
version: 4.21.0
|
||||
@@ -78,24 +78,28 @@ packages:
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@biomejs/cli-linux-arm64@1.9.4':
|
||||
resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@biomejs/cli-linux-x64-musl@1.9.4':
|
||||
resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@biomejs/cli-linux-x64@1.9.4':
|
||||
resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@biomejs/cli-win32-arm64@1.9.4':
|
||||
resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==}
|
||||
@@ -316,66 +320,79 @@ packages:
|
||||
resolution: {integrity: sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.60.0':
|
||||
resolution: {integrity: sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.60.0':
|
||||
resolution: {integrity: sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.60.0':
|
||||
resolution: {integrity: sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.60.0':
|
||||
resolution: {integrity: sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-loong64-musl@4.60.0':
|
||||
resolution: {integrity: sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.60.0':
|
||||
resolution: {integrity: sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-musl@4.60.0':
|
||||
resolution: {integrity: sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.60.0':
|
||||
resolution: {integrity: sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.60.0':
|
||||
resolution: {integrity: sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.60.0':
|
||||
resolution: {integrity: sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.60.0':
|
||||
resolution: {integrity: sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.60.0':
|
||||
resolution: {integrity: sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openbsd-x64@4.60.0':
|
||||
resolution: {integrity: sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==}
|
||||
@@ -653,8 +670,8 @@ packages:
|
||||
mz@2.7.0:
|
||||
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
|
||||
|
||||
nanoid@3.3.12:
|
||||
resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==}
|
||||
nanoid@3.3.16:
|
||||
resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
@@ -695,7 +712,7 @@ packages:
|
||||
engines: {node: '>= 18'}
|
||||
peerDependencies:
|
||||
jiti: '>=1.21.0'
|
||||
postcss: '>=8.5.10'
|
||||
postcss: '>=8.5.18 <9.0.0'
|
||||
tsx: ^4.8.1
|
||||
yaml: ^2.4.2
|
||||
peerDependenciesMeta:
|
||||
@@ -708,8 +725,8 @@ packages:
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
postcss@8.5.15:
|
||||
resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
|
||||
postcss@8.5.23:
|
||||
resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
readdirp@4.1.2:
|
||||
@@ -821,7 +838,7 @@ packages:
|
||||
peerDependencies:
|
||||
'@microsoft/api-extractor': ^7.36.0
|
||||
'@swc/core': ^1
|
||||
postcss: '>=8.5.10'
|
||||
postcss: '>=8.5.18 <9.0.0'
|
||||
typescript: '>=4.5.0'
|
||||
peerDependenciesMeta:
|
||||
'@microsoft/api-extractor':
|
||||
@@ -1388,7 +1405,7 @@ snapshots:
|
||||
object-assign: 4.1.1
|
||||
thenify-all: 1.6.0
|
||||
|
||||
nanoid@3.3.12: {}
|
||||
nanoid@3.3.16: {}
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
||||
@@ -1424,16 +1441,16 @@ snapshots:
|
||||
mlly: 1.8.2
|
||||
pathe: 2.0.3
|
||||
|
||||
postcss-load-config@6.0.1(postcss@8.5.15)(tsx@4.21.0):
|
||||
postcss-load-config@6.0.1(postcss@8.5.23)(tsx@4.21.0):
|
||||
dependencies:
|
||||
lilconfig: 3.1.3
|
||||
optionalDependencies:
|
||||
postcss: 8.5.15
|
||||
postcss: 8.5.23
|
||||
tsx: 4.21.0
|
||||
|
||||
postcss@8.5.15:
|
||||
postcss@8.5.23:
|
||||
dependencies:
|
||||
nanoid: 3.3.12
|
||||
nanoid: 3.3.16
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
@@ -1554,7 +1571,7 @@ snapshots:
|
||||
|
||||
ts-interface-checker@0.1.13: {}
|
||||
|
||||
tsup@8.5.1(postcss@8.5.15)(tsx@4.21.0)(typescript@5.9.3):
|
||||
tsup@8.5.1(postcss@8.5.23)(tsx@4.21.0)(typescript@5.9.3):
|
||||
dependencies:
|
||||
bundle-require: 5.1.0(esbuild@0.28.1)
|
||||
cac: 6.7.14
|
||||
@@ -1565,7 +1582,7 @@ snapshots:
|
||||
fix-dts-default-cjs-exports: 1.0.1
|
||||
joycon: 3.1.1
|
||||
picocolors: 1.1.1
|
||||
postcss-load-config: 6.0.1(postcss@8.5.15)(tsx@4.21.0)
|
||||
postcss-load-config: 6.0.1(postcss@8.5.23)(tsx@4.21.0)
|
||||
resolve-from: 5.0.0
|
||||
rollup: 4.60.0
|
||||
source-map: 0.7.6
|
||||
@@ -1574,7 +1591,7 @@ snapshots:
|
||||
tinyglobby: 0.2.15
|
||||
tree-kill: 1.2.2
|
||||
optionalDependencies:
|
||||
postcss: 8.5.15
|
||||
postcss: 8.5.23
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- jiti
|
||||
@@ -1602,7 +1619,7 @@ snapshots:
|
||||
esbuild: 0.28.1
|
||||
fdir: 6.5.0(picomatch@4.0.4)
|
||||
picomatch: 4.0.4
|
||||
postcss: 8.5.15
|
||||
postcss: 8.5.23
|
||||
rollup: 4.60.0
|
||||
tinyglobby: 0.2.15
|
||||
optionalDependencies:
|
||||
|
||||
@@ -10,5 +10,5 @@ overrides:
|
||||
langsmith@<0.6.0: ^0.6.0
|
||||
tar-fs@>=2.0.0 <2.1.4: ^2.1.4
|
||||
picomatch@<2.3.2: ^2.3.2
|
||||
"postcss@<8.5.10": ">=8.5.10"
|
||||
"esbuild": ">=0.28.1"
|
||||
"postcss@<8.5.18": ">=8.5.18 <9.0.0"
|
||||
|
||||
@@ -14,7 +14,11 @@ export interface AddOptions {
|
||||
immutable?: boolean;
|
||||
infer?: boolean;
|
||||
expires?: string;
|
||||
categories?: string[];
|
||||
customInstructions?: string;
|
||||
agentCustomInstructions?: string;
|
||||
customCategories?: Record<string, string>[];
|
||||
structuredDataSchema?: Record<string, unknown>;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
export interface SearchOptions {
|
||||
@@ -28,6 +32,9 @@ export interface SearchOptions {
|
||||
keyword?: boolean;
|
||||
filters?: Record<string, unknown>;
|
||||
fields?: string[];
|
||||
showExpired?: boolean;
|
||||
referenceDate?: string | number;
|
||||
latestOnly?: boolean;
|
||||
}
|
||||
|
||||
export interface ListOptions {
|
||||
@@ -40,6 +47,8 @@ export interface ListOptions {
|
||||
category?: string;
|
||||
after?: string;
|
||||
before?: string;
|
||||
showExpired?: boolean;
|
||||
latestOnly?: boolean;
|
||||
}
|
||||
|
||||
export interface DeleteOptions {
|
||||
@@ -48,6 +57,12 @@ export interface DeleteOptions {
|
||||
agentId?: string;
|
||||
appId?: string;
|
||||
runId?: string;
|
||||
deleteLinked?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateOptions {
|
||||
expirationDate?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
export interface EntityIds {
|
||||
@@ -77,6 +92,7 @@ export interface Backend {
|
||||
memoryId: string,
|
||||
content?: string,
|
||||
metadata?: Record<string, unknown>,
|
||||
opts?: UpdateOptions,
|
||||
): Promise<Record<string, unknown>>;
|
||||
|
||||
delete(
|
||||
|
||||
@@ -15,8 +15,13 @@ import {
|
||||
type ListOptions,
|
||||
NotFoundError,
|
||||
type SearchOptions,
|
||||
type UpdateOptions,
|
||||
} from "./base.js";
|
||||
|
||||
function encodePathSegment(value: unknown): string {
|
||||
return encodeURIComponent(String(value));
|
||||
}
|
||||
|
||||
export class PlatformBackend implements Backend {
|
||||
private baseUrl: string;
|
||||
private headers: Record<string, string>;
|
||||
@@ -146,7 +151,15 @@ export class PlatformBackend implements Backend {
|
||||
if (opts.immutable) payload.immutable = true;
|
||||
if (opts.infer === false) payload.infer = false;
|
||||
if (opts.expires) payload.expiration_date = opts.expires;
|
||||
if (opts.categories) payload.categories = opts.categories;
|
||||
if (opts.customInstructions)
|
||||
payload.custom_instructions = opts.customInstructions;
|
||||
if (opts.agentCustomInstructions)
|
||||
payload.agent_custom_instructions = opts.agentCustomInstructions;
|
||||
if (opts.customCategories)
|
||||
payload.custom_categories = opts.customCategories;
|
||||
if (opts.structuredDataSchema)
|
||||
payload.structured_data_schema = opts.structuredDataSchema;
|
||||
if (opts.timestamp !== undefined) payload.timestamp = opts.timestamp;
|
||||
payload.source = "CLI";
|
||||
|
||||
return (await this._request("POST", "/v3/memories/add/", {
|
||||
@@ -207,6 +220,10 @@ export class PlatformBackend implements Backend {
|
||||
if (opts.rerank) payload.rerank = true;
|
||||
if (opts.keyword) payload.keyword_search = true;
|
||||
if (opts.fields) payload.fields = opts.fields;
|
||||
if (opts.showExpired) payload.show_expired = true;
|
||||
if (opts.referenceDate !== undefined)
|
||||
payload.reference_date = opts.referenceDate;
|
||||
if (opts.latestOnly) payload.latest_only = true;
|
||||
payload.source = "CLI";
|
||||
|
||||
const result = (await this._request("POST", "/v3/memories/search/", {
|
||||
@@ -218,9 +235,13 @@ export class PlatformBackend implements Backend {
|
||||
}
|
||||
|
||||
async get(memoryId: string): Promise<Record<string, unknown>> {
|
||||
return (await this._request("GET", `/v1/memories/${memoryId}/`, {
|
||||
params: { source: "CLI" },
|
||||
})) as Record<string, unknown>;
|
||||
return (await this._request(
|
||||
"GET",
|
||||
`/v1/memories/${encodePathSegment(memoryId)}/`,
|
||||
{
|
||||
params: { source: "CLI" },
|
||||
},
|
||||
)) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
async listMemories(
|
||||
@@ -257,6 +278,8 @@ export class PlatformBackend implements Backend {
|
||||
extraFilters: Object.keys(extra).length > 0 ? extra : undefined,
|
||||
});
|
||||
if (apiFilters) payload.filters = apiFilters;
|
||||
if (opts.showExpired) payload.show_expired = true;
|
||||
if (opts.latestOnly) payload.latest_only = true;
|
||||
payload.source = "CLI";
|
||||
|
||||
const result = (await this._request("POST", "/v3/memories/", {
|
||||
@@ -272,14 +295,21 @@ export class PlatformBackend implements Backend {
|
||||
memoryId: string,
|
||||
content?: string,
|
||||
metadata?: Record<string, unknown>,
|
||||
opts: UpdateOptions = {},
|
||||
): Promise<Record<string, unknown>> {
|
||||
const payload: Record<string, unknown> = {};
|
||||
if (content) payload.text = content;
|
||||
if (metadata) payload.metadata = metadata;
|
||||
if (opts.expirationDate) payload.expiration_date = opts.expirationDate;
|
||||
if (opts.timestamp !== undefined) payload.timestamp = opts.timestamp;
|
||||
payload.source = "CLI";
|
||||
return (await this._request("PUT", `/v1/memories/${memoryId}/`, {
|
||||
json: payload,
|
||||
})) as Record<string, unknown>;
|
||||
return (await this._request(
|
||||
"PUT",
|
||||
`/v1/memories/${encodePathSegment(memoryId)}/`,
|
||||
{
|
||||
json: payload,
|
||||
},
|
||||
)) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
async delete(
|
||||
@@ -297,9 +327,13 @@ export class PlatformBackend implements Backend {
|
||||
})) as Record<string, unknown>;
|
||||
}
|
||||
if (memoryId) {
|
||||
return (await this._request("DELETE", `/v1/memories/${memoryId}/`, {
|
||||
params: { source: "CLI" },
|
||||
})) as Record<string, unknown>;
|
||||
const params: Record<string, string> = { source: "CLI" };
|
||||
if (opts.deleteLinked) params.delete_linked = "true";
|
||||
return (await this._request(
|
||||
"DELETE",
|
||||
`/v1/memories/${encodePathSegment(memoryId)}/`,
|
||||
{ params },
|
||||
)) as Record<string, unknown>;
|
||||
}
|
||||
throw new Error("Either memoryId or --all is required");
|
||||
}
|
||||
@@ -316,16 +350,18 @@ export class PlatformBackend implements Backend {
|
||||
if (entities.length === 0) {
|
||||
throw new Error("At least one entity ID is required for deleteEntities.");
|
||||
}
|
||||
// Delete each provided entity via the v2 path-based endpoint
|
||||
let result: Record<string, unknown> = {};
|
||||
// Delete each provided entity via the v2 path-based endpoint. Key each
|
||||
// response by entity type so a multi-entity delete (e.g. --user-id and
|
||||
// --agent-id together) doesn't discard everything but the last result.
|
||||
const results: Record<string, unknown> = {};
|
||||
for (const [entityType, entityId] of entities) {
|
||||
result = (await this._request(
|
||||
results[entityType] = (await this._request(
|
||||
"DELETE",
|
||||
`/v2/entities/${entityType}/${entityId}/`,
|
||||
`/v2/entities/${encodePathSegment(entityType)}/${encodePathSegment(entityId)}/`,
|
||||
{ params: { source: "CLI" } },
|
||||
)) as Record<string, unknown>;
|
||||
}
|
||||
return result;
|
||||
return results;
|
||||
}
|
||||
|
||||
async ping(): Promise<Record<string, unknown>> {
|
||||
@@ -384,9 +420,9 @@ export class PlatformBackend implements Backend {
|
||||
}
|
||||
|
||||
async getEvent(eventId: string): Promise<Record<string, unknown>> {
|
||||
return (await this._request("GET", `/v1/event/${eventId}/`)) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
return (await this._request(
|
||||
"GET",
|
||||
`/v1/event/${encodePathSegment(eventId)}/`,
|
||||
)) as Record<string, unknown>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,16 +21,19 @@ import {
|
||||
formatSingleMemory,
|
||||
printResultSummary,
|
||||
} from "../output.js";
|
||||
import { isAgentMode, setCurrentCommand } from "../state.js";
|
||||
import { isAgentMode, setCurrentCommand, stdinIsPiped } from "../state.js";
|
||||
|
||||
/** True only when stdin is an actual pipe or file redirect — never in agent mode. */
|
||||
function _stdinIsPiped(): boolean {
|
||||
if (isAgentMode()) return false;
|
||||
try {
|
||||
const stat = fs.fstatSync(0);
|
||||
return stat.isFIFO() || stat.isFile();
|
||||
} catch {
|
||||
return false;
|
||||
/** Exit 1 if value is not a future YYYY-MM-DD date. */
|
||||
function _validateExpires(value: string): void {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
||||
printError(
|
||||
"Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31).",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (new Date(value) <= new Date()) {
|
||||
printError("--expires date must be in the future.");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,10 +52,23 @@ export async function cmdAdd(
|
||||
infer?: boolean;
|
||||
expires?: string;
|
||||
categories?: string;
|
||||
customInstructions?: string;
|
||||
agentCustomInstructions?: string;
|
||||
customCategories?: string;
|
||||
structuredDataSchema?: string;
|
||||
timestamp?: number;
|
||||
output: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
setCurrentCommand("add");
|
||||
|
||||
if (opts.categories) {
|
||||
printError(
|
||||
"--categories is not supported on add. Use --custom-categories instead.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let msgs: Record<string, unknown>[] | undefined;
|
||||
let content = text;
|
||||
|
||||
@@ -78,7 +94,7 @@ export async function cmdAdd(
|
||||
}
|
||||
}
|
||||
// Read from stdin only if stdin is an actual pipe or file redirect
|
||||
else if (!content && _stdinIsPiped()) {
|
||||
else if (!content && stdinIsPiped()) {
|
||||
content = fs.readFileSync(0, "utf-8").trim();
|
||||
}
|
||||
|
||||
@@ -93,20 +109,6 @@ export async function cmdAdd(
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate --expires
|
||||
if (opts.expires) {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(opts.expires)) {
|
||||
printError(
|
||||
"Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31).",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (new Date(opts.expires) <= new Date()) {
|
||||
printError("--expires date must be in the future.");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
let meta: Record<string, unknown> | undefined;
|
||||
if (opts.metadata) {
|
||||
try {
|
||||
@@ -117,15 +119,28 @@ export async function cmdAdd(
|
||||
}
|
||||
}
|
||||
|
||||
let cats: string[] | undefined;
|
||||
if (opts.categories) {
|
||||
let customCats: Record<string, string>[] | undefined;
|
||||
if (opts.customCategories) {
|
||||
try {
|
||||
cats = JSON.parse(opts.categories);
|
||||
customCats = JSON.parse(opts.customCategories);
|
||||
} catch {
|
||||
cats = opts.categories.split(",").map((c) => c.trim());
|
||||
printError("Invalid JSON in --custom-categories.");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
let schema: Record<string, unknown> | undefined;
|
||||
if (opts.structuredDataSchema) {
|
||||
try {
|
||||
schema = JSON.parse(opts.structuredDataSchema);
|
||||
} catch {
|
||||
printError("Invalid JSON in --structured-data-schema.");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.expires) _validateExpires(opts.expires);
|
||||
|
||||
let result: Record<string, unknown>;
|
||||
try {
|
||||
result = await timedStatus("Adding memory...", async () => {
|
||||
@@ -138,7 +153,11 @@ export async function cmdAdd(
|
||||
immutable: opts.immutable,
|
||||
infer: opts.infer !== false,
|
||||
expires: opts.expires,
|
||||
categories: cats,
|
||||
customInstructions: opts.customInstructions,
|
||||
agentCustomInstructions: opts.agentCustomInstructions,
|
||||
customCategories: customCats,
|
||||
structuredDataSchema: schema,
|
||||
timestamp: opts.timestamp,
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -223,6 +242,9 @@ export async function cmdSearch(
|
||||
keyword: boolean;
|
||||
filterJson?: string;
|
||||
fields?: string;
|
||||
showExpired?: boolean;
|
||||
referenceDate?: string;
|
||||
latestOnly?: boolean;
|
||||
output: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
@@ -271,6 +293,9 @@ export async function cmdSearch(
|
||||
keyword: opts.keyword,
|
||||
filters,
|
||||
fields: fieldList,
|
||||
showExpired: opts.showExpired,
|
||||
referenceDate: opts.referenceDate,
|
||||
latestOnly: opts.latestOnly,
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -364,6 +389,8 @@ export async function cmdList(
|
||||
category?: string;
|
||||
after?: string;
|
||||
before?: string;
|
||||
showExpired?: boolean;
|
||||
latestOnly?: boolean;
|
||||
output: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
@@ -391,6 +418,8 @@ export async function cmdList(
|
||||
category: opts.category,
|
||||
after: opts.after,
|
||||
before: opts.before,
|
||||
showExpired: opts.showExpired,
|
||||
latestOnly: opts.latestOnly,
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -450,7 +479,12 @@ export async function cmdUpdate(
|
||||
backend: Backend,
|
||||
memoryId: string,
|
||||
text: string | undefined,
|
||||
opts: { metadata?: string; output: string },
|
||||
opts: {
|
||||
metadata?: string;
|
||||
expires?: string;
|
||||
timestamp?: number;
|
||||
output: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
setCurrentCommand("update");
|
||||
let meta: Record<string, unknown> | undefined;
|
||||
@@ -463,11 +497,16 @@ export async function cmdUpdate(
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.expires) _validateExpires(opts.expires);
|
||||
|
||||
const start = performance.now();
|
||||
let result: Record<string, unknown>;
|
||||
try {
|
||||
result = await timedStatus("Updating memory...", async () => {
|
||||
return backend.update(memoryId, text, meta);
|
||||
return backend.update(memoryId, text, meta, {
|
||||
expirationDate: opts.expires,
|
||||
timestamp: opts.timestamp,
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
printError(e instanceof Error ? e.message : String(e));
|
||||
@@ -493,7 +532,12 @@ export async function cmdUpdate(
|
||||
export async function cmdDelete(
|
||||
backend: Backend,
|
||||
memoryId: string,
|
||||
opts: { output: string; dryRun?: boolean; force?: boolean },
|
||||
opts: {
|
||||
output: string;
|
||||
dryRun?: boolean;
|
||||
force?: boolean;
|
||||
deleteLinked?: boolean;
|
||||
},
|
||||
): Promise<void> {
|
||||
setCurrentCommand("delete");
|
||||
if (opts.dryRun) {
|
||||
@@ -514,7 +558,7 @@ export async function cmdDelete(
|
||||
let result: Record<string, unknown>;
|
||||
try {
|
||||
result = await timedStatus("Deleting...", async () => {
|
||||
return backend.delete(memoryId);
|
||||
return backend.delete(memoryId, { deleteLinked: opts.deleteLinked });
|
||||
});
|
||||
} catch (e) {
|
||||
printError(e instanceof Error ? e.message : String(e));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Utility commands: status, version, import.
|
||||
* Utility commands: status, import.
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
@@ -8,7 +8,6 @@ import type { Backend } from "../backend/base.js";
|
||||
import { colors, printError, printSuccess, timedStatus } from "../branding.js";
|
||||
import { formatAgentEnvelope, formatJsonEnvelope } from "../output.js";
|
||||
import { setCurrentCommand } from "../state.js";
|
||||
import { CLI_VERSION } from "../version.js";
|
||||
|
||||
const { brand, dim, success, error: errorColor } = colors;
|
||||
|
||||
@@ -82,10 +81,6 @@ export async function cmdStatus(
|
||||
console.log();
|
||||
}
|
||||
|
||||
export function cmdVersion(): void {
|
||||
console.log(` ${brand("◆ Mem0")} CLI v${CLI_VERSION}`);
|
||||
}
|
||||
|
||||
export async function cmdImport(
|
||||
backend: Backend,
|
||||
filePath: string,
|
||||
|
||||
+10
-1
@@ -36,7 +36,16 @@ const COMMAND_GROUPS: { panel: string; commands: string[] }[] = [
|
||||
},
|
||||
{
|
||||
panel: "Management",
|
||||
commands: ["init", "status", "import", "help", "entity", "event", "config"],
|
||||
commands: [
|
||||
"init",
|
||||
"status",
|
||||
"version",
|
||||
"import",
|
||||
"help",
|
||||
"entity",
|
||||
"event",
|
||||
"config",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
+78
-8
@@ -17,6 +17,7 @@ import {
|
||||
isAgentMode,
|
||||
setAgentMode,
|
||||
setCurrentCommand,
|
||||
stdinIsPiped,
|
||||
takeNotice,
|
||||
} from "./state.js";
|
||||
import { captureEvent } from "./telemetry.js";
|
||||
@@ -94,6 +95,10 @@ async function getBackendOnly(
|
||||
return (await getBackendAndConfig(apiKey, baseUrl)).backend;
|
||||
}
|
||||
|
||||
function printVersion(): void {
|
||||
console.log(` ${colors.brand("◆ Mem0")} CLI v${CLI_VERSION}`);
|
||||
}
|
||||
|
||||
function checkAgentMode(): boolean {
|
||||
const rootOpts = program.opts();
|
||||
const isAgent = !!(rootOpts.json || rootOpts.agent);
|
||||
@@ -153,7 +158,7 @@ program
|
||||
.enablePositionalOptions()
|
||||
.option("--version", "Show version and exit.")
|
||||
.on("option:version", () => {
|
||||
console.log(` ${colors.brand("◆ Mem0")} CLI v${CLI_VERSION}`);
|
||||
printVersion();
|
||||
process.exit(0);
|
||||
})
|
||||
.option("--json", "Output as JSON for agent/programmatic use.")
|
||||
@@ -319,7 +324,29 @@ program
|
||||
.option("--immutable", "Prevent future updates.", false)
|
||||
.option("--no-infer", "Skip inference, store raw.")
|
||||
.option("--expires <date>", "Expiration date (YYYY-MM-DD).")
|
||||
.option("--categories <value>", "Categories (JSON array or comma-separated).")
|
||||
.option(
|
||||
"--categories <value>",
|
||||
"Not supported on add, use --custom-categories instead.",
|
||||
)
|
||||
.option(
|
||||
"--custom-instructions <text>",
|
||||
"Custom instructions for fact extraction.",
|
||||
)
|
||||
.option(
|
||||
"--agent-custom-instructions <text>",
|
||||
"Extraction instructions for agent-scoped memories, overriding the project setting.",
|
||||
)
|
||||
.option(
|
||||
"--custom-categories <json>",
|
||||
"Custom categories as a JSON array of {name: description} objects.",
|
||||
)
|
||||
.option(
|
||||
"--structured-data-schema <json>",
|
||||
"Schema for structured data extraction, as JSON.",
|
||||
)
|
||||
.option("--timestamp <unix>", "Unix timestamp for the memory.", (v) =>
|
||||
Number.parseInt(v),
|
||||
)
|
||||
.option("-o, --output <format>", "Output format: text, json, quiet.", "text")
|
||||
.option("--api-key <key>", "Override API key.")
|
||||
.option("--base-url <url>", "Override API base URL.")
|
||||
@@ -364,18 +391,31 @@ program
|
||||
)
|
||||
.option("--rerank", "Enable reranking (Platform only).", false)
|
||||
.option("--keyword", "Use keyword search.", false)
|
||||
.option("--filter <json>", "Advanced filter expression (JSON).")
|
||||
.option(
|
||||
"--filter <json>",
|
||||
'Advanced filter as JSON: {"AND": [...]} or {"OR": [...]}, e.g. {"AND": [{"categories": {"in": ["work"]}}]}.',
|
||||
)
|
||||
.option("--fields <list>", "Specific fields to return (comma-separated).")
|
||||
.option("--show-expired", "Include expired memories.", false)
|
||||
.option(
|
||||
"--reference-date <date>",
|
||||
"Reference date for relative queries (YYYY-MM-DD or unix timestamp).",
|
||||
)
|
||||
.option(
|
||||
"--latest-only",
|
||||
"Only return the latest version of each memory.",
|
||||
false,
|
||||
)
|
||||
.option("-o, --output <format>", "Output: text, json, table.", "text")
|
||||
.option("--api-key <key>", "Override API key.")
|
||||
.option("--base-url <url>", "Override API base URL.")
|
||||
.addHelpText(
|
||||
"after",
|
||||
'\nExamples:\n $ mem0 search "preferences" --user-id alice\n $ mem0 search "tools" -u alice -o json -k 5\n $ echo "preferences" | mem0 search -u alice',
|
||||
'\nExamples:\n $ mem0 search "preferences" --user-id alice\n $ mem0 search "tools" -u alice -o json -k 5\n $ echo "preferences" | mem0 search -u alice\n $ mem0 search "invoices" -u alice --filter \'{"AND": [{"categories": {"in": ["work"]}}]}\'',
|
||||
)
|
||||
.action(async (query, opts) => {
|
||||
let resolvedQuery = query;
|
||||
if (!resolvedQuery && !process.stdin.isTTY) {
|
||||
if (!resolvedQuery && stdinIsPiped()) {
|
||||
resolvedQuery = fs.readFileSync(0, "utf-8").trim();
|
||||
}
|
||||
if (!resolvedQuery) {
|
||||
@@ -398,6 +438,9 @@ program
|
||||
keyword: opts.keyword,
|
||||
filterJson: opts.filter,
|
||||
fields: opts.fields,
|
||||
showExpired: opts.showExpired,
|
||||
referenceDate: opts.referenceDate,
|
||||
latestOnly: opts.latestOnly,
|
||||
output,
|
||||
});
|
||||
});
|
||||
@@ -441,6 +484,12 @@ program
|
||||
.option("--category <name>", "Filter by category.")
|
||||
.option("--after <date>", "Created after (YYYY-MM-DD).")
|
||||
.option("--before <date>", "Created before (YYYY-MM-DD).")
|
||||
.option("--show-expired", "Include expired memories.", false)
|
||||
.option(
|
||||
"--latest-only",
|
||||
"Only return the latest version of each memory.",
|
||||
false,
|
||||
)
|
||||
.option("-o, --output <format>", "Output: text, json, table.", "table")
|
||||
.option("--api-key <key>", "Override API key.")
|
||||
.option("--base-url <url>", "Override API base URL.")
|
||||
@@ -464,6 +513,8 @@ program
|
||||
category: opts.category,
|
||||
after: opts.after,
|
||||
before: opts.before,
|
||||
showExpired: opts.showExpired,
|
||||
latestOnly: opts.latestOnly,
|
||||
output,
|
||||
});
|
||||
});
|
||||
@@ -474,6 +525,10 @@ program
|
||||
.command("update <memoryId> [text]")
|
||||
.description("Update a memory's text or metadata.")
|
||||
.option("-m, --metadata <json>", "Update metadata (JSON).")
|
||||
.option("--expires <date>", "Expiration date (YYYY-MM-DD).")
|
||||
.option("--timestamp <unix>", "Unix timestamp for the memory.", (v) =>
|
||||
Number.parseInt(v),
|
||||
)
|
||||
.option("-o, --output <format>", "Output: text, json, quiet.", "text")
|
||||
.option("--api-key <key>", "Override API key.")
|
||||
.option("--base-url <url>", "Override API base URL.")
|
||||
@@ -483,7 +538,7 @@ program
|
||||
)
|
||||
.action(async (memoryId, text, opts) => {
|
||||
let resolvedText = text;
|
||||
if (!resolvedText && !opts.metadata && !process.stdin.isTTY) {
|
||||
if (!resolvedText && stdinIsPiped()) {
|
||||
resolvedText = fs.readFileSync(0, "utf-8").trim();
|
||||
}
|
||||
const { cmdUpdate } = await import("./commands/memory.js");
|
||||
@@ -492,6 +547,8 @@ program
|
||||
const output = isAgent ? "agent" : opts.output;
|
||||
await cmdUpdate(backend, memoryId, resolvedText, {
|
||||
metadata: opts.metadata,
|
||||
expires: opts.expires,
|
||||
timestamp: opts.timestamp,
|
||||
output,
|
||||
});
|
||||
});
|
||||
@@ -510,6 +567,11 @@ program
|
||||
.option("--project", "With --all: delete ALL memories project-wide.", false)
|
||||
.option("--dry-run", "Show what would be deleted without deleting.", false)
|
||||
.option("--force", "Skip confirmation.", false)
|
||||
.option(
|
||||
"--delete-linked",
|
||||
"Also delete memories linked to this memory.",
|
||||
false,
|
||||
)
|
||||
.option("-u, --user-id <id>", "Scope to user.")
|
||||
.option("--agent-id <id>", "Scope to agent.")
|
||||
.option("--app-id <id>", "Scope to app.")
|
||||
@@ -563,6 +625,7 @@ program
|
||||
output,
|
||||
dryRun: opts.dryRun,
|
||||
force: opts.force,
|
||||
deleteLinked: opts.deleteLinked,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -767,6 +830,13 @@ program
|
||||
});
|
||||
});
|
||||
|
||||
program
|
||||
.command("version")
|
||||
.description("Show version and exit.")
|
||||
.action(() => {
|
||||
printVersion();
|
||||
});
|
||||
|
||||
program
|
||||
.command("import <filePath>")
|
||||
.description("Import memories from a JSON file.")
|
||||
@@ -806,8 +876,8 @@ program
|
||||
.addHelpText("after", "\nExamples:\n $ mem0 help\n $ mem0 help --json")
|
||||
.action((opts) => {
|
||||
// opts.json is set when `mem0 help --json` is used (subcommand flag).
|
||||
// program.opts().json is set when the root --json global flag was used first.
|
||||
if (opts.json || program.opts().json) {
|
||||
// program.opts().json/.agent is set when a root global flag was used first.
|
||||
if (opts.json || program.opts().json || program.opts().agent) {
|
||||
// Load spec from parent directory
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const specPath = path.join(__dirname, "..", "..", "cli-spec.json");
|
||||
|
||||
+17
-3
@@ -282,11 +282,24 @@ export function sanitizeAgentData(command: string, data: unknown): unknown {
|
||||
}
|
||||
case "search":
|
||||
return (data as Record<string, unknown>[]).map((r) =>
|
||||
pick(r, ["id", "memory", "score", "created_at", "categories"]),
|
||||
pick(r, [
|
||||
"id",
|
||||
"memory",
|
||||
"score",
|
||||
"created_at",
|
||||
"categories",
|
||||
"expiration_date",
|
||||
]),
|
||||
);
|
||||
case "list":
|
||||
return (data as Record<string, unknown>[]).map((r) =>
|
||||
pick(r, ["id", "memory", "created_at", "categories"]),
|
||||
pick(r, [
|
||||
"id",
|
||||
"memory",
|
||||
"created_at",
|
||||
"categories",
|
||||
"expiration_date",
|
||||
]),
|
||||
);
|
||||
case "get": {
|
||||
const r = data as Record<string, unknown>;
|
||||
@@ -297,11 +310,12 @@ export function sanitizeAgentData(command: string, data: unknown): unknown {
|
||||
"updated_at",
|
||||
"categories",
|
||||
"metadata",
|
||||
"expiration_date",
|
||||
]);
|
||||
}
|
||||
case "update": {
|
||||
const r = data as Record<string, unknown>;
|
||||
return pick(r, ["id", "memory"]);
|
||||
return pick(r, ["id", "memory", "expiration_date"]);
|
||||
}
|
||||
case "delete":
|
||||
case "delete-all":
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* read by commands and branding functions.
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
|
||||
let _agentMode = false;
|
||||
let _currentCommand = "";
|
||||
let _pendingNotice = "";
|
||||
@@ -38,3 +40,14 @@ export function takeNotice(): string {
|
||||
_pendingNotice = "";
|
||||
return msg;
|
||||
}
|
||||
|
||||
/** True only when stdin is an actual pipe or file redirect (never in agent mode). */
|
||||
export function stdinIsPiped(): boolean {
|
||||
if (isAgentMode()) return false;
|
||||
try {
|
||||
const stat = fs.fstatSync(0);
|
||||
return stat.isFIFO() || stat.isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,15 +44,31 @@ describe("CLI Integration — help and version", () => {
|
||||
expect(result.stdout).toContain("search");
|
||||
});
|
||||
|
||||
it("help --json produces valid JSON", () => {
|
||||
const result = run(["help", "--json"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
// spec may have cli.name or top-level name
|
||||
const name = parsed.name ?? parsed.cli?.name;
|
||||
expect(name).toBe("mem0");
|
||||
it("prints the version with --version", () => {
|
||||
const flag = run(["--version"]);
|
||||
expect(flag.exitCode).toBe(0);
|
||||
expect(flag.stdout).toContain("Mem0");
|
||||
});
|
||||
|
||||
it("version subcommand output matches --version output byte-for-byte", () => {
|
||||
const flag = run(["--version"]);
|
||||
const cmd = run(["version"]);
|
||||
expect(cmd.exitCode).toBe(0);
|
||||
expect(cmd.stdout).toBe(flag.stdout);
|
||||
});
|
||||
|
||||
it.each([["help", "--json"], ["--json", "help"], ["--agent", "help"]])(
|
||||
"%s %s produces valid JSON",
|
||||
(...args) => {
|
||||
const result = run(args);
|
||||
expect(result.exitCode).toBe(0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
// spec may have cli.name or top-level name
|
||||
const name = parsed.name ?? parsed.cli?.name;
|
||||
expect(name).toBe("mem0");
|
||||
},
|
||||
);
|
||||
|
||||
it("shows add help", () => {
|
||||
const result = run(["add", "--help"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
@@ -119,6 +135,13 @@ describe("CLI Integration — help and version", () => {
|
||||
expect(result.stdout).toContain("--rerank");
|
||||
});
|
||||
|
||||
it("search help documents the --filter JSON shape with an example", () => {
|
||||
const result = run(["search", "--help"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("AND");
|
||||
expect(result.stdout).toContain("categories");
|
||||
});
|
||||
|
||||
it("list help has --category flag", () => {
|
||||
const result = run(["list", "--help"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
|
||||
+580
-373
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Drift test: every documented v3 add/search/list param must be reachable from the Node CLI.
|
||||
*/
|
||||
|
||||
import { execSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const OPENAPI_PATH = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"docs",
|
||||
"openapi.json",
|
||||
);
|
||||
|
||||
const KNOWN_UNSURFACED: Record<string, Record<string, string>> = {
|
||||
"/v3/memories/add/": {
|
||||
includes: "extraction hint, no CLI flag yet",
|
||||
excludes: "extraction hint, no CLI flag yet",
|
||||
enable_graph: "graph memory toggle, no CLI flag yet",
|
||||
output_format: "response envelope is pinned by the CLI",
|
||||
prompt_profile_id: "no CLI flag yet",
|
||||
temporal_reasoning: "no CLI flag yet",
|
||||
timezone: "no CLI flag yet",
|
||||
observation_datetime: "no CLI flag yet, --timestamp backdates instead",
|
||||
observation_date: "no CLI flag yet, --timestamp backdates instead",
|
||||
},
|
||||
"/v3/memories/search/": {
|
||||
categories: "expressible through --filter",
|
||||
metadata: "expressible through --filter",
|
||||
},
|
||||
"/v3/memories/": {
|
||||
start_date: "covered by --after via filters.created_at.gte",
|
||||
end_date: "covered by --before via filters.created_at.lte",
|
||||
categories: "covered by --category via filters.categories",
|
||||
fields: "no CLI flag yet",
|
||||
keywords: "no CLI flag yet",
|
||||
},
|
||||
};
|
||||
|
||||
const ADD_MAPPING: Record<string, string[]> = {
|
||||
messages: ["--messages", "--file", "text"],
|
||||
user_id: ["--user-id"],
|
||||
agent_id: ["--agent-id"],
|
||||
app_id: ["--app-id"],
|
||||
run_id: ["--run-id"],
|
||||
metadata: ["--metadata"],
|
||||
expiration_date: ["--expires"],
|
||||
custom_instructions: ["--custom-instructions"],
|
||||
agent_custom_instructions: ["--agent-custom-instructions"],
|
||||
custom_categories: ["--custom-categories"],
|
||||
infer: ["--no-infer"],
|
||||
immutable: ["--immutable"],
|
||||
structured_data_schema: ["--structured-data-schema"],
|
||||
timestamp: ["--timestamp"],
|
||||
};
|
||||
|
||||
const SEARCH_MAPPING: Record<string, string[]> = {
|
||||
query: ["query"],
|
||||
filters: ["--filter", "--user-id", "--agent-id", "--run-id"],
|
||||
show_expired: ["--show-expired"],
|
||||
top_k: ["--top-k"],
|
||||
threshold: ["--threshold"],
|
||||
rerank: ["--rerank"],
|
||||
reference_date: ["--reference-date"],
|
||||
fields: ["--fields"],
|
||||
};
|
||||
|
||||
const LIST_MAPPING: Record<string, string[]> = {
|
||||
filters: [
|
||||
"--user-id",
|
||||
"--agent-id",
|
||||
"--run-id",
|
||||
"--category",
|
||||
"--after",
|
||||
"--before",
|
||||
],
|
||||
show_expired: ["--show-expired"],
|
||||
page: ["--page"],
|
||||
page_size: ["--page-size"],
|
||||
};
|
||||
|
||||
function documentedFields(endpoint: string): string[] {
|
||||
const spec = JSON.parse(fs.readFileSync(OPENAPI_PATH, "utf-8"));
|
||||
const schema =
|
||||
spec.paths[endpoint].post.requestBody.content["application/json"].schema;
|
||||
return Object.keys(schema.properties);
|
||||
}
|
||||
|
||||
function helpText(command: string): string {
|
||||
return execSync(`npx tsx src/index.ts ${command} --help`, {
|
||||
cwd: path.join(__dirname, ".."),
|
||||
encoding: "utf-8",
|
||||
timeout: 15000,
|
||||
});
|
||||
}
|
||||
|
||||
function assertAllReachable(
|
||||
endpoint: string,
|
||||
mapping: Record<string, string[]>,
|
||||
command: string,
|
||||
) {
|
||||
const documented = documentedFields(endpoint);
|
||||
const help = helpText(command);
|
||||
for (const field of documented) {
|
||||
if (KNOWN_UNSURFACED[endpoint]?.[field]) continue;
|
||||
const candidates = mapping[field];
|
||||
expect(
|
||||
candidates,
|
||||
`${endpoint}: documented field "${field}" has no mapping entry for command "${command}"`,
|
||||
).toBeDefined();
|
||||
const reachable = candidates.some((flag) =>
|
||||
flag.startsWith("--") ? help.includes(flag) : true,
|
||||
);
|
||||
expect(
|
||||
reachable,
|
||||
`${endpoint}: documented field "${field}" not reachable via any of ${JSON.stringify(candidates)} on command "${command}"`,
|
||||
).toBe(true);
|
||||
}
|
||||
}
|
||||
|
||||
describe("Option parity: Node CLI reachability of documented v3 params", () => {
|
||||
it("add covers documented fields", () => {
|
||||
assertAllReachable("/v3/memories/add/", ADD_MAPPING, "add");
|
||||
});
|
||||
|
||||
it("search covers documented fields", () => {
|
||||
assertAllReachable("/v3/memories/search/", SEARCH_MAPPING, "search");
|
||||
});
|
||||
|
||||
it("list covers documented fields", () => {
|
||||
assertAllReachable("/v3/memories/", LIST_MAPPING, "list");
|
||||
});
|
||||
});
|
||||
|
||||
describe("stdin fallback uses the shared piped-stdin guard", () => {
|
||||
const SOURCES = ["src/index.ts", "src/commands/memory.ts"];
|
||||
|
||||
for (const rel of SOURCES) {
|
||||
it(`${rel} never checks process.stdin.isTTY directly`, () => {
|
||||
const src = fs.readFileSync(path.join(__dirname, "..", rel), "utf-8");
|
||||
expect(
|
||||
src.includes("process.stdin.isTTY"),
|
||||
`${rel}: use stdinIsPiped() from state.ts. A bare !isTTY check is also true for /dev/null and sockets, so readFileSync(0) crashes with EAGAIN in scripts, CI, and agent mode.`,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it(`${rel} guards every readFileSync(0) with stdinIsPiped()`, () => {
|
||||
const src = fs.readFileSync(path.join(__dirname, "..", rel), "utf-8");
|
||||
const lines = src.split("\n");
|
||||
for (const [i, line] of lines.entries()) {
|
||||
if (!line.includes("readFileSync(0")) continue;
|
||||
const guard = lines.slice(Math.max(0, i - 3), i).join("\n");
|
||||
expect(
|
||||
guard.includes("stdinIsPiped()"),
|
||||
`${rel}:${i + 1}: readFileSync(0) must be guarded by stdinIsPiped()`,
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* Tests for the Platform backend (mem0 Platform API client).
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { PlatformBackend } from "../src/backend/platform.js";
|
||||
import { createDefaultConfig } from "../src/config.js";
|
||||
|
||||
function makeBackend(): PlatformBackend {
|
||||
return new PlatformBackend(createDefaultConfig().platform);
|
||||
}
|
||||
|
||||
function mockFetch() {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
headers: { get: vi.fn().mockReturnValue(null) },
|
||||
json: vi.fn().mockResolvedValue({ message: "ok" }),
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
return fetchMock;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe("deleteEntities", () => {
|
||||
it("returns all results keyed by entity type for a multi-entity delete", async () => {
|
||||
const backend = makeBackend();
|
||||
const responses: Record<string, unknown> = {
|
||||
"/v2/entities/user/alice/": { message: "user deleted" },
|
||||
"/v2/entities/agent/bob/": { message: "agent deleted" },
|
||||
};
|
||||
const spy = vi
|
||||
// biome-ignore lint/suspicious/noExplicitAny: spying on a private method
|
||||
.spyOn(backend as any, "_request")
|
||||
.mockImplementation(
|
||||
async (_method: string, path: string) => responses[path],
|
||||
);
|
||||
|
||||
const result = await backend.deleteEntities({
|
||||
userId: "alice",
|
||||
agentId: "bob",
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
user: { message: "user deleted" },
|
||||
agent: { message: "agent deleted" },
|
||||
});
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("keys a single-entity delete by its type", async () => {
|
||||
const backend = makeBackend();
|
||||
// biome-ignore lint/suspicious/noExplicitAny: spying on a private method
|
||||
vi.spyOn(backend as any, "_request").mockResolvedValue({
|
||||
message: "user deleted",
|
||||
});
|
||||
|
||||
const result = await backend.deleteEntities({ userId: "alice" });
|
||||
expect(result).toEqual({ user: { message: "user deleted" } });
|
||||
});
|
||||
|
||||
it("throws when no entity id is provided", async () => {
|
||||
const backend = makeBackend();
|
||||
await expect(backend.deleteEntities({})).rejects.toThrow(
|
||||
"At least one entity ID is required",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PlatformBackend option-parity payloads (MEM-5893)", () => {
|
||||
it("add: custom_instructions, custom_categories, structured_data_schema, timestamp reach the payload alongside existing fields", async () => {
|
||||
const backend = makeBackend();
|
||||
const spy = vi
|
||||
// biome-ignore lint/suspicious/noExplicitAny: spying on a private method
|
||||
.spyOn(backend as any, "_request")
|
||||
.mockResolvedValue({ results: [] });
|
||||
|
||||
await backend.add("hello", undefined, {
|
||||
userId: "alice",
|
||||
metadata: { source: "test" },
|
||||
expires: "2099-01-01",
|
||||
customInstructions: "Extract only preferences.",
|
||||
agentCustomInstructions: "Extract only tool outcomes.",
|
||||
customCategories: [{ prefs: "user preferences" }],
|
||||
structuredDataSchema: { type: "object" },
|
||||
timestamp: 1700000000,
|
||||
});
|
||||
|
||||
const payload = spy.mock.calls[0][2].json;
|
||||
expect(payload.custom_instructions).toBe("Extract only preferences.");
|
||||
expect(payload.agent_custom_instructions).toBe(
|
||||
"Extract only tool outcomes.",
|
||||
);
|
||||
expect(payload.custom_categories).toEqual([{ prefs: "user preferences" }]);
|
||||
expect(payload.structured_data_schema).toEqual({ type: "object" });
|
||||
expect(payload.timestamp).toBe(1700000000);
|
||||
expect(payload.metadata).toEqual({ source: "test" });
|
||||
expect(payload.expiration_date).toBe("2099-01-01");
|
||||
});
|
||||
|
||||
it("add: omitted optional fields are absent from the payload", async () => {
|
||||
const backend = makeBackend();
|
||||
const spy = vi
|
||||
// biome-ignore lint/suspicious/noExplicitAny: spying on a private method
|
||||
.spyOn(backend as any, "_request")
|
||||
.mockResolvedValue({ results: [] });
|
||||
|
||||
await backend.add("hello", undefined, { userId: "alice" });
|
||||
|
||||
const payload = spy.mock.calls[0][2].json;
|
||||
expect(payload).not.toHaveProperty("custom_instructions");
|
||||
expect(payload).not.toHaveProperty("agent_custom_instructions");
|
||||
expect(payload).not.toHaveProperty("custom_categories");
|
||||
expect(payload).not.toHaveProperty("structured_data_schema");
|
||||
expect(payload).not.toHaveProperty("timestamp");
|
||||
});
|
||||
|
||||
it("search: show_expired, reference_date, latest_only reach the payload", async () => {
|
||||
const backend = makeBackend();
|
||||
// biome-ignore lint/suspicious/noExplicitAny: spying on a private method
|
||||
const spy = vi.spyOn(backend as any, "_request").mockResolvedValue([]);
|
||||
|
||||
await backend.search("query", {
|
||||
showExpired: true,
|
||||
referenceDate: "2024-01-01",
|
||||
latestOnly: true,
|
||||
});
|
||||
|
||||
const payload = spy.mock.calls[0][2].json;
|
||||
expect(payload.show_expired).toBe(true);
|
||||
expect(payload.reference_date).toBe("2024-01-01");
|
||||
expect(payload.latest_only).toBe(true);
|
||||
});
|
||||
|
||||
it("search: keyword_search and fields reach the payload", async () => {
|
||||
const backend = makeBackend();
|
||||
// biome-ignore lint/suspicious/noExplicitAny: spying on a private method
|
||||
const spy = vi.spyOn(backend as any, "_request").mockResolvedValue([]);
|
||||
|
||||
await backend.search("query", {
|
||||
keyword: true,
|
||||
fields: ["memory", "score"],
|
||||
});
|
||||
|
||||
const payload = spy.mock.calls[0][2].json;
|
||||
expect(payload.keyword_search).toBe(true);
|
||||
expect(payload.fields).toEqual(["memory", "score"]);
|
||||
});
|
||||
|
||||
it("search: omitted keyword and fields are absent from the payload", async () => {
|
||||
const backend = makeBackend();
|
||||
// biome-ignore lint/suspicious/noExplicitAny: spying on a private method
|
||||
const spy = vi.spyOn(backend as any, "_request").mockResolvedValue([]);
|
||||
|
||||
await backend.search("query", {});
|
||||
|
||||
const payload = spy.mock.calls[0][2].json;
|
||||
expect(payload).not.toHaveProperty("keyword_search");
|
||||
expect(payload).not.toHaveProperty("fields");
|
||||
});
|
||||
|
||||
it("listMemories: show_expired and latest_only are top-level, not nested inside filters", async () => {
|
||||
const backend = makeBackend();
|
||||
// biome-ignore lint/suspicious/noExplicitAny: spying on a private method
|
||||
const spy = vi.spyOn(backend as any, "_request").mockResolvedValue([]);
|
||||
|
||||
await backend.listMemories({
|
||||
userId: "alice",
|
||||
showExpired: true,
|
||||
latestOnly: true,
|
||||
});
|
||||
|
||||
const payload = spy.mock.calls[0][2].json;
|
||||
expect(payload.show_expired).toBe(true);
|
||||
expect(payload.latest_only).toBe(true);
|
||||
expect(payload.filters ?? {}).not.toHaveProperty("show_expired");
|
||||
expect(payload.filters ?? {}).not.toHaveProperty("latest_only");
|
||||
});
|
||||
|
||||
it("update: expiration_date and timestamp reach the payload", async () => {
|
||||
const backend = makeBackend();
|
||||
// biome-ignore lint/suspicious/noExplicitAny: spying on a private method
|
||||
const spy = vi.spyOn(backend as any, "_request").mockResolvedValue({});
|
||||
|
||||
await backend.update("mem-123", undefined, undefined, {
|
||||
expirationDate: "2099-01-01",
|
||||
timestamp: 1700000000,
|
||||
});
|
||||
|
||||
const payload = spy.mock.calls[0][2].json;
|
||||
expect(payload.expiration_date).toBe("2099-01-01");
|
||||
expect(payload.timestamp).toBe(1700000000);
|
||||
});
|
||||
|
||||
it("delete: delete_linked is a query param, not part of the JSON body", async () => {
|
||||
const backend = makeBackend();
|
||||
// biome-ignore lint/suspicious/noExplicitAny: spying on a private method
|
||||
const spy = vi.spyOn(backend as any, "_request").mockResolvedValue({});
|
||||
|
||||
await backend.delete("mem-123", { deleteLinked: true });
|
||||
|
||||
const opts = spy.mock.calls[0][2];
|
||||
expect(opts.params.delete_linked).toBe("true");
|
||||
expect(opts.json).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("PlatformBackend path encoding", () => {
|
||||
it("encodes memory IDs before interpolating them into paths", async () => {
|
||||
const fetchMock = mockFetch();
|
||||
const backend = makeBackend();
|
||||
|
||||
await backend.get("mem/a?b#c");
|
||||
await backend.update("mem/a?b#c", "updated");
|
||||
await backend.delete("mem/a?b#c");
|
||||
|
||||
const urls = fetchMock.mock.calls.map((call) => call[0]);
|
||||
expect(urls).toEqual([
|
||||
"https://api.mem0.ai/v1/memories/mem%2Fa%3Fb%23c/?source=CLI",
|
||||
"https://api.mem0.ai/v1/memories/mem%2Fa%3Fb%23c/",
|
||||
"https://api.mem0.ai/v1/memories/mem%2Fa%3Fb%23c/?source=CLI",
|
||||
]);
|
||||
});
|
||||
|
||||
it("encodes entity and event IDs before interpolating them into paths", async () => {
|
||||
const fetchMock = mockFetch();
|
||||
const backend = makeBackend();
|
||||
|
||||
await backend.deleteEntities({ userId: "org/team?active#frag" });
|
||||
await backend.getEvent("evt/a?b#c");
|
||||
|
||||
const urls = fetchMock.mock.calls.map((call) => call[0]);
|
||||
expect(urls).toEqual([
|
||||
"https://api.mem0.ai/v2/entities/user/org%2Fteam%3Factive%23frag/?source=CLI",
|
||||
"https://api.mem0.ai/v1/event/evt%2Fa%3Fb%23c/",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
# Python CLI (`cli/python/`)
|
||||
|
||||
The `mem0-cli` package on PyPI. Typer-based, entry point `mem0`.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
pip install -e ".[dev]" # dev install: ruff + pytest
|
||||
ruff check . # lint
|
||||
ruff format . # format
|
||||
pytest # test
|
||||
hatch build # build
|
||||
```
|
||||
|
||||
## Conventions
|
||||
|
||||
> **Line length is 100 here, not 120.** The root Python SDK uses 120. Running the root
|
||||
> `make format` over this directory reformats every file and fails CI. Use the local
|
||||
> `ruff` invocations above.
|
||||
|
||||
- **Python 3.10+.** Not 3.9, unlike the root SDK.
|
||||
- **Ruff** with an extended rule set: `E`, `F`, `I`, `W`, `UP`, `B`, `SIM`, `RUF`.
|
||||
Ignores `E501` (formatter handles it), `B008` (required by Typer's argument defaults),
|
||||
and `SIM108`.
|
||||
- **Ruff format:** double quotes, space indent, `docstring-code-format = true`.
|
||||
- **isort** first-party is `mem0_cli` only.
|
||||
- **pytest** for tests.
|
||||
- Target version pinned to `py310`.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
cli/python/
|
||||
├── src/mem0_cli/ package source (src layout)
|
||||
└── tests/
|
||||
```
|
||||
|
||||
Entry point: `mem0 = "mem0_cli.app:main"`.
|
||||
|
||||
## Dependencies
|
||||
|
||||
Typer + Rich + httpx. `mem0ai` is **optional**, exposed through the `[oss]` extra for OSS mode. Do not promote it to a required dependency.
|
||||
|
||||
## CI and release
|
||||
|
||||
- CI: `cli-python-ci.yml`, ruff + pytest + `hatch build` on Python 3.10, 3.11, 3.12.
|
||||
- Release: tag prefix `cli-v*` dispatches `cli-python-cd.yml`, publishing to PyPI over OIDC.
|
||||
@@ -1,49 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to `mem0-cli` (Python) are documented here.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.2.8] — 2026-06-19
|
||||
|
||||
### Security
|
||||
|
||||
- Telemetry no longer passes the Mem0 API key to its child process via
|
||||
command-line arguments. The context is now sent over stdin, so the key is no
|
||||
longer visible in the process list (`ps`, `/proc/<pid>/cmdline`, Activity
|
||||
Monitor). Fixes #4862.
|
||||
|
||||
### Fixed
|
||||
|
||||
- `__version__` now matches the packaged version (was stale at 0.2.4).
|
||||
|
||||
## [0.2.7] — 2026-05-20
|
||||
|
||||
### Added
|
||||
|
||||
- `mem0 whoami` — print the active agent's `default_user_id` (the AGENTRUSH
|
||||
leaderboard identifier). Reads from local config, no network call.
|
||||
- `mem0 agent-rush <add | search>` — subcommand group that wraps the new
|
||||
`/v1/agent-rush/` platform endpoints for the 7-day AGENTRUSH game. Project
|
||||
routing is implicit (resolved server-side); no flags exposed. Pretty-prints
|
||||
platform error codes into actionable hints (e.g. `agentrush_search_first`
|
||||
→ "Run 3 'mem0 agent-rush search' commands before adding.").
|
||||
- PII safety prompt on first `mem0 agent-rush add`. Interactive runs require
|
||||
explicit `y` to acknowledge that AGENTRUSH memories are public; the
|
||||
acknowledgement is persisted in `~/.mem0/config.json` under
|
||||
`agent_rush.acknowledged_at` so the prompt only appears once per machine.
|
||||
Non-interactive (agent) invocations surface the warning to stderr without
|
||||
blocking.
|
||||
- New config schema field: `agent_rush.acknowledged_at` (ISO timestamp,
|
||||
empty until first interactive acknowledgement).
|
||||
|
||||
### Changed
|
||||
|
||||
- HTTP requests from the new agent-rush commands send `X-Mem0-Mode: agent-rush`
|
||||
in addition to the existing source headers, so platform telemetry can split
|
||||
game traffic from regular CLI usage.
|
||||
|
||||
## [0.2.6] and earlier
|
||||
|
||||
Unlogged historical releases. See git history under `cli/python/`.
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
AGENTS.md
|
||||
@@ -241,14 +241,6 @@ Verify your API connection and display the current project.
|
||||
mem0 status
|
||||
```
|
||||
|
||||
### `mem0 version`
|
||||
|
||||
Print the CLI version.
|
||||
|
||||
```bash
|
||||
mem0 version
|
||||
```
|
||||
|
||||
## Agent mode
|
||||
|
||||
Pass `--agent` (or its alias `--json`) as a **global flag** on any command to get output designed for AI agent tool loops:
|
||||
@@ -307,6 +299,8 @@ These flags are available on all commands:
|
||||
| `--base-url` | Override the configured API base URL for this request |
|
||||
| `-o, --output` | Set the output format |
|
||||
|
||||
`mem0 --version` prints the CLI version. It is only valid before a subcommand, not after one.
|
||||
|
||||
## Environment variables
|
||||
|
||||
| Variable | Description |
|
||||
|
||||
@@ -80,7 +80,7 @@ mem0 --help
|
||||
# Using Python directly (with venv activated)
|
||||
source .venv/bin/activate
|
||||
mem0 --help
|
||||
mem0 version
|
||||
mem0 --version
|
||||
|
||||
# Or run without activating
|
||||
.venv/bin/mem0 --help
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "mem0-cli"
|
||||
version = "0.2.8"
|
||||
version = "0.2.12"
|
||||
description = "The official CLI for mem0 — the memory layer for AI agents"
|
||||
readme = "README.md"
|
||||
license = "Apache-2.0"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""mem0 CLI — the command-line interface for the mem0 memory layer."""
|
||||
|
||||
__version__ = "0.2.8"
|
||||
__version__ = "0.2.12"
|
||||
|
||||
@@ -273,7 +273,26 @@ def add(
|
||||
no_infer: bool = typer.Option(False, "--no-infer", help="Skip inference, store raw."),
|
||||
expires: str | None = typer.Option(None, "--expires", help="Expiration date (YYYY-MM-DD)."),
|
||||
categories: str | None = typer.Option(
|
||||
None, "--categories", help="Categories (JSON array or comma-separated)."
|
||||
None, "--categories", help="Not supported on add, use --custom-categories instead."
|
||||
),
|
||||
custom_instructions: str | None = typer.Option(
|
||||
None, "--custom-instructions", help="Custom instructions for fact extraction."
|
||||
),
|
||||
agent_custom_instructions: str | None = typer.Option(
|
||||
None,
|
||||
"--agent-custom-instructions",
|
||||
help="Extraction instructions for agent-scoped memories, overriding the project setting.",
|
||||
),
|
||||
custom_categories: str | None = typer.Option(
|
||||
None,
|
||||
"--custom-categories",
|
||||
help="Custom categories as a JSON array of {name: description} objects.",
|
||||
),
|
||||
structured_data_schema: str | None = typer.Option(
|
||||
None, "--structured-data-schema", help="Schema for structured data extraction, as JSON."
|
||||
),
|
||||
timestamp: int | None = typer.Option(
|
||||
None, "--timestamp", help="Unix timestamp for the memory."
|
||||
),
|
||||
output: str = typer.Option(
|
||||
"text", "--output", "-o", help="Output format: text, json, quiet.", rich_help_panel="Output"
|
||||
@@ -312,6 +331,11 @@ def add(
|
||||
no_infer=no_infer,
|
||||
expires=expires,
|
||||
categories=categories,
|
||||
custom_instructions=custom_instructions,
|
||||
agent_custom_instructions=agent_custom_instructions,
|
||||
custom_categories=custom_categories,
|
||||
structured_data_schema=structured_data_schema,
|
||||
timestamp=timestamp,
|
||||
output=output,
|
||||
)
|
||||
|
||||
@@ -347,7 +371,11 @@ def search(
|
||||
False, "--keyword", help="Use keyword search.", rich_help_panel="Search"
|
||||
),
|
||||
filter_json: str | None = typer.Option(
|
||||
None, "--filter", help="Advanced filter expression (JSON).", rich_help_panel="Search"
|
||||
None,
|
||||
"--filter",
|
||||
help='Advanced filter as JSON: {"AND": [...]} or {"OR": [...]}, '
|
||||
'e.g. {"AND": [{"categories": {"in": ["work"]}}]}.',
|
||||
rich_help_panel="Search",
|
||||
),
|
||||
fields: str | None = typer.Option(
|
||||
None,
|
||||
@@ -355,6 +383,21 @@ def search(
|
||||
help="Specific fields to return (comma-separated).",
|
||||
rich_help_panel="Search",
|
||||
),
|
||||
show_expired: bool = typer.Option(
|
||||
False, "--show-expired", help="Include expired memories.", rich_help_panel="Search"
|
||||
),
|
||||
reference_date: str | None = typer.Option(
|
||||
None,
|
||||
"--reference-date",
|
||||
help="Reference date for relative queries (YYYY-MM-DD or unix timestamp).",
|
||||
rich_help_panel="Search",
|
||||
),
|
||||
latest_only: bool = typer.Option(
|
||||
False,
|
||||
"--latest-only",
|
||||
help="Only return the latest version of each memory.",
|
||||
rich_help_panel="Search",
|
||||
),
|
||||
output: str = typer.Option(
|
||||
"text", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output"
|
||||
),
|
||||
@@ -375,6 +418,7 @@ def search(
|
||||
mem0 search "preferences" --user-id alice
|
||||
mem0 search "tools" -u alice -o json -k 5
|
||||
echo "preferences" | mem0 search -u alice
|
||||
mem0 search "invoices" -u alice --filter '{"AND": [{"categories": {"in": ["work"]}}]}'
|
||||
"""
|
||||
from mem0_cli.commands.memory import cmd_search
|
||||
|
||||
@@ -398,6 +442,9 @@ def search(
|
||||
keyword=keyword,
|
||||
filter_json=filter_json,
|
||||
fields=fields,
|
||||
show_expired=show_expired,
|
||||
reference_date=reference_date,
|
||||
latest_only=latest_only,
|
||||
output=output,
|
||||
)
|
||||
|
||||
@@ -464,6 +511,15 @@ def list_cmd(
|
||||
before: str | None = typer.Option(
|
||||
None, "--before", help="Created before (YYYY-MM-DD).", rich_help_panel="Filters"
|
||||
),
|
||||
show_expired: bool = typer.Option(
|
||||
False, "--show-expired", help="Include expired memories.", rich_help_panel="Filters"
|
||||
),
|
||||
latest_only: bool = typer.Option(
|
||||
False,
|
||||
"--latest-only",
|
||||
help="Only return the latest version of each memory.",
|
||||
rich_help_panel="Filters",
|
||||
),
|
||||
output: str = typer.Option(
|
||||
"table", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output"
|
||||
),
|
||||
@@ -497,6 +553,8 @@ def list_cmd(
|
||||
category=category,
|
||||
after=after,
|
||||
before=before,
|
||||
show_expired=show_expired,
|
||||
latest_only=latest_only,
|
||||
output=output,
|
||||
)
|
||||
|
||||
@@ -509,6 +567,10 @@ def update(
|
||||
memory_id: str = typer.Argument(..., help="Memory ID to update."),
|
||||
text: str | None = typer.Argument(None, help="New memory text."),
|
||||
metadata: str | None = typer.Option(None, "--metadata", "-m", help="Update metadata (JSON)."),
|
||||
expires: str | None = typer.Option(None, "--expires", help="Expiration date (YYYY-MM-DD)."),
|
||||
timestamp: int | None = typer.Option(
|
||||
None, "--timestamp", help="Unix timestamp for the memory."
|
||||
),
|
||||
output: str = typer.Option(
|
||||
"text", "--output", "-o", help="Output: text, json, quiet.", rich_help_panel="Output"
|
||||
),
|
||||
@@ -537,7 +599,15 @@ def update(
|
||||
text = _read_stdin()
|
||||
|
||||
backend = _get_backend(api_key, base_url)
|
||||
cmd_update(backend, memory_id, text, metadata=metadata, output=output)
|
||||
cmd_update(
|
||||
backend,
|
||||
memory_id,
|
||||
text,
|
||||
metadata=metadata,
|
||||
expires=expires,
|
||||
timestamp=timestamp,
|
||||
output=output,
|
||||
)
|
||||
|
||||
|
||||
# ── Memory: delete ────────────────────────────────────────────────────────
|
||||
@@ -559,6 +629,9 @@ def delete(
|
||||
False, "--dry-run", help="Show what would be deleted without deleting."
|
||||
),
|
||||
force: bool = typer.Option(False, "--force", help="Skip confirmation."),
|
||||
delete_linked: bool = typer.Option(
|
||||
False, "--delete-linked", help="Also delete memories linked to this memory."
|
||||
),
|
||||
user_id: str | None = typer.Option(
|
||||
None, "--user-id", "-u", help="Scope to user.", rich_help_panel="Scope"
|
||||
),
|
||||
@@ -616,7 +689,14 @@ def delete(
|
||||
from mem0_cli.commands.memory import cmd_delete
|
||||
|
||||
backend = _get_backend(api_key, base_url)
|
||||
cmd_delete(backend, memory_id, dry_run=dry_run, force=force, output=output)
|
||||
cmd_delete(
|
||||
backend,
|
||||
memory_id,
|
||||
dry_run=dry_run,
|
||||
force=force,
|
||||
delete_linked=delete_linked,
|
||||
output=output,
|
||||
)
|
||||
|
||||
elif all_:
|
||||
_fire_telemetry("delete", {"delete_mode": "all"})
|
||||
@@ -1009,6 +1089,18 @@ def status(
|
||||
)
|
||||
|
||||
|
||||
@app.command(rich_help_panel="Management")
|
||||
def version() -> None:
|
||||
"""Show version and exit.
|
||||
|
||||
Example:
|
||||
mem0 version
|
||||
"""
|
||||
from mem0_cli.commands.utils import cmd_version
|
||||
|
||||
cmd_version()
|
||||
|
||||
|
||||
@app.command("import", rich_help_panel="Management")
|
||||
def import_cmd(
|
||||
file_path: str = typer.Argument(..., help="JSON file to import."),
|
||||
@@ -1068,7 +1160,12 @@ def _build_help_json() -> dict:
|
||||
"--immutable": "Prevent future updates.",
|
||||
"--no-infer": "Skip inference, store raw.",
|
||||
"--expires": "Expiration date (YYYY-MM-DD).",
|
||||
"--categories": "Categories (JSON array or comma-separated).",
|
||||
"--categories": "Not supported on add, use --custom-categories instead.",
|
||||
"--custom-instructions": "Custom instructions for fact extraction.",
|
||||
"--agent-custom-instructions": "Extraction instructions for agent-scoped memories, overriding the project setting.",
|
||||
"--custom-categories": "Custom categories as a JSON array of {name: description} objects.",
|
||||
"--structured-data-schema": "Schema for structured data extraction, as JSON.",
|
||||
"--timestamp": "Unix timestamp for the memory.",
|
||||
"--graph": "Enable graph memory extraction.",
|
||||
"--no-graph": "Disable graph memory extraction.",
|
||||
"--output, -o": "Output format: text, json, quiet.",
|
||||
@@ -1085,8 +1182,14 @@ def _build_help_json() -> dict:
|
||||
"--threshold": "Minimum similarity score (default: 0.3).",
|
||||
"--rerank": "Enable reranking (Platform only).",
|
||||
"--keyword": "Use keyword search instead of semantic.",
|
||||
"--filter": "Advanced filter expression (JSON).",
|
||||
"--filter": (
|
||||
'Advanced filter as JSON: {"AND": [...]} or {"OR": [...]}, '
|
||||
'e.g. {"AND": [{"categories": {"in": ["work"]}}]}.'
|
||||
),
|
||||
"--fields": "Specific fields to return (comma-separated).",
|
||||
"--show-expired": "Include expired memories.",
|
||||
"--reference-date": "Reference date for relative queries (YYYY-MM-DD or unix timestamp).",
|
||||
"--latest-only": "Only return the latest version of each memory.",
|
||||
"--graph": "Enable graph in search.",
|
||||
"--no-graph": "Disable graph in search.",
|
||||
"--output, -o": "Output format: text, json, table.",
|
||||
@@ -1110,6 +1213,8 @@ def _build_help_json() -> dict:
|
||||
"--category": "Filter by category.",
|
||||
"--after": "Created after (YYYY-MM-DD).",
|
||||
"--before": "Created before (YYYY-MM-DD).",
|
||||
"--show-expired": "Include expired memories.",
|
||||
"--latest-only": "Only return the latest version of each memory.",
|
||||
"--graph": "Enable graph in listing.",
|
||||
"--no-graph": "Disable graph in listing.",
|
||||
"--output, -o": "Output format: text, json, table.",
|
||||
@@ -1124,6 +1229,8 @@ def _build_help_json() -> dict:
|
||||
},
|
||||
"options": {
|
||||
"--metadata, -m": "Update metadata (JSON).",
|
||||
"--expires": "Expiration date (YYYY-MM-DD).",
|
||||
"--timestamp": "Unix timestamp for the memory.",
|
||||
"--output, -o": "Output format: text, json, quiet.",
|
||||
},
|
||||
},
|
||||
@@ -1140,6 +1247,7 @@ def _build_help_json() -> dict:
|
||||
"--all": "Delete all memories matching scope filters.",
|
||||
"--entity": "Delete the entity itself and all its memories (cascade).",
|
||||
"--project": "With --all: delete ALL memories project-wide.",
|
||||
"--delete-linked": "Also delete memories linked to this memory.",
|
||||
"--dry-run": "Show what would be deleted without deleting.",
|
||||
"--force": "Skip confirmation.",
|
||||
"--user-id, -u": "Scope to user.",
|
||||
@@ -1269,8 +1377,10 @@ def help(
|
||||
mem0 help
|
||||
mem0 help --json
|
||||
"""
|
||||
if json:
|
||||
console.print(_json.dumps(_build_help_json(), indent=2))
|
||||
from mem0_cli.state import is_agent_mode
|
||||
|
||||
if json or is_agent_mode():
|
||||
console.print_json(_json.dumps(_build_help_json()))
|
||||
else:
|
||||
console.print(
|
||||
f"[{BRAND_COLOR}]◆ mem0 CLI[/] v{__version__} — The Memory Layer for AI Agents\n"
|
||||
|
||||
@@ -25,7 +25,11 @@ class Backend(ABC):
|
||||
immutable: bool = False,
|
||||
infer: bool = True,
|
||||
expires: str | None = None,
|
||||
categories: list[str] | None = None,
|
||||
custom_instructions: str | None = None,
|
||||
agent_custom_instructions: str | None = None,
|
||||
custom_categories: list[dict] | None = None,
|
||||
structured_data_schema: dict | None = None,
|
||||
timestamp: int | None = None,
|
||||
) -> dict: ...
|
||||
|
||||
@abstractmethod
|
||||
@@ -43,6 +47,9 @@ class Backend(ABC):
|
||||
keyword: bool = False,
|
||||
filters: dict | None = None,
|
||||
fields: list[str] | None = None,
|
||||
show_expired: bool = False,
|
||||
reference_date: str | None = None,
|
||||
latest_only: bool = False,
|
||||
) -> list[dict]: ...
|
||||
|
||||
@abstractmethod
|
||||
@@ -61,11 +68,19 @@ class Backend(ABC):
|
||||
category: str | None = None,
|
||||
after: str | None = None,
|
||||
before: str | None = None,
|
||||
show_expired: bool = False,
|
||||
latest_only: bool = False,
|
||||
) -> list[dict]: ...
|
||||
|
||||
@abstractmethod
|
||||
def update(
|
||||
self, memory_id: str, content: str | None = None, metadata: dict | None = None
|
||||
self,
|
||||
memory_id: str,
|
||||
content: str | None = None,
|
||||
metadata: dict | None = None,
|
||||
*,
|
||||
expiration_date: str | None = None,
|
||||
timestamp: int | None = None,
|
||||
) -> dict: ...
|
||||
|
||||
@abstractmethod
|
||||
@@ -78,6 +93,7 @@ class Backend(ABC):
|
||||
agent_id: str | None = None,
|
||||
app_id: str | None = None,
|
||||
run_id: str | None = None,
|
||||
delete_linked: bool = False,
|
||||
) -> dict: ...
|
||||
|
||||
@abstractmethod
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from urllib.parse import quote
|
||||
|
||||
import httpx
|
||||
|
||||
@@ -11,6 +12,10 @@ from mem0_cli.backend.base import Backend
|
||||
from mem0_cli.config import PlatformConfig
|
||||
|
||||
|
||||
def _encode_path_segment(value: Any) -> str:
|
||||
return quote(str(value), safe="")
|
||||
|
||||
|
||||
class PlatformBackend(Backend):
|
||||
"""Backend that talks to the mem0 Platform API."""
|
||||
|
||||
@@ -82,7 +87,11 @@ class PlatformBackend(Backend):
|
||||
immutable: bool = False,
|
||||
infer: bool = True,
|
||||
expires: str | None = None,
|
||||
categories: list[str] | None = None,
|
||||
custom_instructions: str | None = None,
|
||||
agent_custom_instructions: str | None = None,
|
||||
custom_categories: list[dict] | None = None,
|
||||
structured_data_schema: dict | None = None,
|
||||
timestamp: int | None = None,
|
||||
) -> dict:
|
||||
payload: dict[str, Any] = {}
|
||||
|
||||
@@ -107,8 +116,16 @@ class PlatformBackend(Backend):
|
||||
payload["infer"] = False
|
||||
if expires:
|
||||
payload["expiration_date"] = expires
|
||||
if categories:
|
||||
payload["categories"] = categories
|
||||
if custom_instructions:
|
||||
payload["custom_instructions"] = custom_instructions
|
||||
if agent_custom_instructions:
|
||||
payload["agent_custom_instructions"] = agent_custom_instructions
|
||||
if custom_categories:
|
||||
payload["custom_categories"] = custom_categories
|
||||
if structured_data_schema:
|
||||
payload["structured_data_schema"] = structured_data_schema
|
||||
if timestamp is not None:
|
||||
payload["timestamp"] = timestamp
|
||||
payload["source"] = "CLI"
|
||||
|
||||
return self._request("POST", "/v3/memories/add/", json=payload)
|
||||
@@ -168,6 +185,9 @@ class PlatformBackend(Backend):
|
||||
keyword: bool = False,
|
||||
filters: dict | None = None,
|
||||
fields: list[str] | None = None,
|
||||
show_expired: bool = False,
|
||||
reference_date: str | None = None,
|
||||
latest_only: bool = False,
|
||||
) -> list[dict]:
|
||||
payload: dict[str, Any] = {"query": query, "top_k": top_k, "threshold": threshold}
|
||||
|
||||
@@ -186,6 +206,12 @@ class PlatformBackend(Backend):
|
||||
payload["keyword_search"] = True
|
||||
if fields:
|
||||
payload["fields"] = fields
|
||||
if show_expired:
|
||||
payload["show_expired"] = True
|
||||
if reference_date is not None:
|
||||
payload["reference_date"] = reference_date
|
||||
if latest_only:
|
||||
payload["latest_only"] = True
|
||||
payload["source"] = "CLI"
|
||||
|
||||
result = self._request("POST", "/v3/memories/search/", json=payload)
|
||||
@@ -196,7 +222,11 @@ class PlatformBackend(Backend):
|
||||
)
|
||||
|
||||
def get(self, memory_id: str) -> dict:
|
||||
return self._request("GET", f"/v1/memories/{memory_id}/", params={"source": "CLI"})
|
||||
return self._request(
|
||||
"GET",
|
||||
f"/v1/memories/{_encode_path_segment(memory_id)}/",
|
||||
params={"source": "CLI"},
|
||||
)
|
||||
|
||||
def list_memories(
|
||||
self,
|
||||
@@ -210,6 +240,8 @@ class PlatformBackend(Backend):
|
||||
category: str | None = None,
|
||||
after: str | None = None,
|
||||
before: str | None = None,
|
||||
show_expired: bool = False,
|
||||
latest_only: bool = False,
|
||||
) -> list[dict]:
|
||||
payload: dict[str, Any] = {}
|
||||
params = {"page": str(page), "page_size": str(page_size)}
|
||||
@@ -232,6 +264,10 @@ class PlatformBackend(Backend):
|
||||
)
|
||||
if api_filters:
|
||||
payload["filters"] = api_filters
|
||||
if show_expired:
|
||||
payload["show_expired"] = True
|
||||
if latest_only:
|
||||
payload["latest_only"] = True
|
||||
payload["source"] = "CLI"
|
||||
|
||||
result = self._request("POST", "/v3/memories/", json=payload, params=params)
|
||||
@@ -242,15 +278,29 @@ class PlatformBackend(Backend):
|
||||
)
|
||||
|
||||
def update(
|
||||
self, memory_id: str, content: str | None = None, metadata: dict | None = None
|
||||
self,
|
||||
memory_id: str,
|
||||
content: str | None = None,
|
||||
metadata: dict | None = None,
|
||||
*,
|
||||
expiration_date: str | None = None,
|
||||
timestamp: int | None = None,
|
||||
) -> dict:
|
||||
payload: dict[str, Any] = {}
|
||||
if content:
|
||||
payload["text"] = content
|
||||
if metadata:
|
||||
payload["metadata"] = metadata
|
||||
if expiration_date:
|
||||
payload["expiration_date"] = expiration_date
|
||||
if timestamp is not None:
|
||||
payload["timestamp"] = timestamp
|
||||
payload["source"] = "CLI"
|
||||
return self._request("PUT", f"/v1/memories/{memory_id}/", json=payload)
|
||||
return self._request(
|
||||
"PUT",
|
||||
f"/v1/memories/{_encode_path_segment(memory_id)}/",
|
||||
json=payload,
|
||||
)
|
||||
|
||||
def delete(
|
||||
self,
|
||||
@@ -261,6 +311,7 @@ class PlatformBackend(Backend):
|
||||
agent_id: str | None = None,
|
||||
app_id: str | None = None,
|
||||
run_id: str | None = None,
|
||||
delete_linked: bool = False,
|
||||
) -> dict:
|
||||
if all:
|
||||
params: dict[str, str] = {"source": "CLI"}
|
||||
@@ -274,7 +325,14 @@ class PlatformBackend(Backend):
|
||||
params["run_id"] = run_id
|
||||
return self._request("DELETE", "/v1/memories/", params=params)
|
||||
elif memory_id:
|
||||
return self._request("DELETE", f"/v1/memories/{memory_id}/", params={"source": "CLI"})
|
||||
params = {"source": "CLI"}
|
||||
if delete_linked:
|
||||
params["delete_linked"] = "true"
|
||||
return self._request(
|
||||
"DELETE",
|
||||
f"/v1/memories/{_encode_path_segment(memory_id)}/",
|
||||
params=params,
|
||||
)
|
||||
else:
|
||||
raise ValueError("Either memory_id or --all is required")
|
||||
|
||||
@@ -296,13 +354,17 @@ class PlatformBackend(Backend):
|
||||
entities = {t: v for t, v in type_map.items() if v}
|
||||
if not entities:
|
||||
raise ValueError("At least one entity ID is required for delete_entities.")
|
||||
# Delete each provided entity via the v2 path-based endpoint
|
||||
result: dict = {}
|
||||
# Delete each provided entity via the v2 path-based endpoint. Key each
|
||||
# response by entity type so a multi-entity delete (e.g. --user-id and
|
||||
# --agent-id together) doesn't discard everything but the last result.
|
||||
results: dict = {}
|
||||
for entity_type, entity_id in entities.items():
|
||||
result = self._request(
|
||||
"DELETE", f"/v2/entities/{entity_type}/{entity_id}/", params={"source": "CLI"}
|
||||
results[entity_type] = self._request(
|
||||
"DELETE",
|
||||
f"/v2/entities/{_encode_path_segment(entity_type)}/{_encode_path_segment(entity_id)}/",
|
||||
params={"source": "CLI"},
|
||||
)
|
||||
return result
|
||||
return results
|
||||
|
||||
def ping(self, timeout: float | None = None) -> dict:
|
||||
"""Call the ping endpoint and return the raw response.
|
||||
@@ -346,7 +408,7 @@ class PlatformBackend(Backend):
|
||||
return result if isinstance(result, list) else result.get("results", [])
|
||||
|
||||
def get_event(self, event_id: str) -> dict:
|
||||
return self._request("GET", f"/v1/event/{event_id}/")
|
||||
return self._request("GET", f"/v1/event/{_encode_path_segment(event_id)}/")
|
||||
|
||||
|
||||
class AuthError(Exception):
|
||||
|
||||
@@ -4,9 +4,11 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import stat as _stat_mod
|
||||
import sys
|
||||
import time as _time
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
import typer
|
||||
@@ -47,6 +49,18 @@ def _stdin_is_piped() -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _validate_expires(value: str) -> None:
|
||||
"""Exit 1 if value is not a future YYYY-MM-DD date."""
|
||||
if not re.match(r"^\d{4}-\d{2}-\d{2}$", value):
|
||||
print_error(
|
||||
err_console, "Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31)."
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
if date.fromisoformat(value) <= date.today():
|
||||
print_error(err_console, "--expires date must be in the future.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
def cmd_add(
|
||||
backend: Backend,
|
||||
text: str | None,
|
||||
@@ -62,6 +76,11 @@ def cmd_add(
|
||||
no_infer: bool,
|
||||
expires: str | None,
|
||||
categories: str | None,
|
||||
custom_instructions: str | None = None,
|
||||
agent_custom_instructions: str | None = None,
|
||||
custom_categories: str | None = None,
|
||||
structured_data_schema: str | None = None,
|
||||
timestamp: int | None = None,
|
||||
output: str = "text",
|
||||
) -> None:
|
||||
"""Add a memory."""
|
||||
@@ -70,6 +89,13 @@ def cmd_add(
|
||||
set_current_command("add")
|
||||
if is_agent_mode():
|
||||
output = "agent"
|
||||
|
||||
if categories:
|
||||
print_error(
|
||||
err_console, "--categories is not supported on add. Use --custom-categories instead."
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
msgs = None
|
||||
content = text
|
||||
|
||||
@@ -108,27 +134,24 @@ def cmd_add(
|
||||
print_error(err_console, "Invalid JSON in --metadata.")
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
cats = None
|
||||
if categories:
|
||||
custom_cats = None
|
||||
if custom_categories:
|
||||
try:
|
||||
cats = json.loads(categories)
|
||||
custom_cats = json.loads(custom_categories)
|
||||
except json.JSONDecodeError:
|
||||
cats = [c.strip() for c in categories.split(",")]
|
||||
print_error(err_console, "Invalid JSON in --custom-categories.")
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
schema = None
|
||||
if structured_data_schema:
|
||||
try:
|
||||
schema = json.loads(structured_data_schema)
|
||||
except json.JSONDecodeError:
|
||||
print_error(err_console, "Invalid JSON in --structured-data-schema.")
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
# Validate --expires
|
||||
if expires:
|
||||
import re
|
||||
|
||||
if not re.match(r"^\d{4}-\d{2}-\d{2}$", expires):
|
||||
print_error(
|
||||
err_console, "Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31)."
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
from datetime import date
|
||||
|
||||
if date.fromisoformat(expires) <= date.today():
|
||||
print_error(err_console, "--expires date must be in the future.")
|
||||
raise typer.Exit(1)
|
||||
_validate_expires(expires)
|
||||
|
||||
with timed_status(err_console, "Adding memory...") as ts:
|
||||
try:
|
||||
@@ -143,7 +166,11 @@ def cmd_add(
|
||||
immutable=immutable,
|
||||
infer=not no_infer,
|
||||
expires=expires,
|
||||
categories=cats,
|
||||
custom_instructions=custom_instructions,
|
||||
agent_custom_instructions=agent_custom_instructions,
|
||||
custom_categories=custom_cats,
|
||||
structured_data_schema=schema,
|
||||
timestamp=timestamp,
|
||||
)
|
||||
except Exception as e:
|
||||
ts.error_msg = str(e)
|
||||
@@ -224,6 +251,9 @@ def cmd_search(
|
||||
keyword: bool,
|
||||
filter_json: str | None,
|
||||
fields: str | None,
|
||||
show_expired: bool = False,
|
||||
reference_date: str | None = None,
|
||||
latest_only: bool = False,
|
||||
output: str = "text",
|
||||
) -> None:
|
||||
"""Search memories."""
|
||||
@@ -266,6 +296,9 @@ def cmd_search(
|
||||
keyword=keyword,
|
||||
filters=filters,
|
||||
fields=field_list,
|
||||
show_expired=show_expired,
|
||||
reference_date=reference_date,
|
||||
latest_only=latest_only,
|
||||
)
|
||||
except Exception as e:
|
||||
print_error(err_console, str(e))
|
||||
@@ -352,6 +385,8 @@ def cmd_list(
|
||||
category: str | None,
|
||||
after: str | None,
|
||||
before: str | None,
|
||||
show_expired: bool = False,
|
||||
latest_only: bool = False,
|
||||
output: str = "table",
|
||||
) -> None:
|
||||
"""List memories."""
|
||||
@@ -380,6 +415,8 @@ def cmd_list(
|
||||
category=category,
|
||||
after=after,
|
||||
before=before,
|
||||
show_expired=show_expired,
|
||||
latest_only=latest_only,
|
||||
)
|
||||
except Exception as e:
|
||||
print_error(err_console, str(e))
|
||||
@@ -446,6 +483,8 @@ def cmd_update(
|
||||
text: str | None,
|
||||
*,
|
||||
metadata: str | None,
|
||||
expires: str | None = None,
|
||||
timestamp: int | None = None,
|
||||
output: str,
|
||||
) -> None:
|
||||
"""Update a memory."""
|
||||
@@ -462,10 +501,19 @@ def cmd_update(
|
||||
print_error(err_console, "Invalid JSON in --metadata.")
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
if expires:
|
||||
_validate_expires(expires)
|
||||
|
||||
_start = _time.perf_counter()
|
||||
with timed_status(err_console, "Updating memory...") as _ts:
|
||||
try:
|
||||
result = backend.update(memory_id, content=text, metadata=meta)
|
||||
result = backend.update(
|
||||
memory_id,
|
||||
content=text,
|
||||
metadata=meta,
|
||||
expiration_date=expires,
|
||||
timestamp=timestamp,
|
||||
)
|
||||
except Exception as e:
|
||||
print_error(err_console, str(e))
|
||||
raise typer.Exit(1) from None
|
||||
@@ -490,6 +538,7 @@ def cmd_delete(
|
||||
*,
|
||||
dry_run: bool = False,
|
||||
force: bool = False,
|
||||
delete_linked: bool = False,
|
||||
output: str,
|
||||
) -> None:
|
||||
"""Delete a single memory by ID."""
|
||||
@@ -512,7 +561,7 @@ def cmd_delete(
|
||||
_start = _time.perf_counter()
|
||||
with timed_status(err_console, "Deleting...") as _ts:
|
||||
try:
|
||||
result = backend.delete(memory_id=memory_id)
|
||||
result = backend.delete(memory_id=memory_id, delete_linked=delete_linked)
|
||||
except Exception as e:
|
||||
print_error(err_console, str(e))
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
@@ -235,7 +235,10 @@ def set_nested_value(config: Mem0Config, dotted_key: str, value: str) -> bool:
|
||||
if isinstance(current, bool):
|
||||
value = value.lower() in ("true", "1", "yes") # type: ignore[assignment]
|
||||
elif isinstance(current, int):
|
||||
value = int(value) # type: ignore[assignment]
|
||||
try:
|
||||
value = int(value) # type: ignore[assignment]
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
setattr(obj, final_key, value)
|
||||
return True
|
||||
|
||||
@@ -20,8 +20,8 @@ def format_memories_text(console: Console, memories: list[dict], title: str = "m
|
||||
console.print(f"\n[{BRAND_COLOR}]Found {count} {title}:[/]\n")
|
||||
|
||||
for i, mem in enumerate(memories, 1):
|
||||
memory_text = mem.get("memory", mem.get("text", ""))
|
||||
mem_id = mem.get("id", "")[:8]
|
||||
memory_text = mem.get("memory") or mem.get("text") or ""
|
||||
mem_id = (mem.get("id") or "")[:8]
|
||||
score = mem.get("score")
|
||||
created = _format_date(mem.get("created_at"))
|
||||
category = mem.get("categories", [None])
|
||||
@@ -67,8 +67,8 @@ def format_memories_table(
|
||||
table.add_column("Created", max_width=12)
|
||||
|
||||
for mem in memories:
|
||||
mem_id = mem.get("id", "")
|
||||
memory_text = mem.get("memory", mem.get("text", ""))
|
||||
mem_id = mem.get("id") or ""
|
||||
memory_text = mem.get("memory") or mem.get("text") or ""
|
||||
if len(memory_text) > 60:
|
||||
memory_text = memory_text[:57] + "..."
|
||||
categories = mem.get("categories", [])
|
||||
@@ -104,8 +104,8 @@ def format_single_memory(console: Console, mem: dict, output: str = "text") -> N
|
||||
format_json(console, mem)
|
||||
return
|
||||
|
||||
memory_text = mem.get("memory", mem.get("text", ""))
|
||||
mem_id = mem.get("id", "")
|
||||
memory_text = mem.get("memory") or mem.get("text") or ""
|
||||
mem_id = mem.get("id") or ""
|
||||
|
||||
lines = []
|
||||
lines.append(f" [white bold]{memory_text}[/]")
|
||||
@@ -262,16 +262,32 @@ def sanitize_agent_data(command: str, data: Any) -> Any:
|
||||
return result
|
||||
|
||||
if command == "search":
|
||||
return [pick(r, ["id", "memory", "score", "created_at", "categories"]) for r in data]
|
||||
return [
|
||||
pick(r, ["id", "memory", "score", "created_at", "categories", "expiration_date"])
|
||||
for r in data
|
||||
]
|
||||
|
||||
if command == "list":
|
||||
return [pick(r, ["id", "memory", "created_at", "categories"]) for r in data]
|
||||
return [
|
||||
pick(r, ["id", "memory", "created_at", "categories", "expiration_date"]) for r in data
|
||||
]
|
||||
|
||||
if command == "get":
|
||||
return pick(data, ["id", "memory", "created_at", "updated_at", "categories", "metadata"])
|
||||
return pick(
|
||||
data,
|
||||
[
|
||||
"id",
|
||||
"memory",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"categories",
|
||||
"metadata",
|
||||
"expiration_date",
|
||||
],
|
||||
)
|
||||
|
||||
if command == "update":
|
||||
return pick(data, ["id", "memory"])
|
||||
return pick(data, ["id", "memory", "expiration_date"])
|
||||
|
||||
if command in ("delete", "delete-all", "entity delete"):
|
||||
return data
|
||||
|
||||
@@ -7,6 +7,7 @@ boundaries).
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
@@ -84,6 +85,36 @@ class TestCLIIntegration:
|
||||
assert "add" in result.stdout
|
||||
assert "search" in result.stdout
|
||||
|
||||
def test_version_flag_only(self):
|
||||
from mem0_cli import __version__
|
||||
|
||||
flag = _run(["--version"])
|
||||
assert flag.returncode == 0
|
||||
assert __version__ in flag.stdout
|
||||
|
||||
def test_version_subcommand_matches_flag_byte_for_byte(self):
|
||||
flag = _run(["--version"])
|
||||
cmd = _run(["version"])
|
||||
assert cmd.returncode == 0
|
||||
assert cmd.stdout == flag.stdout
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"args",
|
||||
[["help", "--json"], ["--json", "help"], ["help", "--agent"], ["--agent", "help"]],
|
||||
)
|
||||
def test_help_json_produces_valid_json(self, args):
|
||||
result = _run(args)
|
||||
assert result.returncode == 0
|
||||
spec = json.loads(result.stdout)
|
||||
assert spec["name"] == "mem0"
|
||||
assert "add" in spec["commands"]
|
||||
|
||||
def test_help_without_json_is_text(self):
|
||||
result = _run(["help"])
|
||||
assert result.returncode == 0
|
||||
with pytest.raises(json.JSONDecodeError):
|
||||
json.loads(result.stdout)
|
||||
|
||||
def test_add_help(self):
|
||||
result = _run(["add", "--help"])
|
||||
assert result.returncode == 0
|
||||
@@ -101,6 +132,12 @@ class TestCLIIntegration:
|
||||
assert result.returncode == 0
|
||||
assert "top-k" in result.stdout
|
||||
|
||||
def test_search_help_documents_filter_json_shape(self):
|
||||
result = _run(["search", "--help"])
|
||||
assert result.returncode == 0
|
||||
assert "AND" in result.stdout
|
||||
assert "categories" in result.stdout
|
||||
|
||||
def test_list_help(self):
|
||||
result = _run(["list", "--help"])
|
||||
assert result.returncode == 0
|
||||
|
||||
@@ -252,12 +252,13 @@ class TestAddCommand:
|
||||
)
|
||||
mock_backend.add.assert_called_once()
|
||||
|
||||
def test_add_categories_csv(self, mock_backend):
|
||||
def test_add_categories_rejected(self, mock_backend):
|
||||
console, _buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
err_console, err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
pytest.raises((SystemExit, TyperExit)),
|
||||
):
|
||||
cmd_add(
|
||||
mock_backend,
|
||||
@@ -275,7 +276,93 @@ class TestAddCommand:
|
||||
categories="health,prefs",
|
||||
output="text",
|
||||
)
|
||||
mock_backend.add.assert_called_once()
|
||||
assert "--custom-categories" in err_buf.getvalue()
|
||||
mock_backend.add.assert_not_called()
|
||||
|
||||
def test_add_invalid_custom_categories_json(self, mock_backend):
|
||||
console, _buf = _make_console()
|
||||
err_console, err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
pytest.raises((SystemExit, TyperExit)),
|
||||
):
|
||||
cmd_add(
|
||||
mock_backend,
|
||||
"test",
|
||||
user_id="alice",
|
||||
agent_id=None,
|
||||
app_id=None,
|
||||
run_id=None,
|
||||
messages=None,
|
||||
file=None,
|
||||
metadata=None,
|
||||
immutable=False,
|
||||
no_infer=False,
|
||||
expires=None,
|
||||
categories=None,
|
||||
custom_categories="not-json",
|
||||
output="text",
|
||||
)
|
||||
assert "--custom-categories" in err_buf.getvalue()
|
||||
mock_backend.add.assert_not_called()
|
||||
|
||||
def test_add_invalid_structured_data_schema_json(self, mock_backend):
|
||||
console, _buf = _make_console()
|
||||
err_console, err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
pytest.raises((SystemExit, TyperExit)),
|
||||
):
|
||||
cmd_add(
|
||||
mock_backend,
|
||||
"test",
|
||||
user_id="alice",
|
||||
agent_id=None,
|
||||
app_id=None,
|
||||
run_id=None,
|
||||
messages=None,
|
||||
file=None,
|
||||
metadata=None,
|
||||
immutable=False,
|
||||
no_infer=False,
|
||||
expires=None,
|
||||
categories=None,
|
||||
structured_data_schema="not-json",
|
||||
output="text",
|
||||
)
|
||||
assert "--structured-data-schema" in err_buf.getvalue()
|
||||
mock_backend.add.assert_not_called()
|
||||
|
||||
def test_add_regression_metadata_expiration_custom_categories_together(self, mock_backend):
|
||||
console, _buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
):
|
||||
cmd_add(
|
||||
mock_backend,
|
||||
"test",
|
||||
user_id="alice",
|
||||
agent_id=None,
|
||||
app_id=None,
|
||||
run_id=None,
|
||||
messages=None,
|
||||
file=None,
|
||||
metadata='{"source": "test"}',
|
||||
immutable=False,
|
||||
no_infer=False,
|
||||
expires="2099-01-01",
|
||||
categories=None,
|
||||
custom_categories='[{"prefs": "user preferences"}]',
|
||||
output="text",
|
||||
)
|
||||
call_kwargs = mock_backend.add.call_args.kwargs
|
||||
assert call_kwargs["metadata"] == {"source": "test"}
|
||||
assert call_kwargs["expires"] == "2099-01-01"
|
||||
assert call_kwargs["custom_categories"] == [{"prefs": "user preferences"}]
|
||||
|
||||
|
||||
class TestAddDeduplicatesPending:
|
||||
@@ -464,6 +551,36 @@ class TestSearchCommand:
|
||||
)
|
||||
mock_backend.search.assert_called_once()
|
||||
|
||||
def test_search_new_flags_reach_backend(self, mock_backend):
|
||||
console, _buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
):
|
||||
cmd_search(
|
||||
mock_backend,
|
||||
"preferences",
|
||||
user_id="alice",
|
||||
agent_id=None,
|
||||
app_id=None,
|
||||
run_id=None,
|
||||
top_k=10,
|
||||
threshold=0.3,
|
||||
rerank=False,
|
||||
keyword=False,
|
||||
filter_json=None,
|
||||
fields=None,
|
||||
show_expired=True,
|
||||
reference_date="2024-01-01",
|
||||
latest_only=True,
|
||||
output="text",
|
||||
)
|
||||
call_kwargs = mock_backend.search.call_args.kwargs
|
||||
assert call_kwargs["show_expired"] is True
|
||||
assert call_kwargs["reference_date"] == "2024-01-01"
|
||||
assert call_kwargs["latest_only"] is True
|
||||
|
||||
|
||||
class TestGetCommand:
|
||||
def test_get_text(self, mock_backend):
|
||||
@@ -561,6 +678,32 @@ class TestListCommand:
|
||||
output = buf.getvalue()
|
||||
assert "No memories found" in output
|
||||
|
||||
def test_list_new_flags_reach_backend(self, mock_backend):
|
||||
console, _buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
):
|
||||
cmd_list(
|
||||
mock_backend,
|
||||
user_id="alice",
|
||||
agent_id=None,
|
||||
app_id=None,
|
||||
run_id=None,
|
||||
page=1,
|
||||
page_size=100,
|
||||
category=None,
|
||||
after=None,
|
||||
before=None,
|
||||
show_expired=True,
|
||||
latest_only=True,
|
||||
output="table",
|
||||
)
|
||||
call_kwargs = mock_backend.list_memories.call_args.kwargs
|
||||
assert call_kwargs["show_expired"] is True
|
||||
assert call_kwargs["latest_only"] is True
|
||||
|
||||
|
||||
class TestUpdateCommand:
|
||||
def test_update(self, mock_backend):
|
||||
@@ -585,6 +728,26 @@ class TestUpdateCommand:
|
||||
output = buf.getvalue()
|
||||
assert '"memory"' in output
|
||||
|
||||
def test_update_new_fields_reach_backend(self, mock_backend):
|
||||
console, _buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
):
|
||||
cmd_update(
|
||||
mock_backend,
|
||||
"abc-123",
|
||||
"New text",
|
||||
metadata=None,
|
||||
expires="2099-01-01",
|
||||
timestamp=1700000000,
|
||||
output="text",
|
||||
)
|
||||
call_kwargs = mock_backend.update.call_args.kwargs
|
||||
assert call_kwargs["expiration_date"] == "2099-01-01"
|
||||
assert call_kwargs["timestamp"] == 1700000000
|
||||
|
||||
|
||||
class TestDeleteCommand:
|
||||
def test_delete_single(self, mock_backend):
|
||||
@@ -610,6 +773,17 @@ class TestDeleteCommand:
|
||||
assert "dry run" in output.lower()
|
||||
mock_backend.delete.assert_not_called()
|
||||
|
||||
def test_delete_linked_reaches_backend(self, mock_backend):
|
||||
console, _buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
):
|
||||
cmd_delete(mock_backend, "abc-123", delete_linked=True, output="text")
|
||||
call_kwargs = mock_backend.delete.call_args.kwargs
|
||||
assert call_kwargs["delete_linked"] is True
|
||||
|
||||
|
||||
class TestDeleteAllCommand:
|
||||
def test_delete_all_force(self, mock_backend):
|
||||
|
||||
@@ -139,6 +139,11 @@ class TestNestedAccess:
|
||||
assert set_nested_value(config, "platform.api_key", "new-key")
|
||||
assert config.platform.api_key == "new-key"
|
||||
|
||||
def test_set_int_value_rejects_invalid_input(self):
|
||||
config = Mem0Config()
|
||||
assert set_nested_value(config, "version", "abc") is False
|
||||
assert config.version == 1
|
||||
|
||||
def test_set_nonexistent_key(self):
|
||||
config = Mem0Config()
|
||||
assert set_nested_value(config, "nonexistent.key", "val") is False
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
"""Drift test: every documented v3 add/search/list param must be reachable from the Python CLI."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import typer.main
|
||||
|
||||
from mem0_cli.app import app
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[3]
|
||||
OPENAPI_PATH = REPO_ROOT / "docs" / "openapi.json"
|
||||
|
||||
KNOWN_UNSURFACED: dict[tuple[str, str], str] = {
|
||||
("/v3/memories/add/", "includes"): "extraction hint, no CLI flag yet",
|
||||
("/v3/memories/add/", "excludes"): "extraction hint, no CLI flag yet",
|
||||
("/v3/memories/add/", "enable_graph"): "graph memory toggle, no CLI flag yet",
|
||||
("/v3/memories/add/", "output_format"): "response envelope is pinned by the CLI",
|
||||
("/v3/memories/add/", "prompt_profile_id"): "no CLI flag yet",
|
||||
("/v3/memories/add/", "temporal_reasoning"): "no CLI flag yet",
|
||||
("/v3/memories/add/", "timezone"): "no CLI flag yet",
|
||||
("/v3/memories/add/", "observation_datetime"): "no CLI flag yet, --timestamp backdates instead",
|
||||
("/v3/memories/add/", "observation_date"): "no CLI flag yet, --timestamp backdates instead",
|
||||
("/v3/memories/search/", "categories"): "expressible through --filter",
|
||||
("/v3/memories/search/", "metadata"): "expressible through --filter",
|
||||
("/v3/memories/", "start_date"): "covered by --after via filters.created_at.gte",
|
||||
("/v3/memories/", "end_date"): "covered by --before via filters.created_at.lte",
|
||||
("/v3/memories/", "categories"): "covered by --category via filters.categories",
|
||||
("/v3/memories/", "fields"): "no CLI flag yet",
|
||||
("/v3/memories/", "keywords"): "no CLI flag yet",
|
||||
}
|
||||
|
||||
ADD_MAPPING: dict[str, list[str]] = {
|
||||
"messages": ["messages", "file", "text"],
|
||||
"user_id": ["user_id"],
|
||||
"agent_id": ["agent_id"],
|
||||
"app_id": ["app_id"],
|
||||
"run_id": ["run_id"],
|
||||
"metadata": ["metadata"],
|
||||
"expiration_date": ["expires"],
|
||||
"custom_instructions": ["custom_instructions"],
|
||||
"agent_custom_instructions": ["agent_custom_instructions"],
|
||||
"custom_categories": ["custom_categories"],
|
||||
"infer": ["no_infer"],
|
||||
"immutable": ["immutable"],
|
||||
"structured_data_schema": ["structured_data_schema"],
|
||||
"timestamp": ["timestamp"],
|
||||
}
|
||||
|
||||
SEARCH_MAPPING: dict[str, list[str]] = {
|
||||
"query": ["query"],
|
||||
"filters": ["filter_json", "user_id", "agent_id", "run_id"],
|
||||
"show_expired": ["show_expired"],
|
||||
"top_k": ["top_k"],
|
||||
"threshold": ["threshold"],
|
||||
"rerank": ["rerank"],
|
||||
"reference_date": ["reference_date"],
|
||||
"fields": ["fields"],
|
||||
}
|
||||
|
||||
LIST_MAPPING: dict[str, list[str]] = {
|
||||
"filters": ["user_id", "agent_id", "run_id", "category", "after", "before"],
|
||||
"show_expired": ["show_expired"],
|
||||
"page": ["page"],
|
||||
"page_size": ["page_size"],
|
||||
}
|
||||
|
||||
|
||||
def _documented_fields(endpoint: str) -> set[str]:
|
||||
spec = json.loads(OPENAPI_PATH.read_text())
|
||||
schema = spec["paths"][endpoint]["post"]["requestBody"]["content"]["application/json"]["schema"]
|
||||
return set(schema["properties"])
|
||||
|
||||
|
||||
def _cli_param_names(command_name: str) -> set[str]:
|
||||
click_app = typer.main.get_command(app)
|
||||
command = click_app.commands[command_name]
|
||||
return {param.name for param in command.params}
|
||||
|
||||
|
||||
def _assert_all_reachable(endpoint: str, mapping: dict[str, list[str]], command_name: str) -> None:
|
||||
documented = _documented_fields(endpoint)
|
||||
reachable = _cli_param_names(command_name)
|
||||
for field in documented:
|
||||
if (endpoint, field) in KNOWN_UNSURFACED:
|
||||
continue
|
||||
candidates = mapping.get(field)
|
||||
assert candidates, (
|
||||
f"{endpoint}: documented field {field!r} has no mapping entry for command {command_name!r}"
|
||||
)
|
||||
assert any(candidate in reachable for candidate in candidates), (
|
||||
f"{endpoint}: documented field {field!r} not reachable via any of {candidates} on command {command_name!r}"
|
||||
)
|
||||
|
||||
|
||||
class TestOptionParity:
|
||||
def test_add_covers_documented_fields(self):
|
||||
_assert_all_reachable("/v3/memories/add/", ADD_MAPPING, "add")
|
||||
|
||||
def test_search_covers_documented_fields(self):
|
||||
_assert_all_reachable("/v3/memories/search/", SEARCH_MAPPING, "search")
|
||||
|
||||
def test_list_covers_documented_fields(self):
|
||||
_assert_all_reachable("/v3/memories/", LIST_MAPPING, "list")
|
||||
@@ -54,6 +54,11 @@ class TestTextFormat:
|
||||
output = buf.getvalue()
|
||||
assert "Found 0" in output
|
||||
|
||||
def test_format_memories_text_handles_null_fields(self):
|
||||
console, buf = _make_console()
|
||||
format_memories_text(console, [{"id": None, "memory": None, "created_at": None}])
|
||||
assert "Found 1 memories" in buf.getvalue()
|
||||
|
||||
|
||||
class TestTableFormat:
|
||||
def test_format_memories_table(self):
|
||||
@@ -70,6 +75,13 @@ class TestTableFormat:
|
||||
# Should still render (empty table)
|
||||
assert "ID" in output
|
||||
|
||||
def test_format_memories_table_handles_null_fields(self):
|
||||
console, buf = _make_console()
|
||||
format_memories_table(console, [{"id": None, "memory": None, "created_at": None}])
|
||||
output = buf.getvalue()
|
||||
assert "ID" in output
|
||||
assert "Memory" in output
|
||||
|
||||
|
||||
class TestSingleMemory:
|
||||
def test_format_single_memory_text(self):
|
||||
@@ -87,6 +99,17 @@ class TestSingleMemory:
|
||||
output = buf.getvalue()
|
||||
assert '"memory"' in output
|
||||
|
||||
def test_format_single_memory_handles_null_fields(self):
|
||||
console, buf = _make_console()
|
||||
format_single_memory(
|
||||
console,
|
||||
{"id": None, "memory": None, "text": "Fallback memory", "created_at": None},
|
||||
"text",
|
||||
)
|
||||
output = buf.getvalue()
|
||||
assert "Fallback memory" in output
|
||||
assert "ID:" not in output
|
||||
|
||||
|
||||
class TestAddResult:
|
||||
def test_format_add_result_text(self):
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Tests for the Platform backend (mem0 Platform API client)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from mem0_cli.backend.platform import PlatformBackend
|
||||
from mem0_cli.config import PlatformConfig
|
||||
|
||||
|
||||
def _make_backend() -> PlatformBackend:
|
||||
# api_key/base_url are only used to build the httpx client; every test here
|
||||
# patches _request, so no real network calls are made.
|
||||
return PlatformBackend(PlatformConfig(api_key="test-key", base_url="https://api.mem0.ai"))
|
||||
|
||||
|
||||
class TestDeleteEntities:
|
||||
def test_multiple_entities_returns_all_results(self):
|
||||
backend = _make_backend()
|
||||
responses = {
|
||||
"/v2/entities/user/alice/": {"message": "user deleted"},
|
||||
"/v2/entities/agent/bob/": {"message": "agent deleted"},
|
||||
}
|
||||
with patch.object(backend, "_request") as mock_request:
|
||||
mock_request.side_effect = lambda method, path, **kw: responses[path]
|
||||
result = backend.delete_entities(user_id="alice", agent_id="bob")
|
||||
|
||||
# Regression: previously only the last entity's response survived.
|
||||
assert result == {
|
||||
"user": {"message": "user deleted"},
|
||||
"agent": {"message": "agent deleted"},
|
||||
}
|
||||
assert mock_request.call_count == 2
|
||||
|
||||
def test_single_entity_keyed_by_type(self):
|
||||
backend = _make_backend()
|
||||
with patch.object(backend, "_request", return_value={"message": "user deleted"}):
|
||||
result = backend.delete_entities(user_id="alice")
|
||||
assert result == {"user": {"message": "user deleted"}}
|
||||
|
||||
def test_no_entities_raises(self):
|
||||
backend = _make_backend()
|
||||
import pytest
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
backend.delete_entities()
|
||||
@@ -0,0 +1,112 @@
|
||||
"""Tests that the MEM-5893 option-parity flags reach the correct request payload/params."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from mem0_cli.backend.platform import PlatformBackend
|
||||
from mem0_cli.config import PlatformConfig
|
||||
|
||||
|
||||
def _make_backend() -> PlatformBackend:
|
||||
return PlatformBackend(PlatformConfig(api_key="test-key", base_url="https://api.mem0.ai"))
|
||||
|
||||
|
||||
class TestAddOptions:
|
||||
def test_new_fields_and_existing_fields_land_in_payload_together(self):
|
||||
backend = _make_backend()
|
||||
with patch.object(backend, "_request", return_value={"results": []}) as mock_request:
|
||||
backend.add(
|
||||
content="hello",
|
||||
user_id="alice",
|
||||
metadata={"source": "test"},
|
||||
expires="2099-01-01",
|
||||
custom_instructions="Extract only preferences.",
|
||||
agent_custom_instructions="Extract only tool outcomes.",
|
||||
custom_categories=[{"prefs": "user preferences"}],
|
||||
structured_data_schema={"type": "object"},
|
||||
timestamp=1700000000,
|
||||
)
|
||||
payload = mock_request.call_args.kwargs["json"]
|
||||
assert payload["custom_instructions"] == "Extract only preferences."
|
||||
assert payload["agent_custom_instructions"] == "Extract only tool outcomes."
|
||||
assert payload["custom_categories"] == [{"prefs": "user preferences"}]
|
||||
assert payload["structured_data_schema"] == {"type": "object"}
|
||||
assert payload["timestamp"] == 1700000000
|
||||
assert payload["metadata"] == {"source": "test"}
|
||||
assert payload["expiration_date"] == "2099-01-01"
|
||||
|
||||
def test_omitted_fields_are_absent_from_payload(self):
|
||||
backend = _make_backend()
|
||||
with patch.object(backend, "_request", return_value={"results": []}) as mock_request:
|
||||
backend.add(content="hello", user_id="alice")
|
||||
payload = mock_request.call_args.kwargs["json"]
|
||||
assert "custom_instructions" not in payload
|
||||
assert "agent_custom_instructions" not in payload
|
||||
assert "custom_categories" not in payload
|
||||
assert "structured_data_schema" not in payload
|
||||
assert "timestamp" not in payload
|
||||
|
||||
|
||||
class TestSearchOptions:
|
||||
def test_show_expired_reference_date_latest_only_reach_payload(self):
|
||||
backend = _make_backend()
|
||||
with patch.object(backend, "_request", return_value=[]) as mock_request:
|
||||
backend.search(
|
||||
"query",
|
||||
show_expired=True,
|
||||
reference_date="2024-01-01",
|
||||
latest_only=True,
|
||||
)
|
||||
payload = mock_request.call_args.kwargs["json"]
|
||||
assert payload["show_expired"] is True
|
||||
assert payload["reference_date"] == "2024-01-01"
|
||||
assert payload["latest_only"] is True
|
||||
|
||||
def test_keyword_and_fields_reach_payload(self):
|
||||
backend = _make_backend()
|
||||
with patch.object(backend, "_request", return_value=[]) as mock_request:
|
||||
backend.search("query", keyword=True, fields=["memory", "score"])
|
||||
payload = mock_request.call_args.kwargs["json"]
|
||||
assert payload["keyword_search"] is True
|
||||
assert payload["fields"] == ["memory", "score"]
|
||||
|
||||
def test_keyword_and_fields_omitted_are_absent_from_payload(self):
|
||||
backend = _make_backend()
|
||||
with patch.object(backend, "_request", return_value=[]) as mock_request:
|
||||
backend.search("query")
|
||||
payload = mock_request.call_args.kwargs["json"]
|
||||
assert "keyword_search" not in payload
|
||||
assert "fields" not in payload
|
||||
|
||||
|
||||
class TestListOptions:
|
||||
def test_show_expired_and_latest_only_are_top_level_not_in_filters(self):
|
||||
backend = _make_backend()
|
||||
with patch.object(backend, "_request", return_value=[]) as mock_request:
|
||||
backend.list_memories(user_id="alice", show_expired=True, latest_only=True)
|
||||
payload = mock_request.call_args.kwargs["json"]
|
||||
assert payload["show_expired"] is True
|
||||
assert payload["latest_only"] is True
|
||||
assert "show_expired" not in payload.get("filters", {})
|
||||
assert "latest_only" not in payload.get("filters", {})
|
||||
|
||||
|
||||
class TestUpdateOptions:
|
||||
def test_expires_and_timestamp_reach_payload(self):
|
||||
backend = _make_backend()
|
||||
with patch.object(backend, "_request", return_value={}) as mock_request:
|
||||
backend.update("mem-123", expiration_date="2099-01-01", timestamp=1700000000)
|
||||
payload = mock_request.call_args.kwargs["json"]
|
||||
assert payload["expiration_date"] == "2099-01-01"
|
||||
assert payload["timestamp"] == 1700000000
|
||||
|
||||
|
||||
class TestDeleteOptions:
|
||||
def test_delete_linked_is_a_query_param_not_json_body(self):
|
||||
backend = _make_backend()
|
||||
with patch.object(backend, "_request", return_value={}) as mock_request:
|
||||
backend.delete(memory_id="mem-123", delete_linked=True)
|
||||
call = mock_request.call_args
|
||||
assert call.kwargs["params"]["delete_linked"] == "true"
|
||||
assert "json" not in call.kwargs
|
||||
@@ -0,0 +1,43 @@
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from mem0_cli.backend.platform import PlatformBackend
|
||||
|
||||
|
||||
def _backend(sample_config):
|
||||
backend = PlatformBackend(sample_config.platform)
|
||||
backend._client = MagicMock()
|
||||
backend._client.request.return_value = MagicMock(
|
||||
status_code=200,
|
||||
json=lambda: {"message": "ok"},
|
||||
headers={},
|
||||
raise_for_status=lambda: None,
|
||||
)
|
||||
return backend
|
||||
|
||||
|
||||
def test_memory_id_path_segments_are_encoded(sample_config):
|
||||
backend = _backend(sample_config)
|
||||
|
||||
backend.get("mem/a?b#c")
|
||||
backend.update("mem/a?b#c", content="updated")
|
||||
backend.delete("mem/a?b#c")
|
||||
|
||||
paths = [call.args[1] for call in backend._client.request.call_args_list]
|
||||
assert paths == [
|
||||
"/v1/memories/mem%2Fa%3Fb%23c/",
|
||||
"/v1/memories/mem%2Fa%3Fb%23c/",
|
||||
"/v1/memories/mem%2Fa%3Fb%23c/",
|
||||
]
|
||||
|
||||
|
||||
def test_entity_and_event_path_segments_are_encoded(sample_config):
|
||||
backend = _backend(sample_config)
|
||||
|
||||
backend.delete_entities(user_id="org/team?active#frag")
|
||||
backend.get_event("evt/a?b#c")
|
||||
|
||||
paths = [call.args[1] for call in backend._client.request.call_args_list]
|
||||
assert paths == [
|
||||
"/v2/entities/user/org%2Fteam%3Factive%23frag/",
|
||||
"/v1/event/evt%2Fa%3Fb%23c/",
|
||||
]
|
||||
@@ -0,0 +1,49 @@
|
||||
# Documentation (`docs/`)
|
||||
|
||||
Mintlify site published at https://docs.mem0.ai.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
make docs # from repo root
|
||||
cd docs && mintlify dev
|
||||
```
|
||||
|
||||
## Structure
|
||||
|
||||
| Path | Contents |
|
||||
|------|----------|
|
||||
| `api-reference/` | Platform REST endpoints |
|
||||
| `open-source/` | Self-hosted SDK guides |
|
||||
| `platform/` | Hosted platform guides |
|
||||
| `integrations/` | One page per integration |
|
||||
| `core-concepts/` | Memory model, graph memory, scoping |
|
||||
| `cookbooks/` | End-to-end recipes |
|
||||
| `contributing/` | Contributor guides |
|
||||
| `docs.json` | Navigation tree |
|
||||
| `openapi.json` | Platform API spec |
|
||||
| `llms.txt` | Scope-tagged index for agents |
|
||||
|
||||
## Adding a page
|
||||
|
||||
Every new `.mdx` page needs three things, or CI fails:
|
||||
|
||||
1. The page itself under the right section.
|
||||
2. A navigation entry in `docs.json`.
|
||||
3. A line in `llms.txt` with a scope tag (`[Platform]`, `[OSS]`, or `[Both]`) and a description that starts with `Use when ...`.
|
||||
|
||||
`docs-llms-txt-check.yml` runs on every PR touching `docs/**/*.mdx` and **blocks the merge** when `llms.txt` is out of sync. To fix:
|
||||
|
||||
```bash
|
||||
python scripts/check-llms-txt-coverage.py --write
|
||||
```
|
||||
|
||||
That scaffolds placeholders under `## Unclassified - needs triage`. Then replace each `[TODO: ...]` tag, rewrite the descriptions as `Use when ...`, move entries into the correct section, and delete the triage heading once it is empty.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Frontmatter needs `title`, `description`, and usually `icon`.
|
||||
- Mintlify components (`<Note>`, `<Card>`, `<Tabs>`, `<CodeGroup>`) are available; prefer them over raw HTML.
|
||||
- Code samples must be runnable. If a sample calls a public SDK method, it has to match the real signature.
|
||||
- Documentation-only PRs are exempt from the `accepted`-issue requirement in the PR gate, but not from the CLA.
|
||||
- Any change to a public SDK signature has to update the matching page here in the same PR.
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
AGENTS.md
|
||||
+1
-1
@@ -24,7 +24,7 @@ mintlify dev
|
||||
|
||||
### Publishing Changes
|
||||
|
||||
Install our Github App to auto propagate changes from your repo to your deployment. Changes will be deployed to production automatically after pushing to the default branch. Find the link to install on your dashboard.
|
||||
Install our GitHub App to auto-propagate changes from your repo to your deployment. Changes will be deployed to production automatically after pushing to the default branch. Find the link to install on your dashboard.
|
||||
|
||||
#### Troubleshooting
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{/* Subtle, value-anchored nudge to star the repo. Drop in at peak-end "win" moments in the OSS docs (after a successful add/search, a server bootstrap, etc.). Keep it off the Platform/API pages. */}
|
||||
{/* Clicks are tracked via PostHog autocapture: the data-ph-capture-attribute-cta below tags each click with cta="star-on-github" so it's filterable as an event property. Metric = count of $autocapture where cta = star-on-github; break down by Current URL to see which win-moment converts. */}
|
||||
<Callout icon="star" iconType="solid" color="#FACC15">
|
||||
**Using Mem0?** <a href="https://github.com/mem0ai/mem0" data-ph-capture-attribute-cta="star-on-github">Star us on GitHub</a> to help more developers discover memory for AI apps.
|
||||
</Callout>
|
||||
@@ -97,7 +97,7 @@ Get your API key from the <a href="https://app.mem0.ai/dashboard/api-keys?utm_so
|
||||
|
||||
## Next Steps
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<CardGroup cols={3}>
|
||||
<Card title="Add Your First Memory" icon="rocket" href="/api-reference/memory/add-memories">
|
||||
Start storing memories via the REST API
|
||||
</Card>
|
||||
@@ -105,4 +105,8 @@ Get your API key from the <a href="https://app.mem0.ai/dashboard/api-keys?utm_so
|
||||
<Card title="Search with Filters" icon="filter" href="/api-reference/memory/search-memories">
|
||||
Learn advanced search and filtering techniques
|
||||
</Card>
|
||||
|
||||
<Card title="Build with cookbooks" icon="book-open" href="/cookbooks/overview">
|
||||
See the API used end to end in real projects.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Preview Dream Scope"
|
||||
description: "A no-write preview of the scope Dream synthesis would analyze for a project."
|
||||
openapi: "post /api/v1/orgs/organizations/{org_id}/projects/{project_id}/dream/preview/"
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Get Dream Activity"
|
||||
description: "Supersede/merge activity feed for a project, newest first (keyset-paginated)."
|
||||
openapi: "get /api/v1/orgs/organizations/{org_id}/projects/{project_id}/dream/activity/"
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Get Dream Configuration"
|
||||
description: "Retrieve a project's Dream (memory synthesis) configuration and plan entitlements."
|
||||
openapi: "get /api/v1/orgs/organizations/{org_id}/projects/{project_id}/dream/config/"
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Get a Synthesized Memory's Sources"
|
||||
description: "The source memories a synthesized (pattern) memory was distilled from."
|
||||
openapi: "get /api/v1/orgs/organizations/{org_id}/projects/{project_id}/dream/memory/{memory_id}/sources/"
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Get Memories in a Dream Run"
|
||||
description: "Keyset page of the synthesized memories within a single synthesis run."
|
||||
openapi: "get /api/v1/orgs/organizations/{org_id}/projects/{project_id}/dream/runs/{run_id}/memories/"
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Get Dream Synthesis Runs"
|
||||
description: "Synthesis activity grouped per run, newest first (keyset-paginated)."
|
||||
openapi: "get /api/v1/orgs/organizations/{org_id}/projects/{project_id}/dream/runs/"
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Get Dream Stats"
|
||||
description: "Lifecycle and synthesis counts for a project, plus reflection freshness."
|
||||
openapi: "get /api/v1/orgs/organizations/{org_id}/projects/{project_id}/dream/stats/"
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Update Dream Configuration"
|
||||
description: "Enable or disable Synthesis (reflection) for a project, or change the reflection mode."
|
||||
openapi: "patch /api/v1/orgs/organizations/{org_id}/projects/{project_id}/dream/config/"
|
||||
---
|
||||
@@ -4,7 +4,7 @@ description: "Add facts, messages, or metadata to a user memory store with async
|
||||
openapi: post /v3/memories/add/
|
||||
---
|
||||
|
||||
Extract and store memories from a conversation using the V3 additive pipeline. The endpoint uses single-pass ADD-only extraction — one LLM call, no UPDATE/DELETE. Memories accumulate over time; nothing is overwritten.
|
||||
Extract and store memories from a conversation using the V3 additive pipeline. The endpoint uses single-pass ADD-only extraction: one LLM call, no UPDATE/DELETE. Memories accumulate over time; nothing is overwritten.
|
||||
|
||||
## Endpoint
|
||||
|
||||
@@ -50,6 +50,7 @@ Provide conversation messages for Mem0 to extract memories from. At least one en
|
||||
| `app_id` | string | No* | Associates the memory with an app. |
|
||||
| `metadata` | object | Optional | Custom key/value metadata (e.g., `{"topic": "preferences"}`). |
|
||||
| `infer` | boolean (default `true`) | Optional | Set to `false` to skip inference and store the provided text as-is. |
|
||||
| `expiration_date` | string | Optional | Date in `YYYY-MM-DD` format. The memory is visible through this date and hidden by default after it passes. |
|
||||
|
||||
> \* At least one entity ID (`user_id`, `agent_id`, `app_id`, or `run_id`) is required.
|
||||
|
||||
@@ -64,9 +65,8 @@ The request is queued for background processing. The response contains an `event
|
||||
<CodeGroup>
|
||||
```json 200 response
|
||||
{
|
||||
"message": "Memory processing has been queued for background execution",
|
||||
"status": "PENDING",
|
||||
"event_id": "evt-uuid"
|
||||
"event_id": "evt-uuid",
|
||||
"status": "PENDING"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -83,3 +83,11 @@ The request is queued for background processing. The response contains an `event
|
||||
<Info>
|
||||
Poll the event status via `GET /v1/event/{event_id}/`. Status will be `SUCCEEDED` or `FAILED` once processing completes.
|
||||
</Info>
|
||||
|
||||
<Info>
|
||||
Memories with `expiration_date` remain stored after they expire. Search and get-all hide them by default; pass `show_expired: true` to include them.
|
||||
</Info>
|
||||
|
||||
<Info>
|
||||
Python uses `expiration_date`; TypeScript uses `expirationDate`.
|
||||
</Info>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user