memor-cli 0.7.1__tar.gz → 0.9.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 (110) hide show
  1. {memor_cli-0.7.1/memor_cli.egg-info → memor_cli-0.9.0}/PKG-INFO +17 -13
  2. {memor_cli-0.7.1 → memor_cli-0.9.0}/README.md +16 -12
  3. memor_cli-0.9.0/memor/__init__.py +1 -0
  4. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/cli.py +30 -10
  5. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/dashboard/static/index.html +4 -1
  6. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/eval/counterfactual.py +33 -13
  7. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/hook_server.py +17 -5
  8. memor_cli-0.9.0/memor/llm/anthropic.py +45 -0
  9. memor_cli-0.9.0/memor/llm/openai_compat.py +27 -0
  10. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/retrieve/retriever.py +5 -5
  11. memor_cli-0.9.0/memor/service.py +297 -0
  12. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/store/sqlite_store.py +19 -1
  13. {memor_cli-0.7.1 → memor_cli-0.9.0/memor_cli.egg-info}/PKG-INFO +17 -13
  14. {memor_cli-0.7.1 → memor_cli-0.9.0}/pyproject.toml +1 -1
  15. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_multi_agent_hook.py +44 -0
  16. memor_cli-0.9.0/tests/test_service.py +127 -0
  17. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_store.py +22 -0
  18. memor_cli-0.7.1/memor/__init__.py +0 -1
  19. memor_cli-0.7.1/memor/llm/anthropic.py +0 -14
  20. memor_cli-0.7.1/memor/llm/openai_compat.py +0 -20
  21. memor_cli-0.7.1/memor/service.py +0 -187
  22. memor_cli-0.7.1/tests/test_service.py +0 -25
  23. {memor_cli-0.7.1 → memor_cli-0.9.0}/LICENSE +0 -0
  24. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/daemon.py +0 -0
  25. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/dashboard/__init__.py +0 -0
  26. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/dashboard/server.py +0 -0
  27. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/distill/__init__.py +0 -0
  28. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/distill/distiller.py +0 -0
  29. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/distill/extractive.py +0 -0
  30. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/embed/__init__.py +0 -0
  31. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/embed/api.py +0 -0
  32. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/embed/fake.py +0 -0
  33. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/embed/local.py +0 -0
  34. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/eval/__init__.py +0 -0
  35. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/eval/baselines/__init__.py +0 -0
  36. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/eval/baselines/base.py +0 -0
  37. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/eval/baselines/claude_mem.py +0 -0
  38. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/eval/baselines/graphiti.py +0 -0
  39. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/eval/dataset.py +0 -0
  40. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/eval/embed_benchmark.py +0 -0
  41. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/eval/judge.py +0 -0
  42. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/eval/metrics.py +0 -0
  43. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/eval/runner.py +0 -0
  44. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/feedback.py +0 -0
  45. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/global_memories.py +0 -0
  46. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/hook_cli.py +0 -0
  47. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/ingest/__init__.py +0 -0
  48. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/ingest/claude_code.py +0 -0
  49. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/ingest/documents.py +0 -0
  50. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/interfaces.py +0 -0
  51. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/llm/__init__.py +0 -0
  52. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/llm/base.py +0 -0
  53. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/project.py +0 -0
  54. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/query_complexity.py +0 -0
  55. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/recall.py +0 -0
  56. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/redact.py +0 -0
  57. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/retrieve/__init__.py +0 -0
  58. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/session_context.py +0 -0
  59. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/store/__init__.py +0 -0
  60. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/tokencount.py +0 -0
  61. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/turn_metrics.py +0 -0
  62. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor/types.py +0 -0
  63. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor_cli.egg-info/SOURCES.txt +0 -0
  64. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor_cli.egg-info/dependency_links.txt +0 -0
  65. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor_cli.egg-info/entry_points.txt +0 -0
  66. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor_cli.egg-info/requires.txt +0 -0
  67. {memor_cli-0.7.1 → memor_cli-0.9.0}/memor_cli.egg-info/top_level.txt +0 -0
  68. {memor_cli-0.7.1 → memor_cli-0.9.0}/setup.cfg +0 -0
  69. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_cli_smoke.py +0 -0
  70. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_counterfactual.py +0 -0
  71. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_daemon.py +0 -0
  72. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_dashboard.py +0 -0
  73. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_dashboard_quality.py +0 -0
  74. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_dataset_builder.py +0 -0
  75. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_dimension_safety.py +0 -0
  76. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_distiller.py +0 -0
  77. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_embed.py +0 -0
  78. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_embed_benchmark.py +0 -0
  79. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_eval_ablation.py +0 -0
  80. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_eval_runner.py +0 -0
  81. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_external_baselines.py +0 -0
  82. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_extractive.py +0 -0
  83. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_feedback.py +0 -0
  84. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_global_memories.py +0 -0
  85. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_hook.py +0 -0
  86. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_hook_server.py +0 -0
  87. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_hybrid_retrieval.py +0 -0
  88. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_ingest_claude_code.py +0 -0
  89. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_ingest_documents.py +0 -0
  90. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_install_hook.py +0 -0
  91. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_interfaces.py +0 -0
  92. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_judge.py +0 -0
  93. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_metrics.py +0 -0
  94. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_negative_feedback.py +0 -0
  95. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_noise_filter.py +0 -0
  96. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_project_resolver.py +0 -0
  97. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_quality_gate.py +0 -0
  98. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_query_complexity.py +0 -0
  99. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_recall_core.py +0 -0
  100. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_redact.py +0 -0
  101. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_retriever.py +0 -0
  102. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_roi_trend.py +0 -0
  103. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_semantic_feedback.py +0 -0
  104. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_session_context.py +0 -0
  105. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_skill_recall.py +0 -0
  106. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_supersession.py +0 -0
  107. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_tokencount.py +0 -0
  108. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_turn_metrics.py +0 -0
  109. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_types.py +0 -0
  110. {memor_cli-0.7.1 → memor_cli-0.9.0}/tests/test_vec_compaction.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memor-cli
