mempalace-code 1.3.0__tar.gz → 1.4.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.
Files changed (135) hide show
  1. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/CHANGELOG.md +13 -0
  2. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/CLAUDE.md +2 -1
  3. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/PKG-INFO +29 -5
  4. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/README.md +28 -4
  5. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/docs/BACKLOG-archived.yaml +12 -0
  6. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/docs/BACKLOG.yaml +0 -41
  7. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/mempalace/cli.py +110 -0
  8. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/mempalace/miner.py +41 -5
  9. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/mempalace/storage.py +23 -2
  10. mempalace_code-1.4.0/mempalace/watcher.py +619 -0
  11. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/pyproject.toml +1 -1
  12. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/tests/test_miner.py +45 -2
  13. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/tests/test_symbol_extract.py +35 -0
  14. mempalace_code-1.3.0/mempalace/watcher.py +0 -240
  15. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/.claude/prompts/codex-hardening-review.md +0 -0
  16. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/.claude/prompts/codex-plan-review.md +0 -0
  17. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/.claude/settings.json +0 -0
  18. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/.claude/skills/_shared/commit-checkpoint.md +0 -0
  19. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/.claude/skills/_shared/mode-classification.md +0 -0
  20. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/.claude/skills/_shared/task-state.md +0 -0
  21. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/.claude/skills/bench/SKILL.md +0 -0
  22. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/.claude/skills/doc-refresh/INSTRUCTIONS.md +0 -0
  23. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/.claude/skills/doc-refresh/SKILL.md +0 -0
  24. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/.claude/skills/entropy-gc/INSTRUCTIONS.md +0 -0
  25. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/.claude/skills/entropy-gc/SKILL.md +0 -0
  26. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/.claude/skills/mine/SKILL.md +0 -0
  27. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/.claude/skills/palace-health/SKILL.md +0 -0
  28. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/.claude/skills/release/SKILL.md +0 -0
  29. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/.claude/skills/ship/INSTRUCTIONS.md +0 -0
  30. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/.claude/skills/ship/SKILL.md +0 -0
  31. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/.claude/skills/start/INSTRUCTIONS.md +0 -0
  32. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/.claude/skills/start/SKILL.md +0 -0
  33. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/.claude/skills/status/SKILL.md +0 -0
  34. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/.claude/skills/task-hardening/INSTRUCTIONS.md +0 -0
  35. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/.claude/skills/task-hardening/SKILL.md +0 -0
  36. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/.claude/skills/task-plan/INSTRUCTIONS.md +0 -0
  37. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/.claude/skills/task-plan/SKILL.md +0 -0
  38. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/.claude/skills/verify/INSTRUCTIONS.md +0 -0
  39. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/.claude/skills/verify/SKILL.md +0 -0
  40. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  41. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  42. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  43. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/.github/workflows/ci.yml +0 -0
  44. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/.github/workflows/publish.yml +0 -0
  45. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/.gitignore +0 -0
  46. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/.pre-commit-config.yaml +0 -0
  47. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/CONTRIBUTING.md +0 -0
  48. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/LICENSE +0 -0
  49. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/NOTICE +0 -0
  50. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/assets/mempalace_banner.jpg +0 -0
  51. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/benchmarks/BENCHMARKS.md +0 -0
  52. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/benchmarks/HYBRID_MODE.md +0 -0
  53. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/benchmarks/README.md +0 -0
  54. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/benchmarks/convomem_bench.py +0 -0
  55. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/benchmarks/dotnet_bench.py +0 -0
  56. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/benchmarks/embed_ab_bench.py +0 -0
  57. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/benchmarks/locomo_bench.py +0 -0
  58. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/benchmarks/longmemeval_bench.py +0 -0
  59. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/benchmarks/membench_bench.py +0 -0
  60. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/benchmarks/results_embed_ab_2026-04-09.json +0 -0
  61. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/benchmarks/results_token_delta_mempalace.json +0 -0
  62. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/benchmarks/results_token_delta_wh40k.json +0 -0
  63. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/benchmarks/token_delta_bench.py +0 -0
  64. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/docs/AGENT_INSTALL.md +0 -0
  65. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/docs/BACKUP_RESTORE.md +0 -0
  66. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/docs/BENCH_TOKEN_DELTA.md +0 -0
  67. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/docs/COMPARISON_GRAPHIFY.md +0 -0
  68. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/docs/HOW_SEARCH_WORKS.md +0 -0
  69. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/docs/OFFLINE_USAGE.md +0 -0
  70. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/docs/UPSTREAM_HARDENING.md +0 -0
  71. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/docs/WHY_THIS_FORK.md +0 -0
  72. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/examples/HOOKS_TUTORIAL.md +0 -0
  73. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/examples/basic_mining.py +0 -0
  74. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/examples/convo_import.py +0 -0
  75. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/examples/gemini_cli_setup.md +0 -0
  76. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/examples/mcp_setup.md +0 -0
  77. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/hooks/README.md +0 -0
  78. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/hooks/mempal_precompact_hook.sh +0 -0
  79. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/hooks/mempal_save_hook.sh +0 -0
  80. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/mempalace/README.md +0 -0
  81. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/mempalace/__init__.py +0 -0
  82. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/mempalace/__main__.py +0 -0
  83. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/mempalace/_chroma_store.py +0 -0
  84. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/mempalace/backup.py +0 -0
  85. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/mempalace/config.py +0 -0
  86. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/mempalace/convo_miner.py +0 -0
  87. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/mempalace/dialect.py +0 -0
  88. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/mempalace/entity_detector.py +0 -0
  89. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/mempalace/entity_registry.py +0 -0
  90. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/mempalace/export.py +0 -0
  91. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/mempalace/general_extractor.py +0 -0
  92. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/mempalace/knowledge_graph.py +0 -0
  93. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/mempalace/layers.py +0 -0
  94. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/mempalace/mcp_server.py +0 -0
  95. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/mempalace/migrate.py +0 -0
  96. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/mempalace/normalize.py +0 -0
  97. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/mempalace/onboarding.py +0 -0
  98. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/mempalace/palace_graph.py +0 -0
  99. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/mempalace/py.typed +0 -0
  100. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/mempalace/room_detector_local.py +0 -0
  101. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/mempalace/searcher.py +0 -0
  102. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/mempalace/spellcheck.py +0 -0
  103. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/mempalace/split_mega_files.py +0 -0
  104. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/mempalace/treesitter.py +0 -0
  105. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/mempalace/version.py +0 -0
  106. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/scripts/bootstrap.sh +0 -0
  107. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/scripts/codex-review.sh +0 -0
  108. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/scripts/nuke_wing.py +0 -0
  109. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/tests/conftest.py +0 -0
  110. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/tests/test_backup.py +0 -0
  111. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/tests/test_chroma_compat.py +0 -0
  112. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/tests/test_chunking.py +0 -0
  113. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/tests/test_cli.py +0 -0
  114. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/tests/test_config.py +0 -0
  115. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/tests/test_convo_miner.py +0 -0
  116. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/tests/test_dialect.py +0 -0
  117. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/tests/test_dotnet_config.py +0 -0
  118. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/tests/test_e2e.py +0 -0
  119. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/tests/test_embed_ab_bench.py +0 -0
  120. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/tests/test_export.py +0 -0
  121. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/tests/test_kg_extract.py +0 -0
  122. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/tests/test_knowledge_graph.py +0 -0
  123. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/tests/test_lang_detect.py +0 -0
  124. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/tests/test_mcp_server.py +0 -0
  125. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/tests/test_migrate.py +0 -0
  126. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/tests/test_normalize.py +0 -0
  127. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/tests/test_offline.py +0 -0
  128. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/tests/test_searcher.py +0 -0
  129. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/tests/test_split_mega_files.py +0 -0
  130. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/tests/test_storage.py +0 -0
  131. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/tests/test_storage_lance.py +0 -0
  132. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/tests/test_treesitter.py +0 -0
  133. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/tests/test_version_consistency.py +0 -0
  134. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/tests/test_watcher.py +0 -0
  135. {mempalace_code-1.3.0 → mempalace_code-1.4.0}/uv.lock +0 -0
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.4.0 — 2026-04-19
4
+
5
+ ### Added
6
+ - **Watcher quiet mode** — re-mines suppress verbose output; only logs a one-line summary when drawers are actually filed; no-op commits produce zero log noise; optimize skipped on empty batches
7
+ - **Per-project `bin/` skip** — `bin/` no longer globally skipped; only excluded when .NET project markers (`.csproj`, `.sln`, `.fsproj`, `.vbproj`) are present (MINE-BIN-SKIP-DIRS)
8
+ - **Kotlin nested generic receiver** — `fun <T> List<Pair<K,V>>.ext()` now parsed correctly (MINE-KOTLIN-GENERIC-RECEIVER-NESTED)
9
+ - `mine()` now returns stats dict (`files_processed`, `drawers_filed`, `elapsed_secs`)
10
+
11
+ ### Fixed
12
+ - **Watcher on-commit detection** — `watchfiles.DefaultFilter` ignores `.git/` by default; on-commit mode now passes `watch_filter=None` so `.git/refs/heads/` changes are detected
13
+ - **Watcher log buffering** — flush Python stdout/stderr before restoring file descriptors to prevent mine() output leaking to real stdout
14
+ - **HuggingFace/safetensors noise** — suppress BertModel LOAD REPORT and progress bars via OS fd-level redirect during model init
15
+
3
16
  ## v1.3.0 — 2026-04-19
