memor-cli 0.8.0__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 (107) hide show
  1. {memor_cli-0.8.0/memor_cli.egg-info → memor_cli-0.9.0}/PKG-INFO +9 -8
  2. {memor_cli-0.8.0 → memor_cli-0.9.0}/README.md +8 -7
  3. memor_cli-0.9.0/memor/__init__.py +1 -0
  4. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/dashboard/static/index.html +4 -1
  5. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/hook_server.py +17 -5
  6. memor_cli-0.9.0/memor/llm/openai_compat.py +27 -0
  7. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/retrieve/retriever.py +5 -5
  8. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/store/sqlite_store.py +19 -1
  9. {memor_cli-0.8.0 → memor_cli-0.9.0/memor_cli.egg-info}/PKG-INFO +9 -8
  10. {memor_cli-0.8.0 → memor_cli-0.9.0}/pyproject.toml +1 -1
  11. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_multi_agent_hook.py +44 -0
  12. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_store.py +22 -0
  13. memor_cli-0.8.0/memor/__init__.py +0 -1
  14. memor_cli-0.8.0/memor/llm/openai_compat.py +0 -20
  15. {memor_cli-0.8.0 → memor_cli-0.9.0}/LICENSE +0 -0
  16. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/cli.py +0 -0
  17. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/daemon.py +0 -0
  18. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/dashboard/__init__.py +0 -0
  19. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/dashboard/server.py +0 -0
  20. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/distill/__init__.py +0 -0
  21. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/distill/distiller.py +0 -0
  22. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/distill/extractive.py +0 -0
  23. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/embed/__init__.py +0 -0
  24. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/embed/api.py +0 -0
  25. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/embed/fake.py +0 -0
  26. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/embed/local.py +0 -0
  27. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/eval/__init__.py +0 -0
  28. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/eval/baselines/__init__.py +0 -0
  29. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/eval/baselines/base.py +0 -0
  30. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/eval/baselines/claude_mem.py +0 -0
  31. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/eval/baselines/graphiti.py +0 -0
  32. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/eval/counterfactual.py +0 -0
  33. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/eval/dataset.py +0 -0
  34. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/eval/embed_benchmark.py +0 -0
  35. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/eval/judge.py +0 -0
  36. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/eval/metrics.py +0 -0
  37. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/eval/runner.py +0 -0
  38. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/feedback.py +0 -0
  39. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/global_memories.py +0 -0
  40. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/hook_cli.py +0 -0
  41. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/ingest/__init__.py +0 -0
  42. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/ingest/claude_code.py +0 -0
  43. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/ingest/documents.py +0 -0
  44. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/interfaces.py +0 -0
  45. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/llm/__init__.py +0 -0
  46. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/llm/anthropic.py +0 -0
  47. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/llm/base.py +0 -0
  48. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/project.py +0 -0
  49. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/query_complexity.py +0 -0
  50. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/recall.py +0 -0
  51. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/redact.py +0 -0
  52. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/retrieve/__init__.py +0 -0
  53. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/service.py +0 -0
  54. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/session_context.py +0 -0
  55. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/store/__init__.py +0 -0
  56. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/tokencount.py +0 -0
  57. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/turn_metrics.py +0 -0
  58. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/types.py +0 -0
  59. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor_cli.egg-info/SOURCES.txt +0 -0
  60. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor_cli.egg-info/dependency_links.txt +0 -0
  61. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor_cli.egg-info/entry_points.txt +0 -0
  62. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor_cli.egg-info/requires.txt +0 -0
  63. {memor_cli-0.8.0 → memor_cli-0.9.0}/memor_cli.egg-info/top_level.txt +0 -0
  64. {memor_cli-0.8.0 → memor_cli-0.9.0}/setup.cfg +0 -0
  65. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_cli_smoke.py +0 -0
  66. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_counterfactual.py +0 -0
  67. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_daemon.py +0 -0
  68. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_dashboard.py +0 -0
  69. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_dashboard_quality.py +0 -0
  70. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_dataset_builder.py +0 -0
  71. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_dimension_safety.py +0 -0
  72. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_distiller.py +0 -0
  73. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_embed.py +0 -0
  74. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_embed_benchmark.py +0 -0
  75. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_eval_ablation.py +0 -0
  76. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_eval_runner.py +0 -0
  77. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_external_baselines.py +0 -0
  78. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_extractive.py +0 -0
  79. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_feedback.py +0 -0
  80. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_global_memories.py +0 -0
  81. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_hook.py +0 -0
  82. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_hook_server.py +0 -0
  83. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_hybrid_retrieval.py +0 -0
  84. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_ingest_claude_code.py +0 -0
  85. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_ingest_documents.py +0 -0
  86. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_install_hook.py +0 -0
  87. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_interfaces.py +0 -0
  88. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_judge.py +0 -0
  89. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_metrics.py +0 -0
  90. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_negative_feedback.py +0 -0
  91. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_noise_filter.py +0 -0
  92. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_project_resolver.py +0 -0
  93. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_quality_gate.py +0 -0
  94. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_query_complexity.py +0 -0
  95. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_recall_core.py +0 -0
  96. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_redact.py +0 -0
  97. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_retriever.py +0 -0
  98. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_roi_trend.py +0 -0
  99. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_semantic_feedback.py +0 -0
  100. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_service.py +0 -0
  101. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_session_context.py +0 -0
  102. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_skill_recall.py +0 -0
  103. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_supersession.py +0 -0
  104. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_tokencount.py +0 -0
  105. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_turn_metrics.py +0 -0
  106. {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_types.py +0 -0
  107. {memor_cli-0.8.0 → 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.8.0
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
 
@@ -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
@@ -283,7 +284,7 @@ memor/
283
284
  +-- judge.py LLM-as-judge evaluation
284
285
  +-- embed_benchmark.py Embedding model comparison
285
286
 
286
- 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
287
288
  memor/hook_server.py Hook server with agent detection + response formatting
288
289
  skill/recall.py Standalone recall script
289
290
  ```
@@ -331,7 +332,7 @@ cd memor-ai
331
332
  python3 -m venv .venv && source .venv/bin/activate
332
333
  pip install -e ".[dev]"
333
334
 
334
- pytest # 298 tests
335
+ pytest # 304 tests
335
336
  ```
336
337
 
337
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
 
@@ -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
@@ -246,7 +247,7 @@ memor/
246
247
  +-- judge.py LLM-as-judge evaluation
247
248
  +-- embed_benchmark.py Embedding model comparison
248
249
 
249
- 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
250
251
  memor/hook_server.py Hook server with agent detection + response formatting
251
252
  skill/recall.py Standalone recall script
252
253
  ```
@@ -294,7 +295,7 @@ cd memor-ai
294
295
  python3 -m venv .venv && source .venv/bin/activate
295
296
  pip install -e ".[dev]"
296
297
 
297
- pytest # 298 tests
298
+ pytest # 304 tests
298
299
  ```
299
300
 
300
301
  ---
@@ -0,0 +1 @@
1
+ __version__ = "0.9.0"
@@ -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)';
@@ -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,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)
@@ -170,8 +170,13 @@ class SqliteStore:
170
170
  text=r["text"], token_count=r["token_count"], created_at=r["created_at"],