3
- Version: 0.7.1
3
+ Version: 0.9.0
4
4
  Summary: Measured memory for coding agents. Fire and forget — no API keys needed.
5
5
  Author-email: Nimit Bhandari <nimitbhandari17@gmail.com>
6
6
  License-Expression: MIT
@@ -50,9 +50,9 @@ Dynamic: license-file
50
50
  [![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)]()
51
51
  [![PyPI](https://img.shields.io/pypi/v/memor-cli.svg)](https://pypi.org/project/memor-cli/)
52
52
 
53
- **Automatic background memory for Claude Code, Codex, and Copilot.** Fire and forget — no API keys needed.
53
+ **Automatic background memory for Claude Code, Cursor, Codex, and Copilot.** Fire and forget — no API keys needed.
54
54
 
55
- Memor watches your coding sessions, extracts decisions and patterns, and recalls relevant context on every prompt. Works with Claude Code, OpenAI Codex CLI, and GitHub Copilot CLI. Zero configuration. One install. Your agent remembers everything.
55
+ Memor watches your coding sessions, extracts decisions and patterns, and recalls relevant context on every prompt. Works with Claude Code, Cursor, OpenAI Codex CLI, and GitHub Copilot CLI. Zero configuration. One install. Your agent remembers everything.
56
56
 
57
57
  ---
58
58
 
@@ -73,7 +73,7 @@ memor service install
73
73
  memor daemon
74
74
  ```
75
75
 
76
- That's it. Every prompt now gets automatic context recall. Open the dashboard to see it working:
76
+ That's it. Every prompt now gets automatic context recall. `memor service install` also starts the dashboard as a background service, so it's already live at http://localhost:8420 (and is recycled whenever you stop/restart/uninstall the service). To run it in the foreground instead:
77
77
 
78
78
  ```bash
79
79
  memor dashboard
@@ -119,15 +119,16 @@ memor dashboard
119
119
  | **Claude Code** | `UserPromptSubmit` + `additionalContext` | `~/.claude/settings.json` | `memor install-hook --agent claude` |
120
120
  | **Codex CLI** | `UserPromptSubmit` + `additionalContext` | `~/.codex/hooks/hooks.json` | `memor install-hook --agent codex` |
121
121
  | **Copilot CLI** | `userPromptSubmitted` + `additionalContext` | `~/.copilot/hooks/memor.json` | `memor install-hook --agent copilot` |
122
+ | **Cursor** | `beforeSubmitPrompt` + `additionalContext` | `~/.claude/settings.json` (loaded as Claude user hooks) | automatic — covered by the Claude install |
122
123
 
123
- A single `memor-hook` binary auto-detects which agent is calling it — no separate entry points needed. The dashboard tracks recalls per agent so you can see usage across all your environments.
124
+ A single `memor-hook` binary auto-detects which agent is calling it — no separate entry points needed. Cursor loads the same Claude user hooks, so installing for Claude Code covers Cursor too. The dashboard tracks recalls per agent so you can see usage across all your environments.
124
125
 
125
126
  > **Note:** Cloud-hosted agents (Codex cloud, Copilot cloud agent) run in remote sandboxes and cannot reach local hooks. MCP server support for sandboxed agents is planned ([#26](https://github.com/bnimit/memor-ai/issues/26)).
126
127
 
127
128
  **Two background processes:**
128
129
 
129
130
  1. **Daemon** — polls `~/.claude/projects/` for transcripts, embeds chunks, runs distillation, analyzes feedback (positive and negative), promotes cross-project patterns to global scope, compacts duplicates, auto-compacts the vector index when bloated, tracks session-level token usage. All local.
130
- 2. **Hook** — fires on every prompt, recalls relevant memories, injects them as context. Sub-15ms. Works across Claude Code, Codex, and Copilot.
131
+ 2. **Hook** — fires on every prompt, recalls relevant memories, injects them as context. Sub-15ms. Works across Claude Code, Cursor, Codex, and Copilot.
131
132
 
132
133
  **No API keys required.** Embeddings run locally via [model2vec](https://github.com/MinishLab/model2vec) (potion-base-8M, 256-dim). Vectors stored in [sqlite-vec](https://github.com/asg017/sqlite-vec). Everything runs on your machine.
133
134
 
@@ -201,7 +202,7 @@ memor dashboard
201
202
 
202
203
  Dark fintech-inspired UI showing:
203
204
  - **Hero metrics** — total memories, recall count, avg latency, coverage — with sparkline bars
204
- - **Agent breakdown** — per-agent recall stats (Claude, Codex, Copilot) with hit rates
205
+ - **Agent breakdown** — per-agent recall stats (Claude, Cursor, Codex, Copilot) with hit rates
205
206
  - **Daily recall activity** — stacked bar chart of hits vs misses over time
206
207
  - **Session efficiency** — real token savings measured from API usage data (avg tokens/turn with vs without recall)
207
208
  - **Per-project breakdown** — artifact counts, token totals, last activity
@@ -218,10 +219,13 @@ memor install-hook Install hook + download model (interactive
218
219
  memor daemon Auto-ingest + distill (background watcher)
219
220
  memor dashboard Web dashboard on localhost:8420
220
221
  memor version Print installed version
221
- memor service install Run daemon as background service (launchd/systemd)
222
- memor service stop Stop the background service
223
- memor service uninstall Remove the background service
224
- memor service status Check if the service is running
222
+ memor service install Run daemon + dashboard as background services (launchd/systemd)
223
+ --no-dashboard Install only the daemon
224
+ memor service restart Restart both services (use after `pipx upgrade`)
225
+ memor service stop Stop both background services
226
+ memor service uninstall Remove both background services
227
+ memor service status Show daemon + dashboard status
228
+ (dashboard port: set MEMOR_DASHBOARD_PORT, default 8420)
225
229
  memor query <text> Search memories from the CLI
226
230
  memor reingest Wipe DB and re-ingest everything
227
231
  memor reingest --project <name> Re-ingest only one project
@@ -280,7 +284,7 @@ memor/
280
284
  +-- judge.py LLM-as-judge evaluation
281
285
  +-- embed_benchmark.py Embedding model comparison
282
286
 
283
- memor/hook_cli.py Hook entry point — auto-detects Claude/Codex/Copilot
287
+ memor/hook_cli.py Hook entry point — auto-detects Claude/Cursor/Codex/Copilot
284
288
  memor/hook_server.py Hook server with agent detection + response formatting
285
289
  skill/recall.py Standalone recall script
286
290
  ```
@@ -328,7 +332,7 @@ cd memor-ai
328
332
  python3 -m venv .venv && source .venv/bin/activate
329
333
  pip install -e ".[dev]"
330
334
 
331
- pytest # 287 tests
335
+ pytest # 304 tests
332
336
  ```
333
337
 
334
338
  ---
@@ -13,9 +13,9 @@
13
13
  [![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)]()
14
14
  [![PyPI](https://img.shields.io/pypi/v/memor-cli.svg)](https://pypi.org/project/memor-cli/)
15
15
 
16
- **Automatic background memory for Claude Code, Codex, and Copilot.** Fire and forget — no API keys needed.
16
+ **Automatic background memory for Claude Code, Cursor, Codex, and Copilot.** Fire and forget — no API keys needed.
17
17
 
18
- Memor watches your coding sessions, extracts decisions and patterns, and recalls relevant context on every prompt. Works with Claude Code, OpenAI Codex CLI, and GitHub Copilot CLI. Zero configuration. One install. Your agent remembers everything.
18
+ Memor watches your coding sessions, extracts decisions and patterns, and recalls relevant context on every prompt. Works with Claude Code, Cursor, OpenAI Codex CLI, and GitHub Copilot CLI. Zero configuration. One install. Your agent remembers everything.
19
19
 
20
20
  ---
21
21
 
@@ -36,7 +36,7 @@ memor service install
36
36
  memor daemon
37
37
  ```
38
38
 
39
- That's it. Every prompt now gets automatic context recall. Open the dashboard to see it working:
39
+ That's it. Every prompt now gets automatic context recall. `memor service install` also starts the dashboard as a background service, so it's already live at http://localhost:8420 (and is recycled whenever you stop/restart/uninstall the service). To run it in the foreground instead:
40
40
 
41
41
  ```bash
42
42
  memor dashboard
@@ -82,15 +82,16 @@ memor dashboard
82
82
  | **Claude Code** | `UserPromptSubmit` + `additionalContext` | `~/.claude/settings.json` | `memor install-hook --agent claude` |
83
83
  | **Codex CLI** | `UserPromptSubmit` + `additionalContext` | `~/.codex/hooks/hooks.json` | `memor install-hook --agent codex` |
84
84
  | **Copilot CLI** | `userPromptSubmitted` + `additionalContext` | `~/.copilot/hooks/memor.json` | `memor install-hook --agent copilot` |
85
+ | **Cursor** | `beforeSubmitPrompt` + `additionalContext` | `~/.claude/settings.json` (loaded as Claude user hooks) | automatic — covered by the Claude install |
85
86
 
86
- A single `memor-hook` binary auto-detects which agent is calling it — no separate entry points needed. The dashboard tracks recalls per agent so you can see usage across all your environments.
87
+ A single `memor-hook` binary auto-detects which agent is calling it — no separate entry points needed. Cursor loads the same Claude user hooks, so installing for Claude Code covers Cursor too. The dashboard tracks recalls per agent so you can see usage across all your environments.
87
88
 
88
89
  > **Note:** Cloud-hosted agents (Codex cloud, Copilot cloud agent) run in remote sandboxes and cannot reach local hooks. MCP server support for sandboxed agents is planned ([#26](https://github.com/bnimit/memor-ai/issues/26)).
89
90
 
90
91
  **Two background processes:**
91
92
 
92
93
  1. **Daemon** — polls `~/.claude/projects/` for transcripts, embeds chunks, runs distillation, analyzes feedback (positive and negative), promotes cross-project patterns to global scope, compacts duplicates, auto-compacts the vector index when bloated, tracks session-level token usage. All local.
93
- 2. **Hook** — fires on every prompt, recalls relevant memories, injects them as context. Sub-15ms. Works across Claude Code, Codex, and Copilot.
94
+ 2. **Hook** — fires on every prompt, recalls relevant memories, injects them as context. Sub-15ms. Works across Claude Code, Cursor, Codex, and Copilot.
94
95
 
95
96
  **No API keys required.** Embeddings run locally via [model2vec](https://github.com/MinishLab/model2vec) (potion-base-8M, 256-dim). Vectors stored in [sqlite-vec](https://github.com/asg017/sqlite-vec). Everything runs on your machine.
96
97
 
@@ -164,7 +165,7 @@ memor dashboard
164
165
 
165
166
  Dark fintech-inspired UI showing:
166
167
  - **Hero metrics** — total memories, recall count, avg latency, coverage — with sparkline bars
167
- - **Agent breakdown** — per-agent recall stats (Claude, Codex, Copilot) with hit rates
168
+ - **Agent breakdown** — per-agent recall stats (Claude, Cursor, Codex, Copilot) with hit rates
168
169
  - **Daily recall activity** — stacked bar chart of hits vs misses over time
169
170
  - **Session efficiency** — real token savings measured from API usage data (avg tokens/turn with vs without recall)
170
171
  - **Per-project breakdown** — artifact counts, token totals, last activity
@@ -181,10 +182,13 @@ memor install-hook Install hook + download model (interactive
181
182
  memor daemon Auto-ingest + distill (background watcher)
182
183
  memor dashboard Web dashboard on localhost:8420
183
184
  memor version Print installed version
184
- memor service install Run daemon as background service (launchd/systemd)
185
- memor service stop Stop the background service
186
- memor service uninstall Remove the background service
187
- memor service status Check if the service is running
185
+ memor service install Run daemon + dashboard as background services (launchd/systemd)
186
+ --no-dashboard Install only the daemon
187
+ memor service restart Restart both services (use after `pipx upgrade`)
188
+ memor service stop Stop both background services
189
+ memor service uninstall Remove both background services
190
+ memor service status Show daemon + dashboard status
191
+ (dashboard port: set MEMOR_DASHBOARD_PORT, default 8420)
188
192
  memor query <text> Search memories from the CLI
189
193
  memor reingest Wipe DB and re-ingest everything
190
194
  memor reingest --project <name> Re-ingest only one project
@@ -243,7 +247,7 @@ memor/
243
247
  +-- judge.py LLM-as-judge evaluation
244
248
  +-- embed_benchmark.py Embedding model comparison
245
249
 
246
- memor/hook_cli.py Hook entry point — auto-detects Claude/Codex/Copilot
250
+ memor/hook_cli.py Hook entry point — auto-detects Claude/Cursor/Codex/Copilot
247
251
  memor/hook_server.py Hook server with agent detection + response formatting
248
252
  skill/recall.py Standalone recall script
249
253
  ```
@@ -291,7 +295,7 @@ cd memor-ai
291
295
  python3 -m venv .venv && source .venv/bin/activate
292
296
  pip install -e ".[dev]"
293
297
 
294
- pytest # 287 tests
298
+ pytest # 304 tests
295
299
  ```
296
300
 
297
301
  ---
@@ -0,0 +1 @@
1
+ __version__ = "0.9.0"
@@ -46,15 +46,18 @@ memor — measured memory for coding agents
46
46
  GETTING STARTED
47
47
  memor install-hook Install the recall hook + download model
48
48
  --agent claude|codex|copilot Choose your agent (default: claude)
49
- memor service install Start the daemon as a background service
49
+ memor service install Start the daemon + dashboard as background services
50
50
  memor dashboard Open the web dashboard at localhost:8420
51
51
 
52
52
  SERVICE MANAGEMENT
53
- memor service install Install and start as a background service (survives reboots)
54
- memor service status Check if the daemon is running
55
- memor service stop Stop the service (restarts on next login)
56
- memor service uninstall Stop and remove the service completely
53
+ memor service install Install/start daemon + dashboard (survives reboots)
54
+ --no-dashboard Install only the daemon
55
+ memor service status Show daemon + dashboard status
56
+ memor service restart Restart both (use after `pipx upgrade`)
57
+ memor service stop Stop both services (restart on next login)
58
+ memor service uninstall Stop and remove both services completely
57
59
  memor daemon Run the daemon in the foreground (alternative to service)
60
+ (dashboard port: set MEMOR_DASHBOARD_PORT, default 8420)
58
61
 
59
62
  QUERYING
60
63
  memor query <text> Search memories from the command line
@@ -218,7 +221,7 @@ def eval_counterfactual_cmd(project: str = typer.Option(...), db: str = "memor.d
218
221
  import os
219
222
  llm = OpenAICompatLLM(base_url=os.environ.get("OPENAI_BASE_URL", "http://localhost:11434/v1"),
220
223
  api_key=os.environ.get("OPENAI_API_KEY", ""), model=llm_model)
221
- summary = run_suite(cases, store=s, embedder=e, llm=llm, k=k)
224
+ summary = run_suite(cases, store=s, embedder=e, llm=llm, db_path=_db_path(db), k=k)
222
225
  typer.echo(json.dumps({k: v for k, v in summary.items() if k != "cases"}, indent=2))
223
226
  typer.echo("")
224
227
  typer.echo(f" Win: {summary['win_count']}/{summary['n_cases']} ({summary['win_pct']}%)")
@@ -638,11 +641,14 @@ def version():
638
641
 
639
642
 
640
643
  @service_app.command("install")
641
- def service_install():
642
- """Install and start the daemon as a background service (survives reboots)."""
644
+ def service_install(
645
+ no_dashboard: bool = typer.Option(
646
+ False, "--no-dashboard", help="Install only the daemon, not the dashboard."),
647
+ ):
648
+ """Install and start the daemon + dashboard as background services (survives reboots)."""
643
649
  from memor.service import install
644
650
  try:
645
- typer.echo(install())
651
+ typer.echo(install(with_dashboard=not no_dashboard))
646
652
  except FileNotFoundError as e:
647
653
  typer.echo(str(e), err=True)
648
654
  raise typer.Exit(1)
@@ -651,9 +657,23 @@ def service_install():
651
657
  raise typer.Exit(1)
652
658
 
653
659
 
660
+ @service_app.command("restart")
661
+ def service_restart():
662
+ """Restart both services (use after `pipx upgrade` to run the new version)."""
663
+ from memor.service import restart
664
+ try:
665
+ typer.echo(restart())
666
+ except FileNotFoundError as e:
667
+ typer.echo(str(e), err=True)
668
+ raise typer.Exit(1)
669
+ except Exception as e:
670
+ typer.echo(f"Failed to restart service: {e}", err=True)
671
+ raise typer.Exit(1)
672
+
673
+
654
674
  @service_app.command("uninstall")
655
675
  def service_uninstall():
656
- """Stop and remove the background service."""
676
+ """Stop and remove the background services (daemon + dashboard)."""
657
677
  from memor.service import uninstall
658
678
  typer.echo(uninstall())
659
679
 
@@ -310,6 +310,8 @@
310
310
  .badge-codex::before { background: #3dd68c; }
311
311
  .badge-copilot { background: rgba(100,160,255,0.12); color: #64a0ff; }
312
312
  .badge-copilot::before { background: #64a0ff; }
313
+ .badge-cursor { background: rgba(192,132,252,0.12); color: #c084fc; }
314
+ .badge-cursor::before { background: #c084fc; }
313
315
 
314
316
  .quality-bar { display: flex; height: 6px; border-radius: 3px; overflow: hidden; background: var(--surface3); width: 100%; min-width: 60px; }
315
317
  .quality-fill { height: 100%; border-radius: 3px; transition: width 0.3s; }
@@ -613,6 +615,7 @@
613
615
  claude: ['badge-claude', 'Claude'],
614
616
  codex: ['badge-codex', 'Codex'],
615
617
  copilot: ['badge-copilot', 'Copilot'],
618
+ cursor: ['badge-cursor', 'Cursor'],
616
619
  };
617
620
  var m = map[agent || 'claude'] || ['badge-claude', agent || 'claude'];
618
621
  return '<span class="badge ' + m[0] + '">' + m[1] + '</span>';
@@ -1033,7 +1036,7 @@
1033
1036
  if (!data || data.length <= 1) { section.style.display = 'none'; return; }
1034
1037
  section.style.display = '';
1035
1038
  grid.innerHTML = '';
1036
- var colors = { claude: 'var(--accent)', codex: '#3dd68c', copilot: '#64a0ff' };
1039
+ var colors = { claude: 'var(--accent)', codex: '#3dd68c', copilot: '#64a0ff', cursor: '#c084fc' };
1037
1040
  data.forEach(function(row) {
1038
1041
  var agent = row.agent || 'claude';
1039
1042
  var color = colors[agent] || 'var(--text-muted)';
@@ -13,8 +13,7 @@ import re
13
13
  import time
14
14
  from dataclasses import dataclass
15
15
 
16
- from memor.types import Artifact, Scope
17
- from memor.retrieve.retriever import Retriever
16
+ from memor.types import Artifact
18
17
 
19
18
 
20
19
  class Outcome(enum.Enum):
@@ -114,20 +113,38 @@ def parse_verdict_json(raw: str) -> CounterfactualVerdict:
114
113
  )
115
114
 
116
115
 
117
- def run_case(case: CounterfactualCase, *, store, embedder, llm,
116
+ def run_case(case: CounterfactualCase, *, store, embedder, llm, db_path,
118
117
  k: int = 8) -> CounterfactualVerdict:
119
- scope = Scope(project=case.scope_project)
120
- r = Retriever(store, embedder, k=k)
121
- trace = r.query(case.query, scope)
118
+ """Judge a case using the PRODUCTION recall() path, not a bare Retriever.
119
+
120
+ This mirrors what the hook actually injects: same-session exclusion,
121
+ the 0.3/0.15 score threshold, the per-tier token budget, and 600-char
122
+ truncation. Without this, the eval over-recalls (self-recall echoes and
123
+ sub-threshold hits production would never inject), inflating ties and
124
+ producing same-session "losses" that cannot happen in production.
125
+ """
126
+ from memor.query_complexity import route_query, Tier
127
+ from memor.recall import recall
128
+
129
+ tier = route_query(case.query)
130
+ if tier == Tier.SKIP:
131
+ return CounterfactualVerdict(
132
+ outcome=Outcome.TIE,
133
+ reasoning="Production routes this query to SKIP — no recall would occur",
134
+ confidence=1.0)
135
+
136
+ result = recall(case.query, case.scope_project, db_path, embedder=embedder,
137
+ k=tier.k, threshold=0.15, max_tokens=tier.max_tokens,
138
+ session_id=case.session_id)
122
139
 
123
- if not trace.hits:
140
+ if not result.hit_ids:
124
141
  return CounterfactualVerdict(
125
142
  outcome=Outcome.TIE,
126
- reasoning="No context recalled — nothing to evaluate",
143
+ reasoning="No context recalled via production path — nothing to evaluate",
127
144
  confidence=1.0)
128
145
 
129
- recalled = "\n\n".join(
130
- f"[{h.artifact.kind}] {h.artifact.text}" for h in trace.hits)
146
+ # Judge exactly what production injects (post-exclusion/threshold/budget/truncation).
147
+ recalled = result.formatted_context
131
148
  holdout = "\n".join(case.holdout_texts)
132
149
 
133
150
  prompt = COUNTERFACTUAL_PROMPT.format(
@@ -136,11 +153,14 @@ def run_case(case: CounterfactualCase, *, store, embedder, llm,
136
153
  return parse_verdict_json(raw)
137
154
 
138
155
 
139
- def run_suite(cases: list[CounterfactualCase], *, store, embedder, llm,
156
+ def run_suite(cases: list[CounterfactualCase], *, store, embedder, llm, db_path,
140
157
  k: int = 8) -> dict:
141
158
  verdicts = []
142
- for c in cases:
143
- v = run_case(c, store=store, embedder=embedder, llm=llm, k=k)
159
+ n = len(cases)
160
+ for i, c in enumerate(cases, 1):
161
+ print(f" [{i}/{n}] judging case...", end="", flush=True)
162
+ v = run_case(c, store=store, embedder=embedder, llm=llm, db_path=db_path, k=k)
163
+ print(f" {v.outcome.value}", flush=True)
144
164
  verdicts.append((c, v))
145
165
  verdict_list = [v for _, v in verdicts]
146
166
  summary = summarize_verdicts(verdict_list)
@@ -17,11 +17,17 @@ def detect_agent(req: dict) -> str:
17
17
  event = req.get("hook_event_name", "")
18
18
  if event == "userPromptSubmitted":
19
19
  return "copilot"
20
- # A real Codex payload carries both `model` and a Codex-specific `turn_id`
21
- # extension; Claude Code sends neither. We match on EITHER (not both) so
22
- # detection still holds if a Codex version drops one field. Do NOT key on
23
- # `permission_mode`: it is a base field on every Claude Code hook input, so
24
- # it cannot discriminate between the two.
20
+ # Cursor fires a `beforeSubmitPrompt` hook and stamps every payload with a
21
+ # `cursor_version` field. It must be matched BEFORE the codex check below,
22
+ # because Cursor also sends `model` (e.g. "composer-2.5-fast") which would
23
+ # otherwise be mistaken for Codex.
24
+ if event == "beforeSubmitPrompt" or "cursor_version" in req:
25
+ return "cursor"
26
+ # A real Codex payload carries `model` and/or a Codex-specific `turn_id`
27
+ # extension; Claude Code sends neither. We match on EITHER so detection still
28
+ # holds if a Codex version drops one field. Do NOT key on `permission_mode`:
29
+ # it is a base field on every Claude Code hook input, so it cannot
30
+ # discriminate between the two.
25
31
  if "turn_id" in req or "model" in req:
26
32
  return "codex"
27
33
  return "claude"
@@ -68,6 +74,12 @@ def handle_request(req: dict, *, db_path: str = DEFAULT_DB,
68
74
  from memor.project import resolve_project
69
75
 
70
76
  cwd = req.get("cwd", "")
77
+ if not cwd:
78
+ # Cursor sends `workspace_roots` (a list) instead of `cwd`; fall back to
79
+ # the first root so the recall is scoped to the real project.
80
+ roots = req.get("workspace_roots") or []
81
+ if roots:
82
+ cwd = roots[0]
71
83
  project = resolve_project(cwd) if cwd else "unknown"
72
84
  query = req.get("prompt", "")
73
85
  session_id = req.get("session_id", "")
@@ -0,0 +1,45 @@
1
+ import time
2
+
3
+
4
+ class AnthropicLLM:
5
+ def __init__(self, model: str = "claude-opus-4-8", api_key: str | None = None,
6
+ *, max_retries: int = 8):
7
+ import anthropic
8
+
9
+ # Disable the SDK's own (silent) retries so our visible backoff below
10
+ # is the single source of waiting — otherwise the SDK sleeps on 429
11
+ # internally before raising, which looks like a hang.
12
+ self.client = anthropic.Anthropic(api_key=api_key, max_retries=0)
13
+ self.model = model
14
+ self.max_retries = max_retries
15
+
16
+ def complete(self, prompt: str, *, max_tokens: int = 1024) -> str:
17
+ import anthropic
18
+
19
+ attempt = 0
20
+ while True:
21
+ try:
22
+ msg = self.client.messages.create(
23
+ model=self.model,
24
+ max_tokens=max_tokens,
25
+ messages=[{"role": "user", "content": prompt}],
26
+ )
27
+ return "".join(b.text for b in msg.content if b.type == "text")
28
+ except anthropic.RateLimitError as e:
29
+ attempt += 1
30
+ if attempt > self.max_retries:
31
+ raise
32
+ # Honor Retry-After when present; otherwise wait out the
33
+ # per-minute token window (tier-1 limits reset each minute).
34
+ wait = _retry_after_seconds(e) or 60
35
+ print(f" rate limited (429); waiting {wait}s then retrying "
36
+ f"(attempt {attempt}/{self.max_retries})...", flush=True)
37
+ time.sleep(wait)
38
+
39
+
40
+ def _retry_after_seconds(err) -> float | None:
41
+ try:
42
+ val = err.response.headers.get("retry-after")
43
+ return float(val) if val is not None else None
44
+ except Exception:
45
+ return None
@@ -0,0 +1,27 @@
1
+ import httpx
2
+
3
+
4
+ class OpenAICompatLLM:
5
+ def __init__(self, base_url: str, api_key: str, model: str,
6
+ temperature: float | None = None):
7
+ self.base_url, self.api_key, self.model = base_url, api_key, model
8
+ # temperature=0 makes the eval judge deterministic/repeatable; None omits
9
+ # the field (server default) for normal use.
10
+ self.temperature = temperature
11
+
12
+ def complete(self, prompt: str, *, max_tokens: int = 1024) -> str:
13
+ payload = {
14
+ "model": self.model,
15
+ "max_tokens": max_tokens,
16
+ "messages": [{"role": "user", "content": prompt}],
17
+ }
18
+ if self.temperature is not None:
19
+ payload["temperature"] = self.temperature
20
+ r = httpx.post(
21
+ f"{self.base_url}/chat/completions",
22
+ headers={"Authorization": f"Bearer {self.api_key}"},
23
+ json=payload,
24
+ timeout=60,
25
+ )
26
+ r.raise_for_status()
27
+ return r.json()["choices"][0]["message"]["content"]
@@ -86,8 +86,10 @@ class Retriever:
86
86
  rel_min = min(rel_vals) if rel_vals else 0.0
87
87
  rel_range = ((max(rel_vals) - rel_min) if rel_vals else 1.0) or 1.0
88
88
 
89
- quality_cache = {}
90
- has_quality = hasattr(self.store, 'get_quality_score')
89
+ if hasattr(self.store, 'get_quality_scores'):
90
+ quality_scores = self.store.get_quality_scores(list(arts_by_id))
91
+ else:
92
+ quality_scores = {}
91
93
 
92
94
  for aid, a in arts_by_id.items():
93
95
  norm_rel = (fused.get(aid, 0.0) - rel_min) / rel_range
@@ -97,9 +99,7 @@ class Retriever:
97
99
 
98
100
  kind_boost = KIND_WEIGHTS.get(a.kind, 1.0) - 1.0
99
101
 
100
- if has_quality and aid not in quality_cache:
101
- quality_cache[aid] = self.store.get_quality_score(aid)
102
- quality = quality_cache.get(aid, 0.5)
102
+ quality = quality_scores.get(aid, 0.5)
103
103
 
104
104
  score = (self.w_sim * norm_rel + self.w_rec * recency
105
105
  + self.w_kind * kind_boost + self.w_qual * quality)