feat(cli): add mm uninstall for state cleanup separate from binary removal#379
Merged
feat(cli): add mm uninstall for state cleanup separate from binary removal#379
Conversation
…removal `uv tool uninstall memtomem` (and the equivalent for other install contexts) only removes the binary — `~/.memtomem/` survives intact, so a subsequent reinstall picks up stale config, fragments, and DB files that may not match the new version's expectations. Documented manual cleanup is `rm -rf ~/.memtomem/`, which misses custom storage paths, running servers, and the per-context binary-uninstall command. `mm uninstall` closes that gap: - Builds a categorised inventory of state files (db + WAL/SHM/journal, config.json, config.d/*, backups, memories/, uploads/, session/pid). - Resolves custom `storage.sqlite_path` so DBs outside `~/.memtomem/` are included; non-DB siblings of a custom path are inventory-only, never deleted. - Refuses to delete while the MCP server is alive (probes `~/.memtomem/.server.pid` via `os.kill(pid, 0)` — open WAL handle during deletion risks corruption). `--force` bypasses for stale-pid cases. - Falls back to default DB path when `load_config_overrides` raises (uninstall is a recovery scenario; can't depend on a valid config). - Detects external editor MCP entries (`~/.claude.json`, `~/.codex/config.toml`, etc.) and reports paths the user must clean manually — never modifies them in this PR. - Prints the exact binary-uninstall command for the detected `mm_binary_origin` (uv-tool / uvx / venv-relative / system / unknown) by reusing the existing `RuntimeProfile` from `cli/init_cmd.py`. Flag semantics: - `--keep-config` preserves config.json + config.d/* + backups - `--keep-data` preserves DB (+ WAL/SHM/journal) + ~/.memtomem/memories/ - `--force` bypasses server liveness check - `-y` skips confirmation prompt Deletion runs in low→high value order (pid/session → fragments → backups → config.json → memories → uploads → DB) with per-group success logging, so a mid-flight failure leaves a recoverable trail and exits 2 with a clear "removed up to <X>, failed at <path>" message. Tests (19 cases, all passing): - Empty state fast path + populated state default deletion - Flag combinations (--keep-config, --keep-data preservation) - Custom storage path inclusion + non-DB sibling preservation - User-managed memory_dirs untouched - External MCP detection + non-modification - Binary hint per-origin (parametrised over all 5 RuntimeProfile values) - Non-TTY abort vs interactive cancellation distinction - Server alive refusal + --force override + stale pid proceed - Config-load fallback when load_config_overrides raises - RuntimeProfile import pin (forces follow-up if init_cmd renames or moves the symbols — MEDIUM 6 mitigation) Docs: `docs/guides/uninstall.md` adds a "Recommended: mm uninstall" section at the top, keeping the manual rm -rf flow below as fallback for users without the CLI available. Out of scope (deferred to follow-up PRs): - Modifying external editor configs (detect-and-report only here) - Provider/model mismatch CLI banner - Schema version tracking / downgrade detection - `RuntimeProfile` extraction to shared module (covered by import pin) - Parsed `mcpServers.memtomem` key check (currently substring grep) - `--dry-run` flag (interactive prompt + inventory is de facto dry-run) Co-Authored-By: Claude <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
uv tool uninstall memtomem(and the equivalent for other install contexts) only removes the binary —~/.memtomem/survives intact, so a subsequent reinstall picks up stale config/fragments/DB that may not match the new version's expectations. Documented manual cleanup isrm -rf ~/.memtomem/, which misses custom storage paths, running servers, and the per-context binary-uninstall command.mm uninstallcloses that gap: builds a categorised inventory of state files, refuses to delete while the MCP server is alive (open WAL handles risk corruption), prints the exact binary-uninstall command for the detected install context, and detects external editor MCP entries (~/.claude.jsonetc.) so the user can clean them up manually.Design highlights
~/.memtomem/; the package manager owns the binary. The command never executes the binary uninstall (different tool's permission scope), but prints the exact command for the detectedmm_binary_origin:uv-tool→uv tool uninstall memtomemuvx→ no action (ephemeral)venv-relative→uv pip uninstall memtomem(orrm -rf .venv)system→pip uninstall memtomemunknown→ conservative fallback hintReuses the existing
RuntimeProfilefromcli/init_cmd.py(no duplication).~/.memtomem/.server.pidviaos.kill(pid, 0). If alive: refuse with exit code 2 + shutdown hint +--forceoverride. Open WAL handles during DB deletion would corrupt the file.config.json. Any exception escapingload_config_overridesis caught and the default DB path is used as fallback (yellow warning shown).storage.sqlite_pathoutside~/.memtomem/is included in the deletion scope (DB + WAL/SHM/journal siblings only). Other siblings of the custom path are inventory-only — never touched. User-managedmemory_dirsare never touched either.~/.claude.json,~/.codex/config.toml,~/.cursor/mcp.json, etc. are detected and reported. Not modified in this PR (per-editor schemas + backup + idempotency need their own design).Tests (19 cases, all passing)
TestEmptyState— fresh HOME, no~/.memtomem/→ exits 0 with binary hintTestDefaultDeletion— full state wipe with pruneTestKeepConfig/TestKeepData— flag preservation semanticsTestCustomStoragePath— custom DB outside default dir included; non-DB siblings preservedTestUserMemoryDirsUntouched—memory_dirspaths never deletedTestExternalsDetectedNotModified—~/.claude.jsonshown but unchangedTestBinaryHintPerOrigin— parametrised over all 5mm_binary_originvaluesTestNonTtyAbortvsTestInteractiveCancellation— distinct messages, both leave state untouchedTestServerAliveRefuses— pid=current process refused;--forcebypassesTestPidStaleProceeds— dead pid is treated as deletableTestConfigFallback—load_config_overridesraise → fallback DB path usedTestRuntimeProfileImportPin— forces follow-up ifinit_cmdrenames/moves the runtime symbolsOut of scope (deferred to follow-up PRs)
RuntimeProfileextraction to shared module (the import pin test catches drift; refactor itself is a separate PR per one-change-per-PR)mcpServers.memtomemkey check (currently substring grep — false-positive-tolerant since it's detect-and-report only)--dry-runflag (interactive prompt + inventory is de-facto dry-run)Verification
uv run ruff check+uv run ruff format --checkpassuv run mypy packages/memtomem/src/memtomem/cli/uninstall_cmd.pycleanuv run pytest packages/memtomem/tests/test_uninstall_cmd.py→ 19 passeduv run pytest -m "not ollama"→ 2127 passed, 0 regressions--forceoverride,--keep-config/--keep-datacombinations all behave as designeddocs/guides/uninstall.mdgains a "Recommended: mm uninstall" section pointing at the new command🤖 Generated with Claude Code