171
171
  meta=json.loads(r["meta"]))
172
172
 
173
+ # sqlite-vec caps the KNN `k` parameter at this value.
174
+ _VEC_KNN_LIMIT = 4096
175
+
173
176
  def search(self, vector: list[float], scope: Scope, k: int) -> list[tuple[Artifact, float]]:
174
177
  from memor.types import GLOBAL_PROJECT
178
+ # Cap the KNN fetch at sqlite-vec's hard limit so large k can't error.
179
+ fetch = min(max(k * 20, 200), self._VEC_KNN_LIMIT)
175
180
  rows = self.db.execute(f"""
176
181
  SELECT a.*, v.distance AS distance
177
182
  FROM (SELECT rowid, distance FROM vec_artifacts
@@ -182,7 +187,7 @@ class SqliteStore:
182
187
  AND (? IS NULL OR a.created_at >= ?)
183
188
  AND (? IS NULL OR a.created_at <= ?)
184
189
  ORDER BY v.distance ASC
185
- """, (_serialize(vector), max(k*20, 200),
190
+ """, (_serialize(vector), fetch,
186
191
  scope.project, scope.project, GLOBAL_PROJECT,
187
192
  scope.since, scope.since,
188
193
  scope.until, scope.until)).fetchall()
@@ -457,6 +462,19 @@ class SqliteStore:
457
462
  (artifact_id,)).fetchone()
458
463
  return row["quality_score"] if row else 0.5
459
464
 
465
+ def get_quality_scores(self, artifact_ids: list[str]) -> dict[str, float]:
466
+ """Batch form of get_quality_score: one query for the whole candidate
467
+ set (callers default missing ids to 0.5). Keeps the widened retrieval
468
+ pool off the N+1-query path on the per-prompt hot path."""
469
+ ids = list(artifact_ids)
470
+ if not ids:
471
+ return {}
472
+ qmarks = ",".join("?" * len(ids))
473
+ rows = self.db.execute(
474
+ f"SELECT artifact_id, quality_score FROM memory_quality "
475
+ f"WHERE artifact_id IN ({qmarks})", ids).fetchall()
476
+ return {r["artifact_id"]: r["quality_score"] for r in rows}
477
+
460
478
  def get_stale_memories(self, days: int = 30) -> list[str]:
461
479
  import time as _time
462
480
  cutoff = _time.time() - (days * 86400)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memor-cli
3
- Version: 0.8.0
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
 
@@ -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
@@ -283,7 +284,7 @@ memor/
283
284
  +-- judge.py LLM-as-judge evaluation
284
285
  +-- embed_benchmark.py Embedding model comparison
285
286
 
286
- 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
287
288
  memor/hook_server.py Hook server with agent detection + response formatting
288
289
  skill/recall.py Standalone recall script
289
290
  ```
@@ -331,7 +332,7 @@ cd memor-ai
331
332
  python3 -m venv .venv && source .venv/bin/activate
332
333
  pip install -e ".[dev]"
333
334
 
334
- pytest # 298 tests
335
+ pytest # 304 tests
335
336
  ```
336
337
 
337
338
  ---
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "memor-cli"
7
- version = "0.8.0"
7
+ version = "0.9.0"
8
8
  description = "Measured memory for coding agents. Fire and forget — no API keys needed."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -44,6 +44,34 @@ def test_detect_copilot():
44
44
  assert detect_agent(req) == "copilot"
45
45
 
46
46
 
47
+ def test_detect_cursor():
48
+ # Real Cursor 3.7.36 beforeSubmitPrompt payload (captured from cursor.hooks
49
+ # log). Note it carries `model` ("composer-2.5-fast") and a unique
50
+ # `cursor_version`, and uses `workspace_roots` instead of `cwd`.
51
+ req = {"conversation_id": "c1", "generation_id": "g1",
52
+ "model": "composer-2.5-fast", "composer_mode": "agent",
53
+ "prompt": "kill the background terminals please", "session_id": "c1",
54
+ "hook_event_name": "beforeSubmitPrompt", "cursor_version": "3.7.36",
55
+ "workspace_roots": ["/proj"]}
56
+ assert detect_agent(req) == "cursor"
57
+
58
+
59
+ def test_detect_cursor_by_version_field():
60
+ # Defensive: cursor_version alone identifies Cursor even if the event name
61
+ # ever changes.
62
+ req = {"prompt": "x", "session_id": "c1", "cursor_version": "3.8.0",
63
+ "hook_event_name": "UserPromptSubmit", "model": "composer-2.5"}
64
+ assert detect_agent(req) == "cursor"
65
+
66
+
67
+ def test_cursor_model_is_not_mistaken_for_codex():
68
+ # Regression: Cursor sends `model`, but it must NOT fall into the codex
69
+ # branch — the cursor markers win.
70
+ req = {"prompt": "x", "hook_event_name": "beforeSubmitPrompt",
71
+ "model": "composer-2.5-fast", "workspace_roots": ["/proj"]}
72
+ assert detect_agent(req) == "cursor"
73
+
74
+
47
75
  def test_detect_unknown_defaults_to_claude():
48
76
  req = {"cwd": "/proj", "prompt": "hello"}
49
77
  assert detect_agent(req) == "claude"
@@ -115,6 +143,22 @@ def test_handle_request_copilot_format(tmp_path):
115
143
  assert "hookSpecificOutput" not in resp
116
144
 
117
145
 
146
+ def test_handle_request_cursor_resolves_project_from_workspace_roots(tmp_path):
147
+ """Cursor sends `workspace_roots` instead of `cwd`. handle_request must fall
148
+ back to it so the recall is scoped to the real project (not 'unknown') and
149
+ logged as the cursor agent."""
150
+ db_path, e = _make_db(tmp_path)
151
+ req = {"prompt": "password hashing", "session_id": "c1",
152
+ "hook_event_name": "beforeSubmitPrompt", "cursor_version": "3.7.36",
153
+ "model": "composer-2.5-fast", "workspace_roots": ["/proj"]}
154
+ handle_request(req, db_path=db_path, embedder=e)
155
+ store = SqliteStore(db_path, dim=16)
156
+ row = store.db.execute(
157
+ "SELECT project, agent FROM recall_log ORDER BY id DESC LIMIT 1").fetchone()
158
+ assert row["project"] == "proj"
159
+ assert row["agent"] == "cursor"
160
+
161
+
118
162
  # --- install-hook config generation ---
119
163
 
120
164
  def test_install_hook_codex(tmp_path):
@@ -32,3 +32,25 @@ def test_deactivate_excludes_from_search(tmp_path):
32
32
  s.deactivate("old", superseded_by="new")
33
33
  ids = [a.id for a, _ in s.search(e.embed(["use library"])[0], Scope(project="p"), k=5)]
34
34
  assert "old" not in ids and "new" in ids
35
+
36
+
37
+ # --- KNN-fetch cap + batched quality lookup (safe retrieval wins) ---
38
+
39
+ def test_search_knn_fetch_capped(tmp_path):
40
+ # A large k must not exceed sqlite-vec's internal knn limit (4096).
41
+ e = FakeEmbedder(dim=16)
42
+ s = SqliteStore(str(tmp_path / "m.db"), dim=16)
43
+ s.add_artifacts([make("a", "p", "hello world", 1)], e.embed(["hello world"]))
44
+ hits = s.search(e.embed(["hello world"])[0], Scope(project="p"), k=300)
45
+ assert len(hits) <= 1 # no OperationalError, returns what's available
46
+
47
+
48
+ def test_get_quality_scores_batch_matches_per_id(tmp_path):
49
+ s = SqliteStore(str(tmp_path / "m.db"), dim=16)
50
+ s.record_recall(["a", "b"])
51
+ s.record_usage(["a"])
52
+ scores = s.get_quality_scores(["a", "b", "missing"])
53
+ assert scores["a"] == s.get_quality_score("a")
54
+ assert scores["b"] == s.get_quality_score("b")
55
+ assert "missing" not in scores
56
+ assert s.get_quality_scores([]) == {}
@@ -1 +0,0 @@
1
- __version__ = "0.8.0"
@@ -1,20 +0,0 @@
1
- import httpx
2
-
3
-
4
- class OpenAICompatLLM:
5
- def __init__(self, base_url: str, api_key: str, model: str):
6
- self.base_url, self.api_key, self.model = base_url, api_key, model
7
-
8
- def complete(self, prompt: str, *, max_tokens: int = 1024) -> str:
9
- r = httpx.post(
10
- f"{self.base_url}/chat/completions",
11
- headers={"Authorization": f"Bearer {self.api_key}"},
12
- json={
13
- "model": self.model,
14
- "max_tokens": max_tokens,
15
- "messages": [{"role": "user", "content": prompt}],
16
- },
17
- timeout=60,
18
- )
19
- r.raise_for_status()
20
- return r.json()["choices"][0]["message"]["content"]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes