[project] name = "bettermemory" version = "7.19.1" description = "Memory you can verify — a trust layer between an AI coding agent and its own past: per-hit staleness verdicts, claim-level use attribution, measured effectiveness, episodic run-state. MCP-native, local-first, MIT, no cloud." readme = "README.md" requires-python = ">=3.11" license = { text = "MIT" } authors = [{ name = "Mattias Rask" }] # PyPI's search index weights keywords; without them the package is # reachable only by exact name. keywords = [ "mcp", "model-context-protocol", "claude", "claude-code", "memory", "agent-memory", "llm", "ai-agents", ] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed", ] # BOTH halves of the mcp bound are load-bearing, for different reasons. # # The `>=2.0.0` FLOOR is the port, not a preference. mcp 2.0.0 deleted # `mcp.server.fastmcp` outright (the successor is # `mcp.server.mcpserver.MCPServer`) and no 1.x ships `mcp.server.mcpserver` # — there is NO overlap version, so a package importing a server class has # to pick a side. `builder.py`, `handlers/_shared.py` and `session.py` # import from the 2.x path and will not run on 1.x. A floor is a real # install-compat break, so it moved once, deliberately, announced in the # changelog — never as a side effect. Users who need 1.x pin # `bettermemory<3.33.0`, which is a workaround that genuinely exists; the # defect in docs/incidents/2026-07-31-mcp-2-unbounded-constraint.md left no # working pin at all, which is the whole asymmetry. # # The `<3.0.0` CAP is that incident's lesson, kept armed. It was unbounded # when mcp 2.0.0 landed; a fresh `pip install bettermemory` resolved the new # major and died at import, while every CI leg stayed green because they all # install from `uv.lock`. The `install from declared constraints` job in # ci.yml is the guard that closes that blind spot — it resolves WITHOUT the # lockfile, so the next upstream major goes red on an ordinary push instead # of on a user's machine. Lift this cap only together with the next port. # # The `<3` cap on pydantic is now the ONLY pydantic ceiling we have, and the # port is what made that true. Under mcp 1.x we were protected from a # pydantic 3.0 release transitively, by mcp's own `pydantic<3.0.0` bound; # mcp 2.0.0 declares `pydantic>=2.12.0` with no upper bound of its own, so # that inherited ceiling is gone and this line is load-bearing on its own. # The floor stays at the version THIS code needs (2.0); the effective # resolved floor is 2.12.0, raised by mcp, and is not restated here so the # constraint keeps expressing our requirement rather than mirroring a # dependency's. dependencies = [ "mcp>=2.0.0,<3.0.0", "pydantic>=2.0,<3.0.0", "pyyaml>=6.0", "platformdirs>=4.0", ] [project.optional-dependencies] dev = [ "pytest>=7.0", "pytest-asyncio>=0.21", "pytest-cov>=4.0", "ruff>=0.1", "mypy>=1.10", "pyright>=1.1.400", "types-PyYAML>=6.0", # Property-based store invariants (tests/test_store_properties.py). # Pinned only on the lower bound; any reasonably modern hypothesis # has the strategies we use. "hypothesis>=6.0", # `llm.OllamaProvider` lazy-imports httpx at call time; dev carries # it so the provider's tests exercise the real client. Not a runtime # dependency — a clean install without dev degrades with an install # hint at the call site. "httpx>=0.27", ] [project.scripts] bettermemory = "bettermemory.server:main" [project.urls] # Surfaced as the "Project links" rail on PyPI — without these the # package page has no path back to source, issues, or release notes. Homepage = "https://github.com/0Mattias/bettermemory" Repository = "https://github.com/0Mattias/bettermemory" Issues = "https://github.com/0Mattias/bettermemory/issues" Changelog = "https://github.com/0Mattias/bettermemory/blob/main/CHANGELOG.md" [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = ["src/bettermemory"] [tool.hatch.build.targets.sdist] # Explicit exclude — two entry classes. Most are untracked dev/test # caches: hatch defaults to `git ls-files` when in a repo, but a # release built from a clean checkout of an unindexed tree (e.g. a CI # runner that does `git clone --filter=blob:none && uv build`) falls # back to "everything not gitignored", and the gitignore alone isn't a # hard guarantee — spell out the cruft so the sdist on PyPI stays # tight and reproducible. `.claude` is the highest-stakes cache: # session scratch plus Workflow-agent worktrees — full untracked # copies of the whole repo that must never sweep into a published # sdist. `.envrc` is the one TRACKED entry (shared direnv config for # the iCloud .venv workaround, ce01f40): its exclude is load-bearing # on the default `git ls-files` path, keeping it in git but out of # the sdist. Do NOT gitignore or `git rm --cached` it. exclude = [ ".hypothesis", ".mypy_cache", ".ruff_cache", ".pytest_cache", ".coverage*", ".DS_Store", ".envrc", ".claude", "dist", "venv", ".venv", "htmlcov", ] [tool.mypy] # Strict on the package itself — surfaces missing annotations and unused # ignores immediately. Tests are looser: pytest fixtures and parametrize # generate enough Any-typed scaffolding that strict-on-tests is mostly # noise without much safety win. files = ["src/bettermemory", "tests"] python_version = "3.11" strict = true # Pydantic v2 ships its own mypy plugin for BaseModel field validation. plugins = ["pydantic.mypy"] [[tool.mypy.overrides]] module = "tests.*" disallow_untyped_defs = false disallow_incomplete_defs = false disallow_untyped_decorators = false warn_return_any = false # Pytest fixture composition produces a lot of "missing type arguments # for generic dict" noise — silence it for tests only. disable_error_code = ["type-arg", "no-untyped-def", "no-untyped-call"] [[tool.mypy.overrides]] # Optional runtime SDKs. `httpx` ships in the [dev] extra but # `bettermemory.llm` reaches for it inside `OllamaProvider.propose()` so we # don't crash imports on a clean install missing dev. `anthropic` / `openai` # are runtime-optional SDKs guarded by the same lazy-import pattern. module = [ "httpx", "anthropic", "openai", "mem0", "mem0.*", ] ignore_missing_imports = true [[tool.mypy.overrides]] # `jsonschema` ships no inline types and `types-jsonschema` is not in the # dev extra. It is NOT optional the way the block above is — `mcp` declares # `jsonschema>=4.20.0` unconditionally, so it is present wherever this # package can even be imported. It appears in exactly one place: # `tests/test_schema_title_scrub.py` re-runs the validation that # `mcp/client/session.py` performs on every structured tool result, which # is the only honest way to show that scrubbing schema `title` annotations # does not change what a client accepts or rejects. Pulling in a stub # package to type three call sites of `validate()` would cost a dependency # for no safety. module = ["jsonschema"] ignore_missing_imports = true [tool.pyright] # Secondary type gate alongside mypy (which stays the strict primary). # Scoped to the package: the test suite leans on dynamic pytest fixture / # monkeypatch patterns that pyright's standard mode flags as noise with no # safety win — the same reason the mypy config loosens `tests.*`. include = ["src/bettermemory"] extraPaths = ["src"] # Type-check against the uv-managed dev venv at `.venv`, which CI's # `uv sync --extra dev` populates in-repo. # # On a checkout inside a cloud-synced folder the venv lives OUTSIDE the # repo (see `.envrc`), so there is no in-repo `.venv` for pyright to find # and every import resolves as missing. The fix there is a `.venv` # symlink pointing at it — `.gitignore` matches `.venv` without a # trailing slash precisely so the symlink form is ignored too and can # never be committed as a machine-specific path. A symlink also keeps the # synced folder holding 0 bytes rather than the environment itself. venvPath = "." venv = ".venv" pythonVersion = "3.11" typeCheckingMode = "standard" reportMissingImports = "error" # Optional runtime deps (anthropic, openai) are lazy-imported inside # try/except ImportError and # aren't installed in the lint env. Their import sites carry targeted # `# pyright: ignore[reportMissingImports]`, mirroring the mypy # `ignore_missing_imports` override above for the same modules. # # reportUnreachable stays off deliberately: the handlers carry # defense-in-depth `isinstance` / None guards at the MCP-JSON boundary # (e.g. verify.py, acknowledge_miss.py) that the static type system proves # "unreachable" but which validate runtime input the types can't enforce. # This matches the mypy config, which likewise does not enable # `warn_unreachable`. reportUnreachable = "none" [tool.pytest.ini_options] asyncio_mode = "auto" testpaths = ["tests"] # Surface DeprecationWarnings from our own code as errors. We do NOT silence # the python-frontmatter codecs.open() deprecation — instead we vendored our # own frontmatter handling in store.py to drop the dependency entirely. # # Two lines, two axes — both are needed: # # - The module-scoped line keys on the module the warning is ATTRIBUTED to. # Our deprecations warn with stacklevel=2, attributing the warning to the # CALLER's frame — so it escalates deprecated-API use inside bettermemory # itself (src/-frame callers), but an unwrapped deprecated call made from a # TEST module is attributed to `tests.test_*`, never matches, and sails # through as a plain warning ("1 passed, 1 warning"). # - The message-scoped line closes that gap: every bettermemory deprecation # message contains "deprecated and will be removed in bettermemory", so it # escalates regardless of the caller's frame. The message field is a regex # matched (re.match) against str(warning) — third-party deprecation texts # don't name bettermemory, so they stay untouched. # # The fence is pinned in tests/test_origin.py ("Deprecation fence" section): # both line literals, the regex-vs-emitted-messages match, and a subprocess # probe proving an unwrapped test-frame call errors under this exact config. filterwarnings = [ "error::DeprecationWarning:bettermemory", "error:.*deprecated and will be removed in bettermemory:DeprecationWarning", ] [tool.ruff] # Generated bench artifacts are committed with the exact bytes their run # JSONs and gate artifacts pin by sha256 — the format gate is for # hand-written code, and reformatting a generated table would break the # sha chain that makes its reads auditable. The emitters own their style. extend-exclude = ["bench/w/artifacts/w3c_table_*.py"] [tool.ruff.lint.per-file-ignores] # conftest.py does sys.path manipulation before imports as a belt-and-suspenders # fix for the iCloud UF_HIDDEN .venv issue — see the comment in conftest.py. "tests/conftest.py" = ["E402"]