4
17
 
5
18
  First-class C#/.NET support — delivers [rergards/mempalace-code#1](https://github.com/rergards/mempalace-code/issues/1) in full.
@@ -72,7 +72,8 @@ Line length: 100. Target: py39. Quote style: double.
72
72
  | `layers.py` | Tiered context loading — L0/L1/L2/L3 wake-up layers for local models |
73
73
  | `palace_graph.py` | Graph traversal and tunnel detection across wings/rooms |
74
74
  | `mcp_server.py` | MCP server — exposes palace tools to Claude Code and other MCP clients |
75
- | `cli.py` | `mempalace` CLI entry point init, mine, search, health, repair, backup |
75
+ | `watcher.py` | File watcher`watch_and_mine`, `watch_all`, launchd/cron schedule rendering |
76
+ | `cli.py` | `mempalace` CLI entry point — init, mine, mine-all, watch, search, health, repair, backup |
76
77
 
77
78
  ## Architecture Principles
78
79
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mempalace-code
3
- Version: 1.3.0
3
+ Version: 1.4.0
4
4
  Summary: Developer memory tool — mine codebases and conversations into a LanceDB-backed searchable palace. No API key required.
5
5
  Project-URL: Homepage, https://github.com/rergards/mempalace-code
6
6
  Project-URL: Repository, https://github.com/rergards/mempalace-code
