[project] # Distribution name on PyPI is `mlx-memo`. Why: (1) `memo` (v0.2.4) is taken # by an unrelated package; (2) `memo-mcp` collides with milasd/memo-mcp # (a ChromaDB-based local journal MCP). The Python module / CLI binary # (`memo`) / MCP server binary (`memo-mcp`) / repo all keep their names — # only the PyPI distribution moved. Back-compat: existing users on # `memo-mcp ≤ 0.4.3` keep working; new installs use `pip install mlx-memo`. name = "mlx-memo" version = "4.16.0" description = "Local-first semantic memory for AI agents — time-travel, contradiction radar, automatic synthesis. MLX (Apple Silicon) or CPU sentence-transformers (Linux) embeddings + sqlite-vec, MCP server. No cloud, no API keys." readme = "README.md" requires-python = ">=3.13" license = { text = "MIT" } authors = [{ name = "Fernando Ferrari" }] keywords = ["mcp", "memory", "obsidian", "mlx", "rag", "agents", "claude", "local-first", "apple-silicon"] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: MacOS :: MacOS X", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering :: Artificial Intelligence", ] dependencies = [ "click>=8.1.7", "rich>=13.7", "pydantic>=2.6", "cryptography>=50.0", # Pinned to a tested range: the vec0 PARTITION KEY / metadata-column # filtering the store relies on is alpha API — cap below 0.2 to avoid # silent breakage on an upstream change. Tested against 0.1.9. "sqlite-vec>=0.1.9,<0.2", "fastmcp>=0.5,<4", # Cap at <4: 3.4.2 patches 9 CVEs "python-frontmatter>=1.1", # YAML config writers (Continue block file, Goose extensions merge) in install-mcp. "pyyaml>=6.0", # First-run interactive picker (pure Python; cross-platform). "questionary>=2.0", # Terminal-native configuration center. Runtime use is TTY-only; no Textual Web. "textual>=8.2.8,<9", "tomli-w>=1.0", # File-watcher (FSEvents on macOS). Used by `memo watch`. "watchdog>=4.0", # MLX runtime — Apple Silicon only. Cap at <1 to avoid silent breakage. "mlx>=0.18,<1; sys_platform == 'darwin' and platform_machine == 'arm64'", "mlx-lm>=0.18; sys_platform == 'darwin' and platform_machine == 'arm64'", # CPU embedder for the supported non-MLX runtime. Restrict this to Linux: # current torch releases do not provide Python 3.13 wheels for Intel macOS. "sentence-transformers>=3.0; sys_platform == 'linux'", # transformers 5.13 broke AutoTokenizer.register(str) which mlx-lm (<=0.31.3) # still calls at import — fresh installs crash on any embed. Lift the cap # once mlx-lm ships a release compatible with transformers >=5.13. "transformers<5.13; sys_platform == 'darwin' and platform_machine == 'arm64'", # StructMap: AST-based signature extraction for TS/JS (proxy context compression). "tree-sitter>=0.23,<1", "tree-sitter-typescript>=0.23,<1", "tree-sitter-javascript>=0.23,<1", ] [project.urls] Homepage = "https://github.com/jagoff/memo" Repository = "https://github.com/jagoff/memo" Issues = "https://github.com/jagoff/memo/issues" Changelog = "https://github.com/jagoff/memo/blob/master/CHANGELOG.md" [project.optional-dependencies] dev = [ # PYSEC-2026-1845 is fixed in pytest 9.0.3. "pytest>=9.0.3,<10", "pytest-cov>=5.0", "pytest-xdist>=3.6", "pytest-timeout>=2.3", "pytest-asyncio>=1.4,<2", "hypothesis>=6.158,<7", "diff-cover>=9,<11", # Starlette >=1.3 deprecates the legacy httpx TestClient transport. "httpx2>=2.5,<3", "freezegun>=1.5", "ruff==0.16.4", "mypy>=1.10", "types-PyYAML>=6.0", ] test-stability = [ "pytest-randomly>=4.1,<5", "pytest-repeat>=0.9.4,<1", ] test-mutation = [ "mutmut>=3,<4", ] # Apple Vision OCR for screenshots/images embedded in Obsidian notes. # macOS-only. Install with: pip install -e ".[ocr]" ocr = [ "pyobjc-framework-Vision>=10.0; sys_platform == 'darwin'", "pyobjc-framework-Quartz>=10.0; sys_platform == 'darwin'", ] # Backward-compatible extra name. The platform-marked core dependency above # installs sentence-transformers automatically on Linux, so the # extra intentionally adds nothing and cannot create duplicate requirements. cpu = [] # Tantivy-backed FTS: replaces FTS5 BM25 when installed. # Adds fuzzy/typo-tolerant search (mode="fuzzy") and configurable BM25. # ARM wheels available on Apple Silicon: pip install -e ".[tantivy]" tantivy = [ "tantivy>=0.22; sys_platform == 'darwin' and platform_machine == 'arm64'", ] # Local multimodal ingest: mlx-vlm captions for text-free images and # mlx-whisper audio transcription. Apple Silicon only, ingest-time only. # Install: pip install "mlx-memo[multimodal]" multimodal = [ # mlx-vlm >=0.6.5 requires transformers >=5.14, which conflicts with the # core MLX runtime cap above. Lift both caps together. "mlx-vlm>=0.1.12,<0.6.5; sys_platform == 'darwin' and platform_machine == 'arm64'", "mlx-whisper>=0.4.1; sys_platform == 'darwin' and platform_machine == 'arm64'", ] # Optional REST API server (`memo http-api`). http = [ "httpx>=0.27,<1", "pillow>=10.0", "fastapi>=0.115", "uvicorn>=0.34", ] [project.scripts] memo = "memo.cli:main" memo-mcp = "memo.server:main" [build-system] requires = ["hatchling==1.32.0"] build-backend = "hatchling.build" [tool.hatch.build] # Defense in depth for both wheel and sdist. The sdist also uses a root # allowlist below; these patterns protect package subtrees from local runtime # state and credentials accidentally created below an included directory. exclude = [ "/.claude/", "/.devin/", "/.superpowers/", "/.worktrees/", "/build/", "/dist/", # Generated by installed/dev asset sync. Canonical release assets are # force-included from the repository roots below; including this ignored # mirror can create duplicate wheel members in non-git build contexts. "/src/memo/agent_assets/", "/worktrees/", "**/.DS_Store", "**/.env", "**/.env.*", "**/.envrc", "**/__pycache__/", "**/*.db", "**/*.py[cdo]", "**/*.sqlite*", "**/config.local.*", ] skip-excluded-dirs = true [tool.hatch.build.targets.sdist] # An sdist is a build input, not a repository backup. Enumerating its release # surface prevents ignored IDE/agent state and worktree artefacts from being # published when a release is built from a dirty developer checkout. include = [ "/src/memo/", "/.agents/plugins/marketplace.json", "/.claude-plugin/marketplace.json", "/.claude-plugin/plugin.json", "/commands/memo.md", "/eval/regression_labels.json", "/hooks/hooks.json", "/plugins/memo/.codex-plugin/plugin.json", "/plugins/memo/.mcp.json", "/plugins/memo/commands/memo.md", "/skills/memo/SKILL.md", "/statusline/memo-statusline.sh", "/CHANGELOG.md", "/LICENSE", "/README.md", "/pyproject.toml", "/server.json", ] [tool.hatch.build.targets.wheel] packages = ["src/memo"] [tool.hatch.build.targets.wheel.force-include] ".agents/plugins/marketplace.json" = "memo/agent_assets/.agents/plugins/marketplace.json" ".claude-plugin/marketplace.json" = "memo/agent_assets/.claude-plugin/marketplace.json" ".claude-plugin/plugin.json" = "memo/agent_assets/.claude-plugin/plugin.json" "commands/memo.md" = "memo/agent_assets/commands/memo.md" # The tuner's curated no-regression gate reads this at runtime; without it in # the wheel the gate silently fails open on every installed runtime. "eval/regression_labels.json" = "memo/agent_assets/eval/regression_labels.json" "hooks/hooks.json" = "memo/agent_assets/hooks/hooks.json" "plugins/memo/.codex-plugin/plugin.json" = "memo/agent_assets/plugins/memo/.codex-plugin/plugin.json" "plugins/memo/.mcp.json" = "memo/agent_assets/plugins/memo/.mcp.json" "plugins/memo/commands/memo.md" = "memo/agent_assets/plugins/memo/commands/memo.md" "server.json" = "memo/agent_assets/server.json" "skills/memo/SKILL.md" = "memo/agent_assets/skills/memo/SKILL.md" "statusline/memo-statusline.sh" = "memo/agent_assets/statusline/memo-statusline.sh" [tool.pytest.ini_options] testpaths = ["tests"] addopts = ["--import-mode=importlib", "--strict-markers", "--strict-config"] asyncio_default_fixture_loop_scope = "function" markers = [ "requires_mlx: smoke test que carga modelos MLX reales (Apple Silicon only). Auto-skipea sin mlx_lm.", "slow: tests >1s (real MLX model loads); no default deselection — CI jobs pass `-m 'not slow'` explicitly (nightly slow-tests runs them).", "float32_precision: asserts an exact float32 value/representation (tight-tolerance cosine, raw blob bytes, or a float32-only model tag) that legitimately diverges under MEMO_VEC_QUANTIZE=int8; deselected by the int8 CI lane.", "db_contract: real SQLite/sqlite-vec behavioral contract.", "resource_hygiene: owns native resources and participates in the serial leak lane.", "concurrency: exercises threads, processes, sockets, locks, WAL, or interleavings.", "conformance: corpus-scale surface conformance (payload size, wall-clock, reported totals) against a ~10k synthetic corpus; own CI lane, deselected from the default lanes.", ] [tool.mypy] python_version = "3.13" [[tool.mypy.overrides]] # pyobjc (Quartz/Vision/Foundation) ship no py.typed marker. # mlx / mlx_lm (Apple-Silicon-only) and huggingface_hub are deferred, # platform-gated imports never installed on the Linux CI runner. # numpy is only pulled in by the optional consolidate path; absent in CI. module = ["frontmatter", "sqlite_vec", "Quartz", "Vision", "Foundation", "mlx", "mlx.*", "mlx_lm", "mlx_lm.*", "mlx_vlm", "mlx_vlm.*", "huggingface_hub", "numpy", "tantivy", # sentence-transformers (+torch) — Linux platform dependency; the ST # embedder import remains deferred. "sentence_transformers", "sentence_transformers.*", "torch", "torch.*", # fastapi + uvicorn — optional [http] extra for the standalone HTTP API # (server_http/cli_http), not installed in the default [dev] CI runtime. "fastapi", "fastapi.*", "uvicorn", "uvicorn.*", # Pillow — same [http] extra. The proxy's Pixel transform imports it # inside a try/except ImportError and returns None when it is absent # ("No-op without the [http] extra", see transforms/pixel.py), so a # runtime without it is a supported configuration, not a broken one. "PIL", "PIL.*"] ignore_missing_imports = true [[tool.mypy.overrides]] # Phase-1 strict: leaf modules with no intra-memo coupling. Ratchet upward # as more modules are cleaned. See plan: memo-health-roadmap. module = ["memo.config", "memo.flags", "memo.util"] disallow_untyped_defs = true disallow_any_generics = true warn_return_any = true [[tool.mypy.overrides]] # Phase-2 strict: recall hot path + core memory ops. Adding untyped-defs # coverage here catches off-by-one and None-escape bugs before they hit # the 5s hook budget. Fix all reported errors before expanding this list. module = ["memo.recall_server", "memo.memory.search_ops", "memo.memory.write_ops"] disallow_untyped_defs = true warn_return_any = true [[tool.mypy.overrides]] # Production-hardening ratchet: release, runtime startup, MCP surface, and # capture modules touched by the 2026-07-12 audit follow-up. module = ["memo.cli_release", "memo.runtime.autoupdate", "memo.server", "memo.surface", "memo.capture_core"] disallow_any_generics = true disallow_untyped_calls = true disallow_untyped_defs = true disallow_incomplete_defs = true check_untyped_defs = true disallow_untyped_decorators = true no_implicit_optional = true warn_return_any = true warn_unused_ignores = true implicit_reexport = false strict_equality = true extra_checks = true [tool.coverage.run] source = ["memo"] omit = ["*/tests/*"] branch = true [tool.coverage.report] # Branch-aware baseline ratchet. Re-measure before raising; never reduce below 74. fail_under = 74 exclude_lines = [ "pragma: no cover", "if TYPE_CHECKING:", "raise NotImplementedError", "if __name__ == .__main__.:", ] [tool.mutmut] source_paths = ["src/memo"] only_mutate = [ "src/memo/store/vec_base.py", "src/memo/memory/search_scoring_ops.py", "src/memo/session.py", "src/memo/sqlite_snapshot.py", ] pytest_add_cli_args_test_selection = [ "tests/test_vector_database_contracts.py", "tests/test_store.py", "tests/test_search_scoring_ops_unit.py", "tests/test_housekeeping_contracts.py", "tests/test_session.py", "tests/test_sqlite_cleanup.py", ] mutate_only_covered_lines = true use_setproctitle = false [tool.ruff] line-length = 100 target-version = "py313" [tool.ruff.lint] select = ["E", "F", "I", "B", "UP", "SIM", "RUF", "S"] # E501: line length handled by formatter, not enforced. # RUF002/RUF003: ambiguous-unicode in docstrings/comments — false positives on # intentional math notation (× for "N angles × M candidates") and Spanish prose. ignore = [ "E501", "RUF002", "RUF003", # S101: assert used intentionally in internal guard checks "S101", # S603: subprocess with trusted internal args (no user input in argv) "S603", # S607: partial executable paths are intentional (git, gh, memo, etc. # expected to be on PATH in the managed runtime environment) "S607", ] [tool.ruff.lint.per-file-ignores] # Tests legitimately hardcode throwaway paths/credentials and swallow errors in # best-effort cleanup — the flake8-bandit security rules are noise here, not in src. "src/memo/store/queries.py" = [ "S608", # safe: deleted_filter is hardcoded, push_clauses built by trusted caller code ] "tests/**" = [ "S105", # hardcoded "password"-looking test fixtures (fake tokens) "S106", # "password"-looking keyword args (e.g. pass_name=) in fixtures "S108", # hardcoded /tmp paths in fixtures (never real temp-file handling) "S110", # try/except/pass in test cleanup "SIM117", # nested `with patch(...)` reads cleaner than one combined with ]