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.
- {memor_cli-0.8.0/memor_cli.egg-info → memor_cli-0.9.0}/PKG-INFO +9 -8
- {memor_cli-0.8.0 → memor_cli-0.9.0}/README.md +8 -7
- memor_cli-0.9.0/memor/__init__.py +1 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/dashboard/static/index.html +4 -1
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/hook_server.py +17 -5
- memor_cli-0.9.0/memor/llm/openai_compat.py +27 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/retrieve/retriever.py +5 -5
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/store/sqlite_store.py +19 -1
- {memor_cli-0.8.0 → memor_cli-0.9.0/memor_cli.egg-info}/PKG-INFO +9 -8
- {memor_cli-0.8.0 → memor_cli-0.9.0}/pyproject.toml +1 -1
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_multi_agent_hook.py +44 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_store.py +22 -0
- memor_cli-0.8.0/memor/__init__.py +0 -1
- memor_cli-0.8.0/memor/llm/openai_compat.py +0 -20
- {memor_cli-0.8.0 → memor_cli-0.9.0}/LICENSE +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/cli.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/daemon.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/dashboard/__init__.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/dashboard/server.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/distill/__init__.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/distill/distiller.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/distill/extractive.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/embed/__init__.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/embed/api.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/embed/fake.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/embed/local.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/eval/__init__.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/eval/baselines/__init__.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/eval/baselines/base.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/eval/baselines/claude_mem.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/eval/baselines/graphiti.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/eval/counterfactual.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/eval/dataset.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/eval/embed_benchmark.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/eval/judge.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/eval/metrics.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/eval/runner.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/feedback.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/global_memories.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/hook_cli.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/ingest/__init__.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/ingest/claude_code.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/ingest/documents.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/interfaces.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/llm/__init__.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/llm/anthropic.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/llm/base.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/project.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/query_complexity.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/recall.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/redact.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/retrieve/__init__.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/service.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/session_context.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/store/__init__.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/tokencount.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/turn_metrics.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor/types.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor_cli.egg-info/SOURCES.txt +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor_cli.egg-info/dependency_links.txt +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor_cli.egg-info/entry_points.txt +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor_cli.egg-info/requires.txt +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/memor_cli.egg-info/top_level.txt +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/setup.cfg +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_cli_smoke.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_counterfactual.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_daemon.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_dashboard.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_dashboard_quality.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_dataset_builder.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_dimension_safety.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_distiller.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_embed.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_embed_benchmark.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_eval_ablation.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_eval_runner.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_external_baselines.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_extractive.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_feedback.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_global_memories.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_hook.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_hook_server.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_hybrid_retrieval.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_ingest_claude_code.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_ingest_documents.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_install_hook.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_interfaces.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_judge.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_metrics.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_negative_feedback.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_noise_filter.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_project_resolver.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_quality_gate.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_query_complexity.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_recall_core.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_redact.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_retriever.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_roi_trend.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_semantic_feedback.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_service.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_session_context.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_skill_recall.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_supersession.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_tokencount.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_turn_metrics.py +0 -0
- {memor_cli-0.8.0 → memor_cli-0.9.0}/tests/test_types.py +0 -0
- {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.
|
|
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
|
[]()
|
|
51
51
|
[](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 #
|
|
335
|
+
pytest # 304 tests
|
|
335
336
|
```
|
|
336
337
|
|
|
337
338
|
---
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
[]()
|
|
14
14
|
[](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
|
+
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
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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),
|
|
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.
|
|
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
|
[]()
|
|
51
51
|
[](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 #
|
|
335
|
+
pytest # 304 tests
|
|
335
336
|
```
|
|
336
337
|
|
|
337
338
|
---
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|