@@ -77,7 +77,7 @@ No cloud. No API keys. No subscription. Nothing leaves your machine.
77
77
  <tr>
78
78
  <td align="center"><strong>595x Token Savings</strong><br><sub>measured peak · median 80x<br><a href="docs/BENCH_TOKEN_DELTA.md">scales with project size</a></sub></td>
79
79
  <td align="center"><strong>Cross-Project Tunnels</strong><br><sub>Search <code>auth</code> in one project<br>find it everywhere</sub></td>
80
- <td align="center"><strong>1002 Tests · $0 Cost</strong><br><sub>Every feature acceptance-gated<br>fully offline after install</sub></td>
80
+ <td align="center"><strong>1008 Tests · $0 Cost</strong><br><sub>Every feature acceptance-gated<br>fully offline after install</sub></td>
81
81
  </tr>
82
82
  </table>
83
83
 
@@ -175,10 +175,30 @@ Tree-sitter is optional (`pip install "mempalace-code[treesitter]"`). Without it
175
175
  mempalace mine ~/projects/myapp # all supported file types
176
176
  mempalace mine ~/projects/myapp --wing myapp # tag with a specific wing
177
177
  mempalace mine ~/chats/ --mode convos # mine conversation exports
178
+ mempalace mine-all ~/projects/ # batch mine all projects in a directory
178
179
  ```
179
180
 
180
181
  Mining is **incremental** by default — content-hash based, only changed files are re-chunked. Use `--full` to force a rebuild.
181
182
 
183
+ ### Auto-Watch
184
+
185
+ Keep your palace in sync automatically. By default, watches `.git/refs/heads/` and re-mines only on **commit** — no noise from work-in-progress saves. Handles multiple branches and worktrees.
186
+
187
+ ```bash
188
+ mempalace watch ~/projects/ # watch all projects (on commit, default)
189
+ mempalace watch ~/projects/ --on-save # watch all file saves instead (noisier)
190
+ mempalace watch ~/projects/ schedule # print launchd/cron snippet for daemon
191
+ ```
192
+
193
+ **Install as persistent daemon (macOS):**
194
+
195
+ ```bash
196
+ mempalace watch ~/projects/ schedule > ~/Library/LaunchAgents/com.mempalace.watch.plist
197
+ launchctl load ~/Library/LaunchAgents/com.mempalace.watch.plist
198
+ ```
199
+
200
+ Starts at login, restarts if crashed. Logs to `/tmp/mempalace-watch.log`.
201
+
182
202
  ---
183
203
 
184
204
  ### The Palace
@@ -421,7 +441,7 @@ This is a code-first fork of [milla-jovovich/mempalace](https://github.com/milla
421
441
  | ChromaDB — [silently deletes data on version bump](https://github.com/milla-jovovich/mempalace/issues/469) | LanceDB — crash-safe Arrow storage, no version-cliff |
422
442
  | "No internet after install" — [false](https://github.com/milla-jovovich/mempalace/issues/524) | `mempalace init` downloads model explicitly; fully offline after |
423
443
  | "100% R@5" — [unverifiable](https://github.com/milla-jovovich/mempalace/issues/27) | Number removed. Methodology caveats documented |
424
- | ~30% test coverage | 1002 tests, every feature acceptance-gated |
444
+ | ~30% test coverage | 1008 tests, every feature acceptance-gated |
425
445
  | No backup, no recovery | `backup` / `restore` / `export` / `import` |
426
446
  | No incremental mining | Content-hash incremental: only changed files re-chunked |
427
447
  | No code-search | `code_search` — filter by language, symbol, glob |
@@ -496,6 +516,10 @@ mempalace mine <dir> --full # force full rebuild
496
516
  mempalace mine <dir> --watch # auto-incremental on file changes
497
517
  mempalace mine-all <parent-dir> # batch mine all projects in a directory
498
518
 
519
+ # Watch (multi-project auto-sync)
520
+ mempalace watch <parent-dir> # watch all initialized projects
521
+ mempalace watch <parent-dir> schedule # print launchd/cron daemon snippet
522
+
499
523
  # Search
500
524
  mempalace search "query" # search everything
501
525
  mempalace search "query" --wing myapp # scoped to wing
@@ -559,7 +583,7 @@ mempalace/
559
583
  ├── benchmarks/ ← reproducible benchmark runners
560
584
  ├── hooks/ ← Claude Code auto-save hooks
561
585
  ├── examples/ ← usage examples
562
- └── tests/ ← 1002 tests
586
+ └── tests/ ← 1008 tests
563
587
  ```
