Skip to content

feat(cli): add mm uninstall for state cleanup separate from binary removal#379

Merged
memtomem merged 1 commit intomainfrom
feat/mm-uninstall-cmd
Apr 22, 2026
Merged

feat(cli): add mm uninstall for state cleanup separate from binary removal#379
memtomem merged 1 commit intomainfrom
feat/mm-uninstall-cmd

Conversation

@memtomem
Copy link
Copy Markdown
Owner

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 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, 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.json etc.) so the user can clean them up manually.

mm uninstall                  # interactive, removes everything
mm uninstall -y               # skip confirmation
mm uninstall --keep-config    # preserve config.json + config.d/* + backups
mm uninstall --keep-data      # preserve DB + memories/
mm uninstall --force          # bypass server liveness check (stale pid case)

Design highlights

  • State vs binary separation — Memtomem owns ~/.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 detected mm_binary_origin:
    • uv-tooluv tool uninstall memtomem
    • uvx → no action (ephemeral)
    • venv-relativeuv pip uninstall memtomem (or rm -rf .venv)
    • systempip uninstall memtomem
    • unknown → conservative fallback hint
      Reuses the existing RuntimeProfile from cli/init_cmd.py (no duplication).
  • Server liveness check — probes ~/.memtomem/.server.pid via os.kill(pid, 0). If alive: refuse with exit code 2 + shutdown hint + --force override. Open WAL handles during DB deletion would corrupt the file.
  • Config-load fallback — uninstall is a recovery scenario, so it can't depend on a valid config.json. Any exception escaping load_config_overrides is caught and the default DB path is used as fallback (yellow warning shown).
  • Custom storage pathstorage.sqlite_path outside ~/.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-managed memory_dirs are never touched either.
  • Ordered low→high-value deletion — pid/session → fragments → backups → config → memories → uploads → DB. Mid-flight failure exits 2 with "removed up to X, failed at Y", leaving a recoverable trail.
  • External MCP detection only~/.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 hint
  • TestDefaultDeletion — full state wipe with prune
  • TestKeepConfig / TestKeepData — flag preservation semantics
  • TestCustomStoragePath — custom DB outside default dir included; non-DB siblings preserved
  • TestUserMemoryDirsUntouchedmemory_dirs paths never deleted
  • TestExternalsDetectedNotModified~/.claude.json shown but unchanged
  • TestBinaryHintPerOrigin — parametrised over all 5 mm_binary_origin values
  • TestNonTtyAbort vs TestInteractiveCancellation — distinct messages, both leave state untouched
  • TestServerAliveRefuses — pid=current process refused; --force bypasses
  • TestPidStaleProceeds — dead pid is treated as deletable
  • TestConfigFallbackload_config_overrides raise → fallback DB path used
  • TestRuntimeProfileImportPin — forces follow-up if init_cmd renames/moves the runtime symbols

Out of scope (deferred to follow-up PRs)

  • Modifying external editor configs (detect-and-report only here)
  • Provider/model mismatch CLI banner at startup
  • Schema version tracking + downgrade detection
  • RuntimeProfile extraction to shared module (the import pin test catches drift; refactor itself is a separate PR per one-change-per-PR)
  • Parsed mcpServers.memtomem key check (currently substring grep — false-positive-tolerant since it's detect-and-report only)
  • --dry-run flag (interactive prompt + inventory is de-facto dry-run)

Verification

  • uv run ruff check + uv run ruff format --check pass
  • uv run mypy packages/memtomem/src/memtomem/cli/uninstall_cmd.py clean
  • uv run pytest packages/memtomem/tests/test_uninstall_cmd.py → 19 passed
  • uv run pytest -m "not ollama" → 2127 passed, 0 regressions
  • Manual smoke under tmp HOME — empty state, populated state, server-alive refusal, --force override, --keep-config / --keep-data combinations all behave as designed
  • Docs updated: docs/guides/uninstall.md gains a "Recommended: mm uninstall" section pointing at the new command

🤖 Generated with Claude Code

…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]>
@memtomem memtomem merged commit 9bf713b into main Apr 22, 2026
7 checks passed
@memtomem memtomem deleted the feat/mm-uninstall-cmd branch April 22, 2026 12:55
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 22, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants