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