564
588
 
565
589
  </details>
@@ -579,7 +603,7 @@ python -m pytest tests/ -x -q # full suite, all local, no network
579
603
  Apache 2.0 — see [LICENSE](LICENSE) and [NOTICE](NOTICE).
580
604
 
581
605
  <!-- Link Definitions -->
582
- [version-shield]: https://img.shields.io/badge/version-1.3.0-4dc9f6?style=flat-square&labelColor=0a0e14
606
+ [version-shield]: https://img.shields.io/badge/version-1.4.0-4dc9f6?style=flat-square&labelColor=0a0e14
583
607
  [release-link]: https://github.com/rergards/mempalace-code/releases
584
608
  [python-shield]: https://img.shields.io/badge/python-3.9+-7dd8f8?style=flat-square&labelColor=0a0e14&logo=python&logoColor=7dd8f8
585
609
  [python-link]: https://www.python.org/
@@ -29,7 +29,7 @@ No cloud. No API keys. No subscription. Nothing leaves your machine.
29
29
  <tr>
30
30
  <td align="center"><strong>595x Token Savings</strong><br><sub>measured peak · median 80x<br><a href="docs/BENCH_TOKEN_DELTA.md">scales with project size</a></sub></td>
31
31
  <td align="center"><strong>Cross-Project Tunnels</strong><br><sub>Search <code>auth</code> in one project<br>find it everywhere</sub></td>
32
- <td align="center"><strong>1002 Tests · $0 Cost</strong><br><sub>Every feature acceptance-gated<br>fully offline after install</sub></td>
32
+ <td align="center"><strong>1008 Tests · $0 Cost</strong><br><sub>Every feature acceptance-gated<br>fully offline after install</sub></td>
33
33
  </tr>
34
34
  </table>
35
35
 
@@ -127,10 +127,30 @@ Tree-sitter is optional (`pip install "mempalace-code[treesitter]"`). Without it
127
127
  mempalace mine ~/projects/myapp # all supported file types
128
128
  mempalace mine ~/projects/myapp --wing myapp # tag with a specific wing
129
129
  mempalace mine ~/chats/ --mode convos # mine conversation exports
130
+ mempalace mine-all ~/projects/ # batch mine all projects in a directory
130
131
  ```
131
132
 
132
133
  Mining is **incremental** by default — content-hash based, only changed files are re-chunked. Use `--full` to force a rebuild.
133
134
 
135
+ ### Auto-Watch
136
+
137
+ Keep your palace in sync automatically. By default, watches `.git/refs/heads/` and re-mines only on **commit** — no noise from work-in-progress saves. Handles multiple branches and worktrees.
138
+
139
+ ```bash
140
+ mempalace watch ~/projects/ # watch all projects (on commit, default)
141
+ mempalace watch ~/projects/ --on-save # watch all file saves instead (noisier)
142
+ mempalace watch ~/projects/ schedule # print launchd/cron snippet for daemon
143
+ ```
144
+
145
+ **Install as persistent daemon (macOS):**
146
+
147
+ ```bash
148
+ mempalace watch ~/projects/ schedule > ~/Library/LaunchAgents/com.mempalace.watch.plist
149
+ launchctl load ~/Library/LaunchAgents/com.mempalace.watch.plist
150
+ ```
151
+
152
+ Starts at login, restarts if crashed. Logs to `/tmp/mempalace-watch.log`.
153
+
134
154
  ---
135
155
 
136
156
  ### The Palace
@@ -373,7 +393,7 @@ This is a code-first fork of [milla-jovovich/mempalace](https://github.com/milla
373
393
  | ChromaDB — [silently deletes data on version bump](https://github.com/milla-jovovich/mempalace/issues/469) | LanceDB — crash-safe Arrow storage, no version-cliff |
374
394
  | "No internet after install" — [false](https://github.com/milla-jovovich/mempalace/issues/524) | `mempalace init` downloads model explicitly; fully offline after |
375
395
  | "100% R@5" — [unverifiable](https://github.com/milla-jovovich/mempalace/issues/27) | Number removed. Methodology caveats documented |
376
- | ~30% test coverage | 1002 tests, every feature acceptance-gated |
396
+ | ~30% test coverage | 1008 tests, every feature acceptance-gated |
377
397
  | No backup, no recovery | `backup` / `restore` / `export` / `import` |
378
398
  | No incremental mining | Content-hash incremental: only changed files re-chunked |
379
399
  | No code-search | `code_search` — filter by language, symbol, glob |
@@ -448,6 +468,10 @@ mempalace mine <dir> --full # force full rebuild
448
468
  mempalace mine <dir> --watch # auto-incremental on file changes
449
469
  mempalace mine-all <parent-dir> # batch mine all projects in a directory
450
470
 
471
+ # Watch (multi-project auto-sync)
472
+ mempalace watch <parent-dir> # watch all initialized projects
473
+ mempalace watch <parent-dir> schedule # print launchd/cron daemon snippet
474
+
451
475
  # Search
452
476
  mempalace search "query" # search everything
453
477
  mempalace search "query" --wing myapp # scoped to wing
@@ -511,7 +535,7 @@ mempalace/
511
535
  ├── benchmarks/ ← reproducible benchmark runners
512
536
  ├── hooks/ ← Claude Code auto-save hooks
513
537
  ├── examples/ ← usage examples
514
- └── tests/ ← 1002 tests
538
+ └── tests/ ← 1008 tests
515
539
  ```
516
540
 
517
541
  </details>
@@ -531,7 +555,7 @@ python -m pytest tests/ -x -q # full suite, all local, no network
531
555
  Apache 2.0 — see [LICENSE](LICENSE) and [NOTICE](NOTICE).
532
556
 
533
557
  <!-- Link Definitions -->
534
- [version-shield]: https://img.shields.io/badge/version-1.3.0-4dc9f6?style=flat-square&labelColor=0a0e14
558
+ [version-shield]: https://img.shields.io/badge/version-1.4.0-4dc9f6?style=flat-square&labelColor=0a0e14
535
559
  [release-link]: https://github.com/rergards/mempalace-code/releases
536
560
  [python-shield]: https://img.shields.io/badge/python-3.9+-7dd8f8?style=flat-square&labelColor=0a0e14&logo=python&logoColor=7dd8f8
537
561
  [python-link]: https://www.python.org/
@@ -148,3 +148,15 @@ items:
148
148
  summary: Track plain Name= attribute as well as x:Name for has_named_control triples
149
149
  resolution: '2026-04-19: Updated parse_xaml_file() section 3 to collect both x:Name and plain Name= per element into a set before emitting has_named_control triples; added 2 regression tests (plain Name= and same-value dedup). All 189 tests pass.'
150
150
  archived_date: "2026-04-19"
151
+ - key: MINE-KOTLIN-GENERIC-RECEIVER-NESTED
152
+ summary: Extract function names from Kotlin generic functions with deeply nested receiver types
153
+ resolution: '2026-04-19: Replaced [^>]+ with (?:[^<>]|<[^<>]*>)* in fun regex; added 2 tests for depth-2 generic nesting (AC-1, AC-2); all 176 symbol extract tests pass'
154
+ archived_date: "2026-04-19"
155
+ - key: MINE-BIN-SKIP-DIRS
156
+ summary: bin/ in SKIP_DIRS silently excludes non-.NET script directories
157
+ resolution: '2026-04-19: Remove bin from global SKIP_DIRS; add _is_dotnet_project() helper; skip bin/ conditionally in scan_project() when .NET markers present. Hardened round-1: fixed weak .dll test fixture in test_bin_dir_skipped_when_sln_at_root.'
158
+ archived_date: "2026-04-19"
159
+ - key: CLARIFY-MINE-BIN-SKIP-DIRS
160
+ summary: Clarify scope for MINE-BIN-SKIP-DIRS before planning can proceed
161
+ resolution: '2026-04-19: Owner decision: implement per-project detection — skip bin/ only when .NET markers (.csproj, .sln, .fsproj, .vbproj) are present. Add non-.NET regression test.'
162
+ archived_date: "2026-04-19"
@@ -840,26 +840,6 @@ items:
840
840
  - [ ] extract_symbol("String getName() {\n}\n", "java") returns ("getName", "method")
841
841
  - [ ] extract_symbol("String name;\n", "java") still returns ("", "")
842
842
  - [ ] All existing Java symbol-extraction tests pass
843
- - key: MINE-KOTLIN-GENERIC-RECEIVER-NESTED
844
- summary: Extract function names from Kotlin generic functions with deeply nested receiver types
845
- type: feature
846
- status: open
847
- priority: low
848
- size: S
849
- section_id: immediate
850
- labels: [feat, miner, kotlin]
851
- description: |-
852
- ## Problem
853
- The fun extraction regex uses negated-class [^>] that stops at the first >, so nested generics like Map<String, List<Int>> or bounds like <T : Comparable<T>> cause extract_symbol to return ("", ""). Chunks are stored but symbol metadata is empty, degrading search relevance for generic utility/extension code.
854
-
855
- ## Scope
856
- - Update fun regex in _KOTLIN_EXTRACT to handle depth-2 nesting
857
- - Cover: type param bounds, generic receivers
858
-
859
- ## Acceptance criteria
860
- - [ ] extract_symbol('fun <T : Comparable<T>> List<T>.sorted(): List<T>', 'kotlin') == ('sorted', 'function')
861
- - [ ] extract_symbol('fun Map<String, List<Int>>.flatten(): List<Int>', 'kotlin') == ('flatten', 'function')
862
- - [ ] All 23 existing Kotlin symbol tests still pass
863
843
  - key: MINE-CSHARP-EXPR-BODY
864
844
  summary: C# expression-bodied properties not detected as boundaries or symbols
865
845
  type: feature
@@ -888,27 +868,6 @@ items:
888
868
  - [ ] extract_symbol('public int Count => _items.Count;\n', 'csharp') returns ('Count', 'property')
889
869
  - [ ] chunk_code() creates a boundary at expression-bodied property declarations
890
870
  - [ ] Existing tests unaffected
891
- - key: MINE-BIN-SKIP-DIRS
892
- summary: bin/ in SKIP_DIRS silently excludes non-.NET script directories
893
- type: task
894
- status: open
895
- priority: low
896
- size: S
897
- section_id: immediate
898
- labels: [miner]
899
- description: |-
900
- ## Problem
901
- The MINE-DOTNET feature added 'bin' to SKIP_DIRS globally. In non-.NET projects (e.g. Ruby on Rails, Go), a 'bin/' directory legitimately contains source scripts/executables that should be mined. Currently these files are silently skipped with no warning.
902
-
903
- ## Scope
904
- - Investigate whether 'bin' should be kept global or made .NET-specific
905
- - Option A: Replace global 'bin' entry with per-project detection (only skip bin/ when .csproj/.sln is present in the project root)
906
- - Option B: Document as a known limitation in CLAUDE.md and README
907
-
908
- ## Acceptance criteria
909
- - A Ruby on Rails or Go project with a 'bin/' directory has those files mined correctly (option A), OR the limitation is clearly documented in user-facing docs (option B)
910
- - .NET project mining still skips bin/ and obj/ correctly
911
- - No existing tests regress
912
871
  - key: BENCH-DOTNET-CLONE-REPO
913
872
  summary: Clone jasontaylordev/CleanArchitecture (pin to a stable tag, e.g. v8.0.x) and record the commit hash before running the benchmark
914
873
  type: task
@@ -14,6 +14,8 @@ Commands:
14
14
  mempalace mine <dir> Mine project files (default)
15
15
  mempalace mine <dir> --mode convos Mine conversation exports
16
16
  mempalace mine-all <parent-dir> Mine all projects in a directory
17
+ mempalace watch <parent-dir> Watch all projects for changes, re-mine automatically
18
+ mempalace watch <parent-dir> schedule Print launchd/cron snippet for watch daemon
17
19
  mempalace search "query" Find anything, exact words
18
20
  mempalace wake-up Show L0 + L1 wake-up context
19
21
  mempalace wake-up --wing my_app Wake-up for a specific project
@@ -770,6 +772,76 @@ def cmd_compress(args):
770
772
  print(" (dry run -- nothing stored)")
771
773
 
772
774
 
775
+ def cmd_watch(args):
776
+ watch_command = getattr(args, "watch_command", None)
777
+ if watch_command == "schedule":
778
+ cmd_watch_schedule(args)
779
+ return
780
+
781
+ # Default: run the watcher
782
+ palace_path = os.path.expanduser(args.palace) if args.palace else MempalaceConfig().palace_path
783
+
784
+ try:
785
+ from .watcher import watch_all
786
+ except ImportError as exc:
787
+ print(f" Error importing watcher: {exc}", file=sys.stderr)
788
+ sys.exit(1)
789
+
790
+ watch_all(
791
+ parent_dir=args.dir,
792
+ palace_path=palace_path,
793
+ agent=args.agent,
794
+ respect_gitignore=not args.no_gitignore,
795
+ on_commit=not getattr(args, "on_save", False),
796
+ )
797
+
798
+
799
+ def cmd_watch_schedule(args):
800
+ import sys as _sys
801
+
802
+ if getattr(args, "install", False):
803
+ print(
804
+ " owner action required: --install is not supported.\n"
805
+ " Print the snippet with 'mempalace watch <dir> schedule'\n"
806
+ " then install it yourself with: launchctl load <plist> (macOS)\n"
807
+ " or: crontab -e (Linux).",
808
+ file=sys.stderr,
809
+ )
810
+ sys.exit(2)
811
+
812
+ platform = _sys.platform
813
+ if platform.startswith("darwin"):
814
+ platform = "darwin"
815
+ elif platform.startswith("linux"):
816
+ platform = "linux"
817
+ else:
818
+ print(
819
+ f" Error: watch scheduling is not supported on {_sys.platform}.\n"
820
+ " 'mempalace watch schedule' works on macOS (launchd) and Linux (cron) only.",
821
+ file=sys.stderr,
822
+ )
823
+ sys.exit(1)
824
+
825
+ from .watcher import render_watch_schedule
826
+
827
+ try:
828
+ snippet = render_watch_schedule(args.dir, platform)
829
+ except ValueError as exc:
830
+ print(f" Error: {exc}", file=sys.stderr)
831
+ sys.exit(1)
832
+
833
+ print(snippet, end="")
834
+ if platform == "darwin":
835
+ plist_path = "~/Library/LaunchAgents/com.mempalace.watch.plist"
836
+ print("\n # To install:", file=sys.stderr)
837
+ print(f" # mempalace watch {args.dir} schedule > {plist_path}", file=sys.stderr)
838
+ print(f" # launchctl load {plist_path}", file=sys.stderr)
839
+ print(" # To stop:", file=sys.stderr)
840
+ print(f" # launchctl unload {plist_path}", file=sys.stderr)
841
+ else:
842
+ print("\n # To install: crontab -e (paste the line above)", file=sys.stderr)
843
+
844
+
773
845
  def cmd_backup_create(args):
774
846
  from .backup import create_backup
775
847
 
@@ -1190,6 +1262,43 @@ def main():
1190
1262
  help="Re-download even if already cached",
1191
1263
  )
1192
1264
 
1265
+ # watch
1266
+ p_watch = sub.add_parser(
1267
+ "watch",
1268
+ help="Watch all initialized projects for changes and re-mine automatically",
1269
+ )
1270
+ p_watch.add_argument(
1271
+ "dir",
1272
+ help="Parent directory containing project subdirectories",
1273
+ )
1274
+ p_watch.add_argument(
1275
+ "--no-gitignore",
1276
+ action="store_true",
1277
+ help="Don't respect .gitignore files when scanning project files",
1278
+ )
1279
+ p_watch.add_argument(
1280
+ "--agent",
1281
+ default="mempalace",
1282
+ help="Name recorded on every drawer (default: mempalace)",
1283
+ )
1284
+ p_watch.add_argument(
1285
+ "--on-save",
1286
+ action="store_true",
1287
+ help="Re-mine on every file save instead of only on git commits (noisier)",
1288
+ )
1289
+ watch_sub = p_watch.add_subparsers(dest="watch_command")
1290
+
1291
+ # watch schedule
1292
+ p_watch_schedule = watch_sub.add_parser(
1293
+ "schedule",
1294
+ help="Print a scheduler snippet (launchd plist or cron line) for the watch daemon",
1295
+ )
1296
+ p_watch_schedule.add_argument(
1297
+ "--install",
1298
+ action="store_true",
1299
+ help="(Accepted but rejected with an explanation — owner action required)",
1300
+ )
1301
+
1193
1302
  # backup
