mlx-memo 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mlx_memo-0.5.0/.claude-plugin/marketplace.json +17 -0
- mlx_memo-0.5.0/.claude-plugin/plugin.json +13 -0
- mlx_memo-0.5.0/.github/workflows/test.yml +34 -0
- mlx_memo-0.5.0/.gitignore +39 -0
- mlx_memo-0.5.0/.mcp.json +10 -0
- mlx_memo-0.5.0/CHANGELOG.md +301 -0
- mlx_memo-0.5.0/CLAUDE.md +124 -0
- mlx_memo-0.5.0/LICENSE +21 -0
- mlx_memo-0.5.0/PKG-INFO +540 -0
- mlx_memo-0.5.0/README.md +503 -0
- mlx_memo-0.5.0/docs/architecture.svg +162 -0
- mlx_memo-0.5.0/docs/tui-dashboard.png +0 -0
- mlx_memo-0.5.0/hooks/hooks.json +58 -0
- mlx_memo-0.5.0/integrations/paperclip-plugin-memo/.gitignore +3 -0
- mlx_memo-0.5.0/integrations/paperclip-plugin-memo/README.md +71 -0
- mlx_memo-0.5.0/integrations/paperclip-plugin-memo/esbuild.config.mjs +17 -0
- mlx_memo-0.5.0/integrations/paperclip-plugin-memo/package.json +48 -0
- mlx_memo-0.5.0/integrations/paperclip-plugin-memo/pnpm-lock.yaml +1242 -0
- mlx_memo-0.5.0/integrations/paperclip-plugin-memo/rollup.config.mjs +28 -0
- mlx_memo-0.5.0/integrations/paperclip-plugin-memo/scripts/install.sh +24 -0
- mlx_memo-0.5.0/integrations/paperclip-plugin-memo/src/manifest.ts +167 -0
- mlx_memo-0.5.0/integrations/paperclip-plugin-memo/src/ui/index.tsx +44 -0
- mlx_memo-0.5.0/integrations/paperclip-plugin-memo/src/worker.ts +275 -0
- mlx_memo-0.5.0/integrations/paperclip-plugin-memo/tests/plugin.spec.ts +51 -0
- mlx_memo-0.5.0/integrations/paperclip-plugin-memo/tsconfig.json +27 -0
- mlx_memo-0.5.0/integrations/paperclip-plugin-memo/vitest.config.ts +8 -0
- mlx_memo-0.5.0/pyproject.toml +79 -0
- mlx_memo-0.5.0/scripts/audit-data-integrity.py +121 -0
- mlx_memo-0.5.0/scripts/clean-ingest-leaks.py +64 -0
- mlx_memo-0.5.0/scripts/migrate-from-mem-vault.py +176 -0
- mlx_memo-0.5.0/skills/memo/SKILL.md +251 -0
- mlx_memo-0.5.0/src/memo/__init__.py +30 -0
- mlx_memo-0.5.0/src/memo/agent.py +377 -0
- mlx_memo-0.5.0/src/memo/analytics.py +332 -0
- mlx_memo-0.5.0/src/memo/capture.py +364 -0
- mlx_memo-0.5.0/src/memo/chunker.py +210 -0
- mlx_memo-0.5.0/src/memo/cli.py +5045 -0
- mlx_memo-0.5.0/src/memo/cognitive.py +419 -0
- mlx_memo-0.5.0/src/memo/collaborative.py +451 -0
- mlx_memo-0.5.0/src/memo/config.py +279 -0
- mlx_memo-0.5.0/src/memo/consolidation.py +350 -0
- mlx_memo-0.5.0/src/memo/contextual.py +289 -0
- mlx_memo-0.5.0/src/memo/crossref.py +308 -0
- mlx_memo-0.5.0/src/memo/dashboard.py +506 -0
- mlx_memo-0.5.0/src/memo/embedder.py +261 -0
- mlx_memo-0.5.0/src/memo/encryption.py +381 -0
- mlx_memo-0.5.0/src/memo/federation.py +242 -0
- mlx_memo-0.5.0/src/memo/graph.py +253 -0
- mlx_memo-0.5.0/src/memo/history.py +160 -0
- mlx_memo-0.5.0/src/memo/import_export.py +338 -0
- mlx_memo-0.5.0/src/memo/lifecycle.py +342 -0
- mlx_memo-0.5.0/src/memo/llm.py +144 -0
- mlx_memo-0.5.0/src/memo/memory.py +1535 -0
- mlx_memo-0.5.0/src/memo/multimodal.py +495 -0
- mlx_memo-0.5.0/src/memo/navigation.py +372 -0
- mlx_memo-0.5.0/src/memo/proactive.py +247 -0
- mlx_memo-0.5.0/src/memo/project.py +71 -0
- mlx_memo-0.5.0/src/memo/queries.py +247 -0
- mlx_memo-0.5.0/src/memo/reranker.py +216 -0
- mlx_memo-0.5.0/src/memo/server.py +1846 -0
- mlx_memo-0.5.0/src/memo/session.py +401 -0
- mlx_memo-0.5.0/src/memo/setup/__init__.py +26 -0
- mlx_memo-0.5.0/src/memo/setup/config_io.py +81 -0
- mlx_memo-0.5.0/src/memo/setup/picker.py +109 -0
- mlx_memo-0.5.0/src/memo/setup/vaults.py +72 -0
- mlx_memo-0.5.0/src/memo/sharing.py +459 -0
- mlx_memo-0.5.0/src/memo/store.py +455 -0
- mlx_memo-0.5.0/src/memo/sync.py +300 -0
- mlx_memo-0.5.0/src/memo/temporal.py +394 -0
- mlx_memo-0.5.0/src/memo/transcript_miner.py +238 -0
- mlx_memo-0.5.0/src/memo/versioning.py +261 -0
- mlx_memo-0.5.0/src/memo/watcher.py +213 -0
- mlx_memo-0.5.0/tests/__init__.py +0 -0
- mlx_memo-0.5.0/tests/conftest.py +59 -0
- mlx_memo-0.5.0/tests/test_agent.py +170 -0
- mlx_memo-0.5.0/tests/test_analytics.py +166 -0
- mlx_memo-0.5.0/tests/test_capture.py +260 -0
- mlx_memo-0.5.0/tests/test_cli_init.py +98 -0
- mlx_memo-0.5.0/tests/test_cli_install_wrapper.py +113 -0
- mlx_memo-0.5.0/tests/test_cli_migrate_vault.py +123 -0
- mlx_memo-0.5.0/tests/test_cognitive.py +271 -0
- mlx_memo-0.5.0/tests/test_collaborative.py +276 -0
- mlx_memo-0.5.0/tests/test_config.py +120 -0
- mlx_memo-0.5.0/tests/test_consolidation.py +207 -0
- mlx_memo-0.5.0/tests/test_contextual.py +237 -0
- mlx_memo-0.5.0/tests/test_crossref.py +228 -0
- mlx_memo-0.5.0/tests/test_dashboard.py +129 -0
- mlx_memo-0.5.0/tests/test_encryption.py +266 -0
- mlx_memo-0.5.0/tests/test_federation.py +227 -0
- mlx_memo-0.5.0/tests/test_import_export.py +190 -0
- mlx_memo-0.5.0/tests/test_lifecycle.py +256 -0
- mlx_memo-0.5.0/tests/test_memory.py +715 -0
- mlx_memo-0.5.0/tests/test_memory_project_tag.py +73 -0
- mlx_memo-0.5.0/tests/test_multimodal.py +250 -0
- mlx_memo-0.5.0/tests/test_navigation.py +251 -0
- mlx_memo-0.5.0/tests/test_proactive.py +149 -0
- mlx_memo-0.5.0/tests/test_project.py +69 -0
- mlx_memo-0.5.0/tests/test_queries.py +270 -0
- mlx_memo-0.5.0/tests/test_reranker.py +174 -0
- mlx_memo-0.5.0/tests/test_server.py +92 -0
- mlx_memo-0.5.0/tests/test_session.py +344 -0
- mlx_memo-0.5.0/tests/test_setup_config_io.py +69 -0
- mlx_memo-0.5.0/tests/test_setup_vaults.py +80 -0
- mlx_memo-0.5.0/tests/test_sharing.py +293 -0
- mlx_memo-0.5.0/tests/test_smoke_mlx.py +101 -0
- mlx_memo-0.5.0/tests/test_store.py +125 -0
- mlx_memo-0.5.0/tests/test_sync.py +169 -0
- mlx_memo-0.5.0/tests/test_temporal.py +197 -0
- mlx_memo-0.5.0/tests/test_transcript_miner.py +100 -0
- mlx_memo-0.5.0/tests/test_versioning.py +258 -0
- mlx_memo-0.5.0/tests/test_watcher.py +71 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
|
3
|
+
"name": "memo",
|
|
4
|
+
"description": "Local MCP memory for AI agents on Apple Silicon. MLX-native, sqlite-vec, markdown-on-disk.",
|
|
5
|
+
"owner": {
|
|
6
|
+
"name": "Fernando Ferrari",
|
|
7
|
+
"url": "https://github.com/jagoff"
|
|
8
|
+
},
|
|
9
|
+
"plugins": [
|
|
10
|
+
{
|
|
11
|
+
"name": "memo",
|
|
12
|
+
"description": "Persistent semantic memory. Slash command `/memo` + MCP tools `mcp__memo__memory_*`.",
|
|
13
|
+
"source": "./",
|
|
14
|
+
"category": "memory"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "memo",
|
|
3
|
+
"description": "Local MCP memory for AI agents — MLX-native on Apple Silicon, sqlite-vec store, markdown-on-disk in your Obsidian vault. Zero Ollama, zero cloud APIs.",
|
|
4
|
+
"version": "0.5.0",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Fernando Ferrari",
|
|
7
|
+
"url": "https://github.com/jagoff"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/jagoff/memo",
|
|
10
|
+
"repository": "https://github.com/jagoff/memo",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": ["memory", "mcp", "mlx", "obsidian", "rag", "agents", "local-first"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [master]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
# Linux runner is enough — `mlx`/`mlx-lm` deps are gated to
|
|
12
|
+
# `darwin/arm64` in pyproject.toml and the real-MLX smoke tests
|
|
13
|
+
# auto-skip via the `requires_mlx` marker (see conftest.py) when
|
|
14
|
+
# `mlx_lm` isn't importable.
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
strategy:
|
|
17
|
+
matrix:
|
|
18
|
+
python-version: ["3.13"]
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
22
|
+
uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: ${{ matrix.python-version }}
|
|
25
|
+
- name: Install uv
|
|
26
|
+
run: pip install uv
|
|
27
|
+
- name: Install package + dev deps
|
|
28
|
+
run: |
|
|
29
|
+
uv venv .venv
|
|
30
|
+
uv pip install -e '.[dev]'
|
|
31
|
+
- name: Lint (ruff)
|
|
32
|
+
run: .venv/bin/ruff check src/ tests/
|
|
33
|
+
- name: Tests (excluding slow real-MLX smoke)
|
|
34
|
+
run: .venv/bin/python -m pytest -q -m "not slow"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
*.egg
|
|
6
|
+
.pytest_cache/
|
|
7
|
+
.mypy_cache/
|
|
8
|
+
.ruff_cache/
|
|
9
|
+
.coverage
|
|
10
|
+
htmlcov/
|
|
11
|
+
|
|
12
|
+
# Build artifacts
|
|
13
|
+
build/
|
|
14
|
+
dist/
|
|
15
|
+
dist.v*/
|
|
16
|
+
|
|
17
|
+
# Environments
|
|
18
|
+
.venv/
|
|
19
|
+
venv/
|
|
20
|
+
.env
|
|
21
|
+
.env.local
|
|
22
|
+
|
|
23
|
+
# Local DB / state
|
|
24
|
+
*.db
|
|
25
|
+
*.db-shm
|
|
26
|
+
*.db-wal
|
|
27
|
+
|
|
28
|
+
# Editor / IDE / OS
|
|
29
|
+
.DS_Store
|
|
30
|
+
.claude/
|
|
31
|
+
.windsurf/
|
|
32
|
+
.idea/
|
|
33
|
+
.vscode/
|
|
34
|
+
|
|
35
|
+
# Node (integrations)
|
|
36
|
+
node_modules/
|
|
37
|
+
integrations/**/node_modules/
|
|
38
|
+
integrations/**/dist/
|
|
39
|
+
integrations/**/.paperclip-sdk/
|
mlx_memo-0.5.0/.mcp.json
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.5.0] - 2026-05-12
|
|
9
|
+
|
|
10
|
+
PyPI dist rename + TUI follow-ups (q/ESC quit, smaller layout).
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **PyPI distribution renamed `memo-mcp` → `mlx-memo`.** The previous
|
|
15
|
+
name collided with [`milasd/memo-mcp`](https://github.com/milasd/memo-mcp)
|
|
16
|
+
(a ChromaDB-backed journal MCP) and risked install ambiguity for new
|
|
17
|
+
users. The Python module (`memo`), CLI binary (`memo`), MCP server
|
|
18
|
+
binary (`memo-mcp`), and the GitHub repo all keep their names — only
|
|
19
|
+
the PyPI dist moved. Existing `pip install memo-mcp ≤ 0.4.3` users
|
|
20
|
+
keep working; new installs use `pip install mlx-memo`. README +
|
|
21
|
+
CLAUDE.md updated, badges repointed.
|
|
22
|
+
|
|
23
|
+
### Added (0.4.2 + 0.4.3 follow-ups, merged into 0.5.0)
|
|
24
|
+
|
|
25
|
+
- **`q` / `ESC` exit** in `memo tui`. Background stdin reader in
|
|
26
|
+
cbreak mode sets a stop event; main loop polls. Falls back to
|
|
27
|
+
Ctrl+C-only when stdin isn't a TTY. Footer advertises the keys.
|
|
28
|
+
- **TUI layout shrunk to ~18 rows**. Removed the panel-framed hero
|
|
29
|
+
(now an inline footer status line); corpus and runtime collapsed
|
|
30
|
+
to single-line summaries.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- **Legacy-path warning** (`stored path(s) don't resolve…`) now
|
|
35
|
+
silenced inside `memo tui` via `MEMO_SUPPRESS_LEGACY_WARN=1`. The
|
|
36
|
+
user can't act on it from inside the alt screen anyway. Outside the
|
|
37
|
+
TUI the message was rephrased ("heads-up: tu índice apunta a paths
|
|
38
|
+
antiguos") and explicitly labeled as not-an-error.
|
|
39
|
+
|
|
40
|
+
## [0.4.1] - 2026-05-12
|
|
41
|
+
|
|
42
|
+
Adds a live terminal dashboard and the recall-log plumbing that feeds it.
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- **`memo tui`** — live, colored Rich-Live dashboard. Six panels:
|
|
47
|
+
corpus stats (totals + per-type breakdown + project count), runtime
|
|
48
|
+
(MLX warm/cold flags for embedder / reranker / chat, vault size,
|
|
49
|
+
watcher status from `launchctl print`), recent saves (last 10 from
|
|
50
|
+
`history.db`), recent recalls (last 8 from the new recall log), top
|
|
51
|
+
tags (project tags highlighted), and 14-day saves/recalls
|
|
52
|
+
sparklines (`▁▂▃▄▅▆▇█`). Refresh `--refresh N` (default 1.0 s).
|
|
53
|
+
Ctrl+C to exit. Zero new deps — Rich was already in.
|
|
54
|
+
- **Recall log JSONL** at `~/.local/share/memo/recall.log`. The
|
|
55
|
+
`memo recall-hook` appends `{ts, prompt, hits[]}` per invocation
|
|
56
|
+
(best-effort, failures swallowed). Auto-rotates at ~200 KB to the
|
|
57
|
+
last 200 entries. Powers the TUI's recall panel; failures here can
|
|
58
|
+
never affect the hook's output.
|
|
59
|
+
- **`/memo tui`** routing in `skills/memo/SKILL.md` (along with
|
|
60
|
+
`/memo watch`, `/memo install-watcher`, `/memo mine-history`).
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
|
|
64
|
+
- New module `memo.dashboard` (~370 lines). Public API:
|
|
65
|
+
`run_tui`, `render`, `sparkline`, `append_recall_log`,
|
|
66
|
+
`read_recall_log`.
|
|
67
|
+
|
|
68
|
+
## [0.4.0] - 2026-05-12
|
|
69
|
+
|
|
70
|
+
Minor release — five "gamechanger" features land alongside a major
|
|
71
|
+
repo-hygiene pass to make the project public-ready.
|
|
72
|
+
|
|
73
|
+
### Added
|
|
74
|
+
|
|
75
|
+
- **Project-scoped recall.** `memo save` now auto-attaches a
|
|
76
|
+
`project:<repo>` tag derived from the git toplevel of the caller's
|
|
77
|
+
cwd (or the `MEMO_PROJECT_TAG` env var). The recall hook reads `cwd`
|
|
78
|
+
from the Claude Code hook payload and additively boosts the score of
|
|
79
|
+
memorias that share the current project tag by
|
|
80
|
+
`MEMO_RECALL_PROJECT_BOOST` (default `0.15`). Opt out per-call with
|
|
81
|
+
`memo save --no-project-tag` or globally with
|
|
82
|
+
`MEMO_AUTO_PROJECT_TAG=0`. New module: `memo.project`.
|
|
83
|
+
- **Token-budget-aware recall.** `MEMO_RECALL_TOKEN_BUDGET` (default
|
|
84
|
+
`0` = off) packs memorias greedily by score until the budget is
|
|
85
|
+
reached; the final memoria gets body-truncated to fit instead of
|
|
86
|
+
being dropped wholesale. Token estimate is `len(text)//4` — no
|
|
87
|
+
tiktoken dep.
|
|
88
|
+
- **`memo mine-history`** — bulk-mine past Claude Code transcripts
|
|
89
|
+
under `~/.claude/projects/<hash>/*.jsonl` for insights, running the
|
|
90
|
+
same prefilter → helper-LLM extract → embedding-dedup pipeline as
|
|
91
|
+
the live capture hook. Resumable per-file via
|
|
92
|
+
`~/.local/share/memo/mine-history.json`. Flags:
|
|
93
|
+
`--path / --since / --limit / --dry-run / --debug / --json`.
|
|
94
|
+
New module: `memo.transcript_miner`.
|
|
95
|
+
- **MCP resources** — `memo://recent` (top-20 by `updated` desc, with
|
|
96
|
+
per-memoria `memo://memory/<id>` links) and `memo://memory/{id}`
|
|
97
|
+
(full record, accepts prefix ≥4 chars). Clients can pin / drag
|
|
98
|
+
memorias into context without paying tool-call overhead per access.
|
|
99
|
+
- **`memo watch`** — file-watcher daemon. Watches `cfg.memory_dir`
|
|
100
|
+
recursively via `watchdog`/FSEvents and triggers a debounced
|
|
101
|
+
`Memory.reindex()` (`--delay` configurable, default 2 s) when `.md`
|
|
102
|
+
files are modified. Bundled installer:
|
|
103
|
+
- `memo install-watcher` writes
|
|
104
|
+
`~/Library/LaunchAgents/com.fer.memo.watch.plist`, loads it via
|
|
105
|
+
`launchctl bootstrap`, and verifies. Logs to
|
|
106
|
+
`~/Library/Logs/memo/`. `KeepAlive=true`.
|
|
107
|
+
- `memo uninstall-watcher` boots out the job and removes the plist.
|
|
108
|
+
New module: `memo.watcher`. New dep: `watchdog>=4.0`.
|
|
109
|
+
|
|
110
|
+
### Changed
|
|
111
|
+
|
|
112
|
+
- **README rewritten for a public audience.** Cleaner hero, expanded
|
|
113
|
+
alternatives matrix (mem0 / letta / cognee / supermemory / mem-vault
|
|
114
|
+
/ MCP-memory reference / engram) with the seven differentiators
|
|
115
|
+
spelled out in plain terms, and an updated `docs/architecture.svg`
|
|
116
|
+
that covers clients → MCP/CLI → core → MLX → storage rather than
|
|
117
|
+
just the recall pipeline.
|
|
118
|
+
- **`.gitignore` expanded** to cover `dist.v*/`, `.claude/`,
|
|
119
|
+
`.windsurf/`, `integrations/**/node_modules/`,
|
|
120
|
+
`integrations/**/dist/`, `integrations/**/.paperclip-sdk/`, and
|
|
121
|
+
common IDE dirs.
|
|
122
|
+
- **Test fixture default** now sets `MEMO_AUTO_PROJECT_TAG=0` so tests
|
|
123
|
+
asserting exact tag sets aren't polluted by the cwd-derived
|
|
124
|
+
project tag. Tests exercising the auto-tag flow opt back in
|
|
125
|
+
explicitly via `monkeypatch.setenv`.
|
|
126
|
+
|
|
127
|
+
### Removed
|
|
128
|
+
|
|
129
|
+
- Stale `dist.v0.2.0/`, `dist.v0.3.0/`, `dist.v0.3.1/` build snapshots
|
|
130
|
+
and accumulated `.DS_Store` files from the repo. `dist/` was already
|
|
131
|
+
gitignored; the dated variants weren't.
|
|
132
|
+
|
|
133
|
+
## [0.3.3] - 2026-05-08
|
|
134
|
+
|
|
135
|
+
Patch release — install-from-git fixes surfaced while validating the
|
|
136
|
+
distributed-install flow on a clean machine.
|
|
137
|
+
|
|
138
|
+
### Fixed
|
|
139
|
+
|
|
140
|
+
- **`memo --version` crashed** with `RuntimeError: 'memo' is not
|
|
141
|
+
installed. Try passing 'package_name' instead.` because click's
|
|
142
|
+
`version_option` defaulted to `package_name="memo"` while the actual
|
|
143
|
+
PyPI/wheel dist is `memo-mcp`. Pinned the lookup explicitly.
|
|
144
|
+
- **`DEFAULT_MEMORY_SUBDIR` pointed at the deprecated archive path**
|
|
145
|
+
(`04-Archive/99-obsidian-system/99-AI/memory`). Updated to the
|
|
146
|
+
current `99-obsidian/99-AI/memory` location, matching the user-facing
|
|
147
|
+
vault reorganization done on 2026-05-08. Existing installs that
|
|
148
|
+
override via `MEMO_MEMORY_SUBDIR` are unaffected.
|
|
149
|
+
|
|
150
|
+
## [0.3.2] - 2026-05-07
|
|
151
|
+
|
|
152
|
+
Patch release — BM25 recall fix.
|
|
153
|
+
|
|
154
|
+
### Fixed
|
|
155
|
+
|
|
156
|
+
- **BM25 search wrapped query in phrase quotes** (`"foo bar"`) which
|
|
157
|
+
required the words to appear consecutively. This killed recall on
|
|
158
|
+
natural multi-word Spanish queries:
|
|
159
|
+
- `"Astor terapia ocupacional"` did NOT match the document titled
|
|
160
|
+
`"Informe Terapia Ocupacional — Astor Ferrari"` because the words
|
|
161
|
+
don't appear in that exact consecutive order.
|
|
162
|
+
|
|
163
|
+
Fix: tokenize via `\w+` (Unicode-aware), wrap each token in its own
|
|
164
|
+
phrase quotes, join with whitespace (FTS5's implicit AND). Now the
|
|
165
|
+
query is `"Astor" "terapia" "ocupacional"` — matches any doc
|
|
166
|
+
containing all 3 words anywhere, any order.
|
|
167
|
+
|
|
168
|
+
### Verified
|
|
169
|
+
|
|
170
|
+
Same query post-fix:
|
|
171
|
+
- BM25: `Astor — Informe Terapia Ocupacional feb 2026` returns first hit ✓
|
|
172
|
+
- Hybrid (vec+BM25 RRF): same doc in top-3 ✓
|
|
173
|
+
|
|
174
|
+
Other corpus queries that benefit:
|
|
175
|
+
- `MLX migration` → `obsidian-rag: migración Ollama → MLX` (was missing pre-fix)
|
|
176
|
+
- `obsidian-rag bug fix` → `Bug pattern — sqlite3 'database is locked'` (was missing pre-fix)
|
|
177
|
+
|
|
178
|
+
## [0.3.1] - 2026-05-07
|
|
179
|
+
|
|
180
|
+
Patch release — bug fixes for the v0.3.0 ingest pipeline.
|
|
181
|
+
|
|
182
|
+
### Added
|
|
183
|
+
|
|
184
|
+
- `memo ingest <vault-path>` CLI command — bulk-ingest .md files from any
|
|
185
|
+
Obsidian vault into the memo index. Synthesizes ids from path hash so
|
|
186
|
+
user .md files are not modified. Idempotent. See README "Ambient memory"
|
|
187
|
+
section for usage.
|
|
188
|
+
- `MEMO_INGEST_MIN_CHARS` env var (default 200) — skip notes shorter than
|
|
189
|
+
this threshold. Tag-only stubs (`#tagA #tagB` + 1-line question)
|
|
190
|
+
produce noisy embeddings near the corpus centroid that match generic
|
|
191
|
+
queries with high false-positive rate. Filtering them improves recall
|
|
192
|
+
precision on queries with proper nouns.
|
|
193
|
+
|
|
194
|
+
### Fixed
|
|
195
|
+
|
|
196
|
+
- **Embedder API misuse** in `recall-hook`, `prewarm`, and `ingest`:
|
|
197
|
+
`MLXEmbedder.embed()` is batched (signature `Sequence[str] →
|
|
198
|
+
list[list[float]]`); passing a bare string iterated per-char,
|
|
199
|
+
producing variable-dim outputs (135, 512, 2465...) instead of 1024.
|
|
200
|
+
Cascade Metal GPU error after several mismatches. Fix: wrap input
|
|
201
|
+
in `[composed]` and take `[0]`.
|
|
202
|
+
- **Plugin install path** — bumping from 0.3.0 to 0.3.1 so users on the
|
|
203
|
+
Claude Code plugin marketplace pick up these fixes via `/plugin update`.
|
|
204
|
+
|
|
205
|
+
## [0.3.0] - 2026-05-07
|
|
206
|
+
|
|
207
|
+
**Game-changer release**: memo turns into an *ambient* context layer.
|
|
208
|
+
Memorias auto-inject as `additionalContext` on every Claude Code prompt,
|
|
209
|
+
without the user invoking `/memo` at all.
|
|
210
|
+
|
|
211
|
+
### Added
|
|
212
|
+
|
|
213
|
+
- `memo recall-hook` CLI command — Claude Code `UserPromptSubmit` hook that
|
|
214
|
+
embeds the user prompt via the MLX embedder, runs vec-only search against
|
|
215
|
+
the memo index, and emits relevant memorias as `additionalContext` markdown
|
|
216
|
+
on stdout. Sub-1.7s warm latency on a 223-doc corpus, well within the
|
|
217
|
+
default 5s hook timeout.
|
|
218
|
+
- `memo prewarm` CLI command — `SessionStart` hook that pre-loads the MLX
|
|
219
|
+
embedder so the first `recall-hook` invocation of the session is fast
|
|
220
|
+
(warm load: ~500ms vs ~2s cold).
|
|
221
|
+
- `hooks/hooks.json` bundled in the plugin — auto-wires the two hooks above
|
|
222
|
+
when users install via `/plugin install memo@memo`.
|
|
223
|
+
- 6 env vars to tune ambient memory behaviour:
|
|
224
|
+
- `MEMO_RECALL_DISABLE` — kill switch (default: enabled).
|
|
225
|
+
- `MEMO_RECALL_TOP_K` — default 3.
|
|
226
|
+
- `MEMO_RECALL_MIN_SIM` — cosine similarity floor, default 0.6.
|
|
227
|
+
- `MEMO_RECALL_MIN_PROMPT_CHARS` — default 12.
|
|
228
|
+
- `MEMO_RECALL_BODY_CHARS` — snippet length, default 240.
|
|
229
|
+
- `MEMO_RECALL_SKIP_SLASH` — skip recall on `/` prompts, default 1.
|
|
230
|
+
- `MEMO_RECALL_DEBUG` — print failure reasons to stderr, default 0.
|
|
231
|
+
|
|
232
|
+
### Why this is the game changer
|
|
233
|
+
|
|
234
|
+
Before: user types `/memo save 'X'` to save and `/memo search 'Y'` to recall.
|
|
235
|
+
Friction = adoption blocker; memory only helps when remembered to invoke.
|
|
236
|
+
|
|
237
|
+
After: memo silently consults the user's past on every prompt and injects
|
|
238
|
+
the most relevant 3 memorias if any score above 0.6 cosine similarity.
|
|
239
|
+
Zero `/memo` invocations needed for the recall side. The agent "knows
|
|
240
|
+
your past" automatically.
|
|
241
|
+
|
|
242
|
+
### Empirical tuning
|
|
243
|
+
|
|
244
|
+
Threshold 0.6 was picked after testing the 223-doc corpus:
|
|
245
|
+
- "qué decidí sobre MLX vs Ollama" → 3 hits at 0.71-0.74 (all relevant).
|
|
246
|
+
- "how to bake apple pie" (corpus has zero food memorias) → 0 hits at 0.6
|
|
247
|
+
(3 noise hits at 0.5-0.6 cut by the floor).
|
|
248
|
+
- "qué hice con whatsapp" → 3 hits at 0.6-0.75 (whatsapp work).
|
|
249
|
+
|
|
250
|
+
### Privacy / local-first
|
|
251
|
+
|
|
252
|
+
Hot path is 100% MLX in-process. Embedder = `Qwen3-Embedding-0.6B-4bit-DWQ`,
|
|
253
|
+
search = `sqlite-vec`. Zero network calls, zero cloud APIs, zero telemetry.
|
|
254
|
+
The hook input (your prompt) never leaves the machine.
|
|
255
|
+
|
|
256
|
+
### Save side (Phase B)
|
|
257
|
+
|
|
258
|
+
Passive memory extraction (auto-`memo save` from chat transcripts) is NOT
|
|
259
|
+
in 0.3.0 — recall side first to validate the architecture, save side in
|
|
260
|
+
0.4.0 once we have signal on whether the recall hook is helpful in real use.
|
|
261
|
+
|
|
262
|
+
## [0.2.0] - 2026-05-07
|
|
263
|
+
|
|
264
|
+
First public release. Distribution name on PyPI is `memo-mcp`
|
|
265
|
+
(`memo` was already taken).
|
|
266
|
+
|
|
267
|
+
### Added
|
|
268
|
+
|
|
269
|
+
- Public PyPI distribution as [`memo-mcp`](https://pypi.org/project/memo-mcp/).
|
|
270
|
+
- Claude Code plugin format (`.claude-plugin/plugin.json`) — single-step install
|
|
271
|
+
via `/plugin install memo@jagoff/memo`.
|
|
272
|
+
- `.mcp.json` bundled in repo root so MCP-aware clients can auto-register.
|
|
273
|
+
- `skills/memo/SKILL.md` bundled in repo — slash-command UX layer for Claude
|
|
274
|
+
Code CLI users (optional).
|
|
275
|
+
- LICENSE file (MIT).
|
|
276
|
+
- README polish: install, quickstart, architecture diagram, comparison vs
|
|
277
|
+
`mem-vault` / `mem0` / `engram`.
|
|
278
|
+
- Migration script `scripts/migrate-from-mem-vault.py` (already shipped in
|
|
279
|
+
0.1.0 development; documented in README for this release).
|
|
280
|
+
|
|
281
|
+
### Changed
|
|
282
|
+
|
|
283
|
+
- Bumped Development Status from `3 - Alpha` to `4 - Beta` in pyproject classifiers.
|
|
284
|
+
- Author name expanded from "Fer" to "Fernando Ferrari" for PyPI metadata clarity.
|
|
285
|
+
|
|
286
|
+
## [0.1.0] - 2026-04-28
|
|
287
|
+
|
|
288
|
+
Initial development release (private — not on PyPI).
|
|
289
|
+
|
|
290
|
+
### Added
|
|
291
|
+
|
|
292
|
+
- MLX-native memory MCP for Apple Silicon. Stack: `mlx-lm` + `mlx`
|
|
293
|
+
(Qwen2.5-7B/3B-Instruct-4bit + Qwen3-Embedding-0.6B-4bit-DWQ),
|
|
294
|
+
`sqlite-vec` for vectors, markdown files in Obsidian vault for storage.
|
|
295
|
+
- Tools exposed: `memory_save`, `memory_search`, `memory_list`, `memory_get`,
|
|
296
|
+
`memory_update`, `memory_delete`, `memory_stats`, `memory_reindex`,
|
|
297
|
+
`memory_ask` (RAG over memorias with inline citations), `memory_consolidate`
|
|
298
|
+
(cluster + LLM merge proposals), graph queries (entity extraction).
|
|
299
|
+
- CLI (`memo`) with subcommands matching MCP tools.
|
|
300
|
+
- MCP server entry point (`memo-mcp`) using FastMCP framework.
|
|
301
|
+
- History tracking via SQLite for memory edits + accesses.
|
mlx_memo-0.5.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## What this is
|
|
6
|
+
|
|
7
|
+
`memo` (PyPI dist name: `mlx-memo` as of 0.5.0; previously `memo-mcp`) is a local MCP memory server for Apple Silicon. Two entry points share the same `Memory` API:
|
|
8
|
+
|
|
9
|
+
- `memo` — Click-based CLI in `src/memo/cli.py` (~25 commands).
|
|
10
|
+
- `memo-mcp` — FastMCP stdio server in `src/memo/server.py` (~13 tools, all prefixed `memory_`).
|
|
11
|
+
|
|
12
|
+
The Python module, CLI binary, and GitHub repo are all named `memo`. Only the PyPI distribution is `memo-mcp` (because `memo` 0.2.4 was already taken). Don't conflate the two when grepping or renaming.
|
|
13
|
+
|
|
14
|
+
## Common commands
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Dev install (the project requires Python ≥ 3.13; the Linux/x86 path is gated by pyproject markers)
|
|
18
|
+
uv pip install -e '.[dev]'
|
|
19
|
+
|
|
20
|
+
# Tests — fast suite (skips slow real-MLX smoke)
|
|
21
|
+
.venv/bin/python -m pytest -q -m "not slow"
|
|
22
|
+
|
|
23
|
+
# Single test
|
|
24
|
+
.venv/bin/python -m pytest tests/test_memory.py::test_save_then_search -q
|
|
25
|
+
|
|
26
|
+
# Real-MLX smoke (Apple Silicon only — auto-skips elsewhere via conftest)
|
|
27
|
+
.venv/bin/python -m pytest -q -m requires_mlx
|
|
28
|
+
|
|
29
|
+
# Lint
|
|
30
|
+
.venv/bin/ruff check src/ tests/
|
|
31
|
+
|
|
32
|
+
# CLI smoke against the real vault
|
|
33
|
+
memo doctor
|
|
34
|
+
memo stats
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
CI (`.github/workflows/test.yml`) runs ruff + `pytest -m "not slow"` on Ubuntu — this works because `mlx`/`mlx-lm` deps are gated to `darwin`/`arm64` in `pyproject.toml`, and `tests/conftest.py` auto-skips `requires_mlx` tests when `mlx_lm` isn't importable.
|
|
38
|
+
|
|
39
|
+
Pytest markers (declared in `pyproject.toml`):
|
|
40
|
+
- `requires_mlx` — loads real MLX models. Auto-skipped without `mlx_lm`. Default run includes them on Apple Silicon.
|
|
41
|
+
- `slow` — anything >1s. Default run excludes via `-m "not slow"`.
|
|
42
|
+
|
|
43
|
+
## Architecture
|
|
44
|
+
|
|
45
|
+
Layered, with the `.md` file in the Obsidian vault as the storage of record. Three sqlite files intentionally split (different WAL, independent reset).
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
CLI / MCP server
|
|
49
|
+
│
|
|
50
|
+
▼
|
|
51
|
+
memory.py (Memory: save / search / list / get / update / delete / reindex / consolidate / ask)
|
|
52
|
+
│
|
|
53
|
+
┌───┼─────────────┬──────────────┬──────────────┐
|
|
54
|
+
▼ ▼ ▼ ▼ ▼
|
|
55
|
+
embedder.py store.py history.py graph.py llm.py
|
|
56
|
+
(MLX (sqlite-vec (events DB) (entities (MLX two-tier:
|
|
57
|
+
embed) memvec.db graph.db) Qwen2.5-7B chat
|
|
58
|
+
+ 3B helper)
|
|
59
|
+
│
|
|
60
|
+
▼
|
|
61
|
+
markdown files in <vault>/99-obsidian/99-AI/memory/ ← source of truth
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Storage of record vs index
|
|
65
|
+
|
|
66
|
+
The `.md` files are authoritative. `memvec.db` (sqlite-vec) is a rebuildable index. The user can edit memorias directly in Obsidian; on next `memo reindex` (or any tool boot), `body_hash` mismatches drive re-embedding. `rm ~/.local/share/memo/memvec.db && memo reindex` is a safe full rebuild — it never touches the `.md` files.
|
|
67
|
+
|
|
68
|
+
Three separate sqlite files in `~/.local/share/memo/`:
|
|
69
|
+
- `memvec.db` — `meta` table + `vec0` virtual table (sqlite-vec). Hot-path read.
|
|
70
|
+
- `history.db` — append-only audit of save/update/delete events.
|
|
71
|
+
- `graph.db` — entity index (proper-noun NER over memorias).
|
|
72
|
+
|
|
73
|
+
Splitting them avoids WAL contention between hot vec reads and batch writes (entity extraction, history). Don't merge them back without good reason.
|
|
74
|
+
|
|
75
|
+
### MLX in-process — Apple Silicon only
|
|
76
|
+
|
|
77
|
+
`embedder.py` uses `mlx_lm.load()` to load a Qwen3-Embedding model, bypasses `lm_head`, last-token-pools the hidden states, and L2-normalises. `llm.py` runs Qwen2.5-Instruct in two tiers (7B chat / 3B helper) via `mlx_lm.generate()`. Both lazy-load under `_load_lock`.
|
|
78
|
+
|
|
79
|
+
Key invariants — don't break these:
|
|
80
|
+
- **Asymmetric retrieval.** Queries get a `Instruct: ...\nQuery: ...` prefix prepended in `embedder.py`; documents go raw. Without the prefix, cosine collapses toward 0. The constant lives in `_QUERY_INSTRUCTION_PREFIX`.
|
|
81
|
+
- **`MLXEmbedder.embed()` is batched.** Signature is `Sequence[str] → list[list[float]]`. Passing a bare string iterates per-character (Python's string-as-iterable), produces variable-dim outputs, and cascades into a Metal GPU error. Always wrap: `embedder.embed([text])[0]`. This was the v0.3.1 bug — don't reintroduce it.
|
|
82
|
+
- **`MEMO_EMBEDDER_DIMS` must match the model.** Asserted at load (1024 for 0.6B, 2560 for 4B, 4096 for 8B). The vec0 schema bakes in `FLOAT[1024]`; swapping models requires `rm memvec.db && memo reindex` — see README "Upgrading the embedder".
|
|
83
|
+
- **`mlx`/`mlx-lm` imports are deferred.** Module-level imports would break Linux CI. Defer until `_ensure_loaded()` (already the convention in `embedder.py` / `llm.py`).
|
|
84
|
+
|
|
85
|
+
### Search modes
|
|
86
|
+
|
|
87
|
+
`Memory.search(query, mode=...)` supports three modes (default `hybrid`):
|
|
88
|
+
- `vec` — cosine over sqlite-vec embeddings.
|
|
89
|
+
- `bm25` — FTS5 over title + tags + body. Uses unicode61 + diacritic stripping for Spanish. The query tokenizer wraps each `\w+` token in its own phrase quotes so multi-word queries become AND-of-tokens, not phrase-match — see v0.3.2 changelog. If you touch BM25 tokenization, re-run `tests/test_memory.py` BM25 cases against multi-word Spanish queries (`"Astor terapia ocupacional"`).
|
|
90
|
+
- `hybrid` — Reciprocal Rank Fusion of vec + BM25 (default).
|
|
91
|
+
|
|
92
|
+
### Ambient memory hooks (`hooks/hooks.json`)
|
|
93
|
+
|
|
94
|
+
When the plugin is installed, two Claude Code hooks are wired:
|
|
95
|
+
- `SessionStart` (matcher `startup|clear`) → `memo prewarm` (async, 30s timeout). Pre-loads MLX so the first recall is fast.
|
|
96
|
+
- `UserPromptSubmit` → `memo recall-hook` (5s timeout). Embeds the prompt, runs vec-only search, prints the top-3 memorias above `MEMO_RECALL_MIN_SIM` (default 0.6) as `additionalContext` markdown on stdout.
|
|
97
|
+
|
|
98
|
+
The 5s timeout is tight. Cold MLX load is ~2s. If you add work to the hook, measure end-to-end latency against the 5s budget with `MEMO_RECALL_DEBUG=1`.
|
|
99
|
+
|
|
100
|
+
## Testing conventions
|
|
101
|
+
|
|
102
|
+
`tests/conftest.py` enforces two rules — preserve them:
|
|
103
|
+
|
|
104
|
+
- **Never touch the real vault.** The `tmp_cfg` fixture builds a `Config` with `data_dir`, `vault_path`, and `state_dir` under pytest's `tmp_path`, and pins `MEMO_CONFIG_FILE` so `Config.from_env()` doesn't read the dev's real `~/.config/memo/config.toml`. Any new test must depend on `tmp_cfg` (or build its own isolated `Config`) — never call `Config.from_env()` in a test without controlling these env vars.
|
|
105
|
+
- **CliRunner-based tests must set `MEMO_NONINTERACTIVE=1`** in the `env=` arg, otherwise the CLI's first-run gate may try to fire the picker mid-test. Also override `MEMO_DATA_DIR`/`MEMO_STATE_DIR` to keep state under `tmp_path`. If your test exercises the embedder via the stub (`monkeypatch.setattr("memo.embedder.MLXEmbedder.embed", ...)`), also pin `MEMO_EMBEDDER_DIMS` to match the stub's output dim — the dev's shell may have `MEMO_EMBEDDER_DIMS=2560` (4B model) exported and CliRunner inherits it.
|
|
106
|
+
- **`requires_mlx` is auto-applied.** A test marked `@pytest.mark.requires_mlx` skips automatically when `mlx_lm` can't be imported. Use it for anything that does a real forward pass; everything else should monkeypatch `MLXEmbedder.embed` / `MLXChat.chat`.
|
|
107
|
+
|
|
108
|
+
`tests/test_smoke_mlx.py` is the canonical example of `requires_mlx` smoke tests.
|
|
109
|
+
|
|
110
|
+
## Versioning + release
|
|
111
|
+
|
|
112
|
+
Version lives in three places — bump together:
|
|
113
|
+
- `pyproject.toml` `[project] version`
|
|
114
|
+
- `.claude-plugin/plugin.json` `version`
|
|
115
|
+
- `CHANGELOG.md` (Keep-a-Changelog format)
|
|
116
|
+
|
|
117
|
+
`src/memo/__init__.py:__version__` is currently pinned at `"0.1.0"` and is not the source of truth — `pyproject.toml` is.
|
|
118
|
+
|
|
119
|
+
## Conventions specific to this repo
|
|
120
|
+
|
|
121
|
+
- **The memoria storage location is user-configurable.** The default is `~/Documents/memo/` (a standard macOS path so the tool works for users who don't use Obsidian). On first interactive run, `memo` prompts an arrow-key picker (`memo init` re-runs it). The chosen path is persisted in `~/.config/memo/config.toml` `[storage] data_dir`. Tests must override via `tmp_cfg` (which sets `data_dir`, `vault_path`, `state_dir` and pins `MEMO_CONFIG_FILE` to a test-only path so the developer's real config doesn't leak in). Resolution order in `Config.from_env()`: explicit kwargs → `MEMO_*` env vars → config file → legacy `MEMO_VAULT_PATH` + `MEMO_MEMORY_SUBDIR` → default. `vault_path` is now optional and only used by `memo ingest`.
|
|
122
|
+
- **Frontmatter schema is fixed.** `id`, `title`, `type`, `tags`, `created`, `updated`. `type` is one of `decision | fact | bug | feedback | preference | note | manual`. Adding a new type means updating the Click `Choice` in `cli.py:save`/`update` AND the docstring in `server.py:memory_save`.
|
|
123
|
+
- **Filename slug is `<YYYY-MM-DD>-<slug>.md`.** Mirrors obsidian-rag's conversation writer. The slugifier lives in `memory.py`.
|
|
124
|
+
- **No Ollama anywhere.** `pyproject.toml` does not declare it; `doctor` does not probe `:11434`. The README leans on this as a selling point — don't reintroduce it as a dependency.
|
mlx_memo-0.5.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fernando Ferrari
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|