1194
1303
  p_backup = sub.add_parser(
1195
1304
  "backup",
@@ -1324,6 +1433,7 @@ def main():
1324
1433
  "init": cmd_init,
1325
1434
  "mine": cmd_mine,
1326
1435
  "mine-all": cmd_mine_all,
1436
+ "watch": cmd_watch,
1327
1437
  "split": cmd_split,
1328
1438
  "search": cmd_search,
1329
1439
  "compress": cmd_compress,
@@ -158,7 +158,6 @@ SKIP_DIRS = {
158
158
  ".tox",
159
159
  ".nox",
160
160
  ".vs",
161
- "bin",
162
161
  "obj",
163
162
  ".idea",
164
163
  ".vscode",
@@ -354,6 +353,27 @@ def is_gitignored(path: Path, matchers: list, is_dir: bool = False) -> bool:
354
353
  return ignored
355
354
 
356
355
 
356
+ _DOTNET_MARKERS = (
357
+ "*.sln",
358
+ "*.csproj",
359
+ "*.fsproj",
360
+ "*.vbproj",
361
+ "*/*.csproj",
362
+ "*/*.fsproj",
363
+ "*/*.vbproj",
364
+ )
365
+
366
+
367
+ def _is_dotnet_project(project_path: Path) -> bool:
368
+ """Return True if *project_path* looks like a .NET project.
369
+
370
+ Checks for .sln at root level and .csproj/.fsproj/.vbproj at root or one
371
+ level deep (the standard layout: Solution.sln at root, Project/Project.csproj
372
+ in a subdirectory). Uses early-exit to minimise filesystem round-trips.
373
+ """
374
+ return any(next(project_path.glob(pat), None) is not None for pat in _DOTNET_MARKERS)
375
+
376
+
357
377
  def should_skip_dir(dirname: str) -> bool:
358
378
  """Skip known generated/cache directories before gitignore matching."""
359
379
  return dirname in SKIP_DIRS or dirname.endswith(".egg-info")
@@ -863,10 +883,11 @@ _KOTLIN_EXTRACT = [
863
883
  (re.compile(r"object\s+(\w+)", re.MULTILINE), "object"),
864
884
  # fun — optional type params (e.g. `fun <T> identity(…)`) and optional receiver type
865
885
  # (e.g. `fun String.isEmpty()` → `isEmpty`, `fun <T> List<T>.map()` → `map`).
866
- # Receiver handles simple types and single-level generics; deeply nested generics are rare.
886
+ # Uses (?:[^<>]|<[^<>]*>)* instead of [^>]+ to handle depth-2 generic nesting, e.g.
887
+ # `fun <T : Comparable<T>> …` and `fun Map<String, List<Int>>.flatten()`.
867
888
  (
868
889
  re.compile(
869
- r"^(?:(?:public|internal|protected|private|abstract|open|final|override|inline|infix|operator|tailrec|suspend|external|expect|actual)\s+)*fun\s+(?:<[^>]+>\s+)?(?:\w+(?:<[^>]+>)?\.)?(\w+)",
890
+ r"^(?:(?:public|internal|protected|private|abstract|open|final|override|inline|infix|operator|tailrec|suspend|external|expect|actual)\s+)*fun\s+(?:<(?:[^<>]|<[^<>]*>)*>\s+)?(?:\w+(?:<(?:[^<>]|<[^<>]*>)*>)?\.)?(\w+)",
870
891
  re.MULTILINE,
871
892
  ),
872
893
  "function",
@@ -1911,6 +1932,7 @@ def scan_project(
1911
1932
  active_matchers = []
1912
1933
  matcher_cache = {}
1913
1934
  include_paths = normalize_include_paths(include_ignored)
1935
+ dotnet_project = _is_dotnet_project(project_path)
1914
1936
 
1915
1937
  for root, dirs, filenames in os.walk(project_path):
1916
1938
  root_path = Path(root)
@@ -1929,7 +1951,7 @@ def scan_project(
1929
1951
  d
1930
1952
  for d in dirs
1931
1953
  if is_force_included(root_path / d, project_path, include_paths)
1932
- or not should_skip_dir(d)
1954
+ or not (should_skip_dir(d) or (dotnet_project and d == "bin"))
1933
1955
  ]
1934
1956
  if respect_gitignore and active_matchers:
1935
1957
  dirs[:] = [
@@ -2623,6 +2645,7 @@ def mine(
2623
2645
  include_ignored: list = None,
2624
2646
  incremental: bool = True,
2625
2647
  kg=None,
2648
+ skip_optimize: bool = False,
2626
2649
  ):
2627
2650
  """Mine a project directory into the palace.
2628
2651
 
@@ -2633,6 +2656,10 @@ def mine(
2633
2656
  *kg* is an optional KnowledgeGraph instance. When provided, .NET project files
2634
2657
  (.csproj, .fsproj, .vbproj) and solution files (.sln) are also parsed for
2635
2658
  structured dependency triples that are written to the knowledge graph.
2659
+
2660
+ When *skip_optimize* is True, post-mine storage compaction is skipped. Callers
2661
+ (e.g. the watcher) that run many mine() calls in sequence should skip optimize
2662
+ on each call and run a single optimize at the end.
2636
2663
  """
2637
2664
 
2638
2665
  project_path = Path(project_dir).expanduser().resolve()
@@ -2811,7 +2838,9 @@ def mine(
2811
2838
  kg.invalidate_by_source_file(stale_path)
2812
2839
 
2813
2840
  config = MempalaceConfig()
2814
- if config.optimize_after_mine:
2841
+ if skip_optimize:
2842
+ pass # caller will optimize later
2843
+ elif config.optimize_after_mine:
2815
2844
  t0 = time.time()
2816
2845
  backup_first = config.backup_before_optimize
2817
2846
  if backup_first:
@@ -2852,6 +2881,13 @@ def mine(
2852
2881
  print('\n Next: mempalace search "what you\'re looking for"')
2853
2882
  print(f"{'=' * 55}\n")
2854
2883
 
2884
+ return {
2885
+ "files_processed": len(files) - files_skipped,
2886
+ "files_skipped": files_skipped,
2887
+ "drawers_filed": total_drawers,
2888
+ "elapsed_secs": elapsed,
2889
+ }
2890
+
2855
2891
 
2856
2892
  # =============================================================================
2857
2893
  # MULTI-PROJECT DETECTION
@@ -241,11 +241,32 @@ class LanceStore(DrawerStore):
241
241
 
242
242
  def __init__(self, palace_path: str, create: bool = True, embed_model: Optional[str] = None):
243
243
  import lancedb
244
+ import logging
244
245
 
245
246
  self._model_name = embed_model or DEFAULT_EMBED_MODEL
246
247
  self._db = lancedb.connect(os.path.join(palace_path, "lance"))
247
- self._embedder = self._get_embedder()
248
- self._table = self._open_or_create(create)
248
+
249
+ # Suppress noisy HF/safetensors output (BertModel LOAD REPORT, tqdm bars,
250
+ # unauthenticated-request warnings). Must redirect at the OS fd level
251
+ # because the noise comes from C/Rust code, not Python.
252
+ hf_logger = logging.getLogger("huggingface_hub")
253
+ prev_level = hf_logger.level
254
+ hf_logger.setLevel(logging.ERROR)
255
+ devnull = os.open(os.devnull, os.O_WRONLY)
256
+ old_stdout = os.dup(1)
257
+ old_stderr = os.dup(2)
258
+ try:
259
+ os.dup2(devnull, 1)
260
+ os.dup2(devnull, 2)
261
+ self._embedder = self._get_embedder()
262
+ self._table = self._open_or_create(create)
263
+ finally:
264
+ os.dup2(old_stdout, 1)
265
+ os.dup2(old_stderr, 2)
266
+ os.close(devnull)
267
+ os.close(old_stdout)
268
+ os.close(old_stderr)
269
+ hf_logger.setLevel(prev_level)
249
270
 
250
271
  def _get_embedder(self):
251
272
  """Load the sentence-transformers embedding model."""