memor-cli 0.7.1__tar.gz → 0.8.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.8.0}/PKG-INFO +10 -7
- {memor_cli-0.7.1 → memor_cli-0.8.0}/README.md +9 -6
- memor_cli-0.8.0/memor/__init__.py +1 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/cli.py +30 -10
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/eval/counterfactual.py +33 -13
- memor_cli-0.8.0/memor/llm/anthropic.py +45 -0
- memor_cli-0.8.0/memor/service.py +297 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0/memor_cli.egg-info}/PKG-INFO +10 -7
- {memor_cli-0.7.1 → memor_cli-0.8.0}/pyproject.toml +1 -1
- memor_cli-0.8.0/tests/test_service.py +127 -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/service.py +0 -187
- memor_cli-0.7.1/tests/test_service.py +0 -25
- {memor_cli-0.7.1 → memor_cli-0.8.0}/LICENSE +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/daemon.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/dashboard/__init__.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/dashboard/server.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/dashboard/static/index.html +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/distill/__init__.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/distill/distiller.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/distill/extractive.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/embed/__init__.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/embed/api.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/embed/fake.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/embed/local.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/eval/__init__.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/eval/baselines/__init__.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/eval/baselines/base.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/eval/baselines/claude_mem.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/eval/baselines/graphiti.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/eval/dataset.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/eval/embed_benchmark.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/eval/judge.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/eval/metrics.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/eval/runner.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/feedback.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/global_memories.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/hook_cli.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/hook_server.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/ingest/__init__.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/ingest/claude_code.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/ingest/documents.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/interfaces.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/llm/__init__.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/llm/base.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/llm/openai_compat.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/project.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/query_complexity.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/recall.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/redact.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/retrieve/__init__.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/retrieve/retriever.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/session_context.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/store/__init__.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/store/sqlite_store.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/tokencount.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/turn_metrics.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor/types.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor_cli.egg-info/SOURCES.txt +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor_cli.egg-info/dependency_links.txt +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor_cli.egg-info/entry_points.txt +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor_cli.egg-info/requires.txt +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/memor_cli.egg-info/top_level.txt +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/setup.cfg +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_cli_smoke.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_counterfactual.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_daemon.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_dashboard.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_dashboard_quality.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_dataset_builder.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_dimension_safety.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_distiller.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_embed.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_embed_benchmark.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_eval_ablation.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_eval_runner.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_external_baselines.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_extractive.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_feedback.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_global_memories.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_hook.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_hook_server.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_hybrid_retrieval.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_ingest_claude_code.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_ingest_documents.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_install_hook.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_interfaces.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_judge.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_metrics.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_multi_agent_hook.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_negative_feedback.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_noise_filter.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_project_resolver.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_quality_gate.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_query_complexity.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_recall_core.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_redact.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_retriever.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_roi_trend.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_semantic_feedback.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_session_context.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_skill_recall.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_store.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_supersession.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_tokencount.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_turn_metrics.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.0}/tests/test_types.py +0 -0
- {memor_cli-0.7.1 → memor_cli-0.8.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.8.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
|
|
@@ -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
|
|
@@ -218,10 +218,13 @@ memor install-hook Install hook + download model (interactive
|
|
|
218
218
|
memor daemon Auto-ingest + distill (background watcher)
|
|
219
219
|
memor dashboard Web dashboard on localhost:8420
|
|
220
220
|
memor version Print installed version
|
|
221
|
-
memor service install Run daemon as background
|
|
222
|
-
|
|
223
|
-
memor service
|
|
224
|
-
memor service
|
|
221
|
+
memor service install Run daemon + dashboard as background services (launchd/systemd)
|
|
222
|
+
--no-dashboard Install only the daemon
|
|
223
|
+
memor service restart Restart both services (use after `pipx upgrade`)
|
|
224
|
+
memor service stop Stop both background services
|
|
225
|
+
memor service uninstall Remove both background services
|
|
226
|
+
memor service status Show daemon + dashboard status
|
|
227
|
+
(dashboard port: set MEMOR_DASHBOARD_PORT, default 8420)
|
|
225
228
|
memor query <text> Search memories from the CLI
|
|
226
229
|
memor reingest Wipe DB and re-ingest everything
|
|
227
230
|
memor reingest --project <name> Re-ingest only one project
|
|
@@ -328,7 +331,7 @@ cd memor-ai
|
|
|
328
331
|
python3 -m venv .venv && source .venv/bin/activate
|
|
329
332
|
pip install -e ".[dev]"
|
|
330
333
|
|
|
331
|
-
pytest #
|
|
334
|
+
pytest # 298 tests
|
|
332
335
|
```
|
|
333
336
|
|
|
334
337
|
---
|
|
@@ -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
|
|
@@ -181,10 +181,13 @@ memor install-hook Install hook + download model (interactive
|
|
|
181
181
|
memor daemon Auto-ingest + distill (background watcher)
|
|
182
182
|
memor dashboard Web dashboard on localhost:8420
|
|
183
183
|
memor version Print installed version
|
|
184
|
-
memor service install Run daemon as background
|
|
185
|
-
|
|
186
|
-
memor service
|
|
187
|
-
memor service
|
|
184
|
+
memor service install Run daemon + dashboard as background services (launchd/systemd)
|
|
185
|
+
--no-dashboard Install only the daemon
|
|
186
|
+
memor service restart Restart both services (use after `pipx upgrade`)
|
|
187
|
+
memor service stop Stop both background services
|
|
188
|
+
memor service uninstall Remove both background services
|
|
189
|
+
memor service status Show daemon + dashboard status
|
|
190
|
+
(dashboard port: set MEMOR_DASHBOARD_PORT, default 8420)
|
|
188
191
|
memor query <text> Search memories from the CLI
|
|
189
192
|
memor reingest Wipe DB and re-ingest everything
|
|
190
193
|
memor reingest --project <name> Re-ingest only one project
|
|
@@ -291,7 +294,7 @@ cd memor-ai
|
|
|
291
294
|
python3 -m venv .venv && source .venv/bin/activate
|
|
292
295
|
pip install -e ".[dev]"
|
|
293
296
|
|
|
294
|
-
pytest #
|
|
297
|
+
pytest # 298 tests
|
|
295
298
|
```
|
|
296
299
|
|
|
297
300
|
---
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.8.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
|
|
|
@@ -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)
|
|
@@ -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,297 @@
|
|
|
1
|
+
"""Manage memor background services (daemon + dashboard) as launchd (macOS)
|
|
2
|
+
or systemd (Linux) user services.
|
|
3
|
+
|
|
4
|
+
Two units are managed together so they cycle in lockstep:
|
|
5
|
+
- daemon (ai.memor.daemon) — ingest + distill
|
|
6
|
+
- dashboard (ai.memor.dashboard) — web UI on MEMOR_DASHBOARD_PORT (default 8420)
|
|
7
|
+
|
|
8
|
+
install/stop/uninstall/status/restart all operate on both units, so stopping or
|
|
9
|
+
reinstalling recycles the dashboard alongside the daemon. `install(with_dashboard=False)`
|
|
10
|
+
installs the daemon only.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import platform
|
|
16
|
+
import shutil
|
|
17
|
+
import subprocess
|
|
18
|
+
import textwrap
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
DAEMON_LABEL = "ai.memor.daemon"
|
|
22
|
+
DASHBOARD_LABEL = "ai.memor.dashboard"
|
|
23
|
+
# Back-compat alias.
|
|
24
|
+
LABEL = DAEMON_LABEL
|
|
25
|
+
|
|
26
|
+
STATE_DIR = Path.home() / ".memor"
|
|
27
|
+
DAEMON_LOG = STATE_DIR / "daemon.log"
|
|
28
|
+
DASHBOARD_LOG = STATE_DIR / "dashboard.log"
|
|
29
|
+
LOG_FILE = DAEMON_LOG # back-compat alias
|
|
30
|
+
|
|
31
|
+
# macOS
|
|
32
|
+
PLIST_DIR = Path.home() / "Library" / "LaunchAgents"
|
|
33
|
+
# Linux
|
|
34
|
+
SYSTEMD_DIR = Path.home() / ".config" / "systemd" / "user"
|
|
35
|
+
|
|
36
|
+
DEFAULT_DASHBOARD_PORT = 8420
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _is_macos() -> bool:
|
|
40
|
+
return platform.system() == "Darwin"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _find_memor_bin() -> str:
|
|
44
|
+
path = shutil.which("memor")
|
|
45
|
+
if not path:
|
|
46
|
+
raise FileNotFoundError(
|
|
47
|
+
"'memor' not found on PATH. Reinstall with: pipx install memor-cli"
|
|
48
|
+
)
|
|
49
|
+
return path
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _dashboard_port() -> int:
|
|
53
|
+
try:
|
|
54
|
+
p = int(os.environ.get("MEMOR_DASHBOARD_PORT", str(DEFAULT_DASHBOARD_PORT)))
|
|
55
|
+
return p if p > 0 else DEFAULT_DASHBOARD_PORT
|
|
56
|
+
except (ValueError, TypeError):
|
|
57
|
+
return DEFAULT_DASHBOARD_PORT
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _units(memor_bin: str, *, with_dashboard: bool = True, port: int | None = None) -> list[dict]:
|
|
61
|
+
"""Describe the services to manage. Each entry has the launchd label,
|
|
62
|
+
systemd unit name, program args (after the memor binary), and log file."""
|
|
63
|
+
if port is None:
|
|
64
|
+
port = _dashboard_port()
|
|
65
|
+
units = [{
|
|
66
|
+
"key": "daemon",
|
|
67
|
+
"label": DAEMON_LABEL,
|
|
68
|
+
"systemd_name": "memor-daemon",
|
|
69
|
+
"description": "Memor daemon — memory layer for coding agents",
|
|
70
|
+
"args": [memor_bin, "daemon"],
|
|
71
|
+
"log": DAEMON_LOG,
|
|
72
|
+
}]
|
|
73
|
+
if with_dashboard:
|
|
74
|
+
units.append({
|
|
75
|
+
"key": "dashboard",
|
|
76
|
+
"label": DASHBOARD_LABEL,
|
|
77
|
+
"systemd_name": "memor-dashboard",
|
|
78
|
+
"description": "Memor dashboard — web UI for memory metrics",
|
|
79
|
+
"args": [memor_bin, "dashboard", "--port", str(port), "--no-open"],
|
|
80
|
+
"log": DASHBOARD_LOG,
|
|
81
|
+
})
|
|
82
|
+
return units
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _all_unit_labels() -> list[tuple[str, str]]:
|
|
86
|
+
"""(launchd label, systemd name) for every unit we might have installed,
|
|
87
|
+
used by uninstall/stop/status which must act regardless of with_dashboard."""
|
|
88
|
+
return [(DAEMON_LABEL, "memor-daemon"), (DASHBOARD_LABEL, "memor-dashboard")]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _plist_path(label: str) -> Path:
|
|
92
|
+
return PLIST_DIR / f"{label}.plist"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _unit_path(systemd_name: str) -> Path:
|
|
96
|
+
return SYSTEMD_DIR / f"{systemd_name}.service"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _plist_content(label: str, args, log_file) -> str:
|
|
100
|
+
if isinstance(args, str): # back-compat: old callers passed just the memor bin
|
|
101
|
+
args = [args, "daemon"]
|
|
102
|
+
arg_xml = "\n".join(f" <string>{a}</string>" for a in args)
|
|
103
|
+
return textwrap.dedent(f"""\
|
|
104
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
105
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
106
|
+
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
107
|
+
<plist version="1.0">
|
|
108
|
+
<dict>
|
|
109
|
+
<key>Label</key>
|
|
110
|
+
<string>{label}</string>
|
|
111
|
+
<key>ProgramArguments</key>
|
|
112
|
+
<array>
|
|
113
|
+
{arg_xml}
|
|
114
|
+
</array>
|
|
115
|
+
<key>RunAtLoad</key>
|
|
116
|
+
<true/>
|
|
117
|
+
<key>KeepAlive</key>
|
|
118
|
+
<true/>
|
|
119
|
+
<key>StandardOutPath</key>
|
|
120
|
+
<string>{log_file}</string>
|
|
121
|
+
<key>StandardErrorPath</key>
|
|
122
|
+
<string>{log_file}</string>
|
|
123
|
+
<key>ProcessType</key>
|
|
124
|
+
<string>Background</string>
|
|
125
|
+
</dict>
|
|
126
|
+
</plist>
|
|
127
|
+
""")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _systemd_unit(label, description="Memor daemon — memory layer for coding agents",
|
|
131
|
+
args=None, log_file=LOG_FILE) -> str:
|
|
132
|
+
if args is None: # back-compat: old signature was _systemd_unit(memor_bin)
|
|
133
|
+
args = [label, "daemon"]
|
|
134
|
+
exec_start = " ".join(str(a) for a in args)
|
|
135
|
+
return textwrap.dedent(f"""\
|
|
136
|
+
[Unit]
|
|
137
|
+
Description={description}
|
|
138
|
+
After=default.target
|
|
139
|
+
|
|
140
|
+
[Service]
|
|
141
|
+
Type=simple
|
|
142
|
+
ExecStart={exec_start}
|
|
143
|
+
Restart=on-failure
|
|
144
|
+
RestartSec=10
|
|
145
|
+
StandardOutput=append:{log_file}
|
|
146
|
+
StandardError=append:{log_file}
|
|
147
|
+
|
|
148
|
+
[Install]
|
|
149
|
+
WantedBy=default.target
|
|
150
|
+
""")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _port_in_use(port: int) -> bool:
|
|
154
|
+
import socket
|
|
155
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
156
|
+
s.settimeout(0.2)
|
|
157
|
+
return s.connect_ex(("127.0.0.1", port)) == 0
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def install(with_dashboard: bool = True) -> str:
|
|
161
|
+
memor_bin = _find_memor_bin()
|
|
162
|
+
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
|
163
|
+
port = _dashboard_port()
|
|
164
|
+
units = _units(memor_bin, with_dashboard=with_dashboard, port=port)
|
|
165
|
+
|
|
166
|
+
warnings = []
|
|
167
|
+
if with_dashboard and _port_in_use(port):
|
|
168
|
+
warnings.append(
|
|
169
|
+
f" warning: port {port} is already in use — the dashboard service may "
|
|
170
|
+
f"crash-loop. Stop the other process or set MEMOR_DASHBOARD_PORT.")
|
|
171
|
+
|
|
172
|
+
lines = []
|
|
173
|
+
if _is_macos():
|
|
174
|
+
PLIST_DIR.mkdir(parents=True, exist_ok=True)
|
|
175
|
+
for u in units:
|
|
176
|
+
path = _plist_path(u["label"])
|
|
177
|
+
path.write_text(_plist_content(u["label"], u["args"], u["log"]))
|
|
178
|
+
subprocess.run(["launchctl", "bootout", f"gui/{os.getuid()}", str(path)],
|
|
179
|
+
capture_output=True)
|
|
180
|
+
subprocess.run(["launchctl", "bootstrap", f"gui/{os.getuid()}", str(path)],
|
|
181
|
+
check=True)
|
|
182
|
+
lines.append(f" {u['key']}: {path}")
|
|
183
|
+
header = "Installed and started launchd services:"
|
|
184
|
+
else:
|
|
185
|
+
SYSTEMD_DIR.mkdir(parents=True, exist_ok=True)
|
|
186
|
+
for u in units:
|
|
187
|
+
path = _unit_path(u["systemd_name"])
|
|
188
|
+
path.write_text(_systemd_unit(u["label"], u["description"], u["args"], u["log"]))
|
|
189
|
+
lines.append(f" {u['key']}: {path}")
|
|
190
|
+
subprocess.run(["systemctl", "--user", "daemon-reload"], check=True)
|
|
191
|
+
for u in units:
|
|
192
|
+
subprocess.run(["systemctl", "--user", "enable", "--now", u["systemd_name"]],
|
|
193
|
+
check=True)
|
|
194
|
+
header = "Installed and started systemd user services:"
|
|
195
|
+
|
|
196
|
+
out = [header, *lines]
|
|
197
|
+
if with_dashboard:
|
|
198
|
+
out.append(f" dashboard: http://localhost:{port}")
|
|
199
|
+
if warnings:
|
|
200
|
+
out.extend(warnings)
|
|
201
|
+
return "\n".join(out)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def uninstall() -> str:
|
|
205
|
+
removed = []
|
|
206
|
+
if _is_macos():
|
|
207
|
+
for label, _ in _all_unit_labels():
|
|
208
|
+
path = _plist_path(label)
|
|
209
|
+
if path.exists():
|
|
210
|
+
subprocess.run(["launchctl", "bootout", f"gui/{os.getuid()}", str(path)],
|
|
211
|
+
capture_output=True)
|
|
212
|
+
path.unlink()
|
|
213
|
+
removed.append(str(path))
|
|
214
|
+
else:
|
|
215
|
+
changed = False
|
|
216
|
+
for _, name in _all_unit_labels():
|
|
217
|
+
path = _unit_path(name)
|
|
218
|
+
if path.exists():
|
|
219
|
+
subprocess.run(["systemctl", "--user", "disable", "--now", name],
|
|
220
|
+
capture_output=True)
|
|
221
|
+
path.unlink()
|
|
222
|
+
removed.append(str(path))
|
|
223
|
+
changed = True
|
|
224
|
+
if changed:
|
|
225
|
+
subprocess.run(["systemctl", "--user", "daemon-reload"], capture_output=True)
|
|
226
|
+
if not removed:
|
|
227
|
+
return "No services installed."
|
|
228
|
+
return "Services stopped and removed:\n" + "\n".join(f" deleted: {p}" for p in removed)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def stop() -> str:
|
|
232
|
+
stopped = []
|
|
233
|
+
if _is_macos():
|
|
234
|
+
for label, _ in _all_unit_labels():
|
|
235
|
+
path = _plist_path(label)
|
|
236
|
+
if path.exists():
|
|
237
|
+
r = subprocess.run(["launchctl", "bootout", f"gui/{os.getuid()}", str(path)],
|
|
238
|
+
capture_output=True, text=True)
|
|
239
|
+
stopped.append(label if r.returncode == 0 else f"{label} (failed)")
|
|
240
|
+
else:
|
|
241
|
+
for _, name in _all_unit_labels():
|
|
242
|
+
if _unit_path(name).exists():
|
|
243
|
+
r = subprocess.run(["systemctl", "--user", "stop", name],
|
|
244
|
+
capture_output=True, text=True)
|
|
245
|
+
stopped.append(name if r.returncode == 0 else f"{name} (failed)")
|
|
246
|
+
if not stopped:
|
|
247
|
+
return "No services installed. Nothing to stop."
|
|
248
|
+
return ("Stopped: " + ", ".join(stopped) +
|
|
249
|
+
"\n They restart on next login. To remove: memor service uninstall")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def restart() -> str:
|
|
253
|
+
"""Stop and reinstall both units — use after `pipx upgrade` to recycle them
|
|
254
|
+
onto the new binary (the running processes keep old code until restarted)."""
|
|
255
|
+
stop()
|
|
256
|
+
return install()
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _macos_unit_status(label: str) -> str:
|
|
260
|
+
path = _plist_path(label)
|
|
261
|
+
if not path.exists():
|
|
262
|
+
return "not installed"
|
|
263
|
+
r = subprocess.run(["launchctl", "print", f"gui/{os.getuid()}/{label}"],
|
|
264
|
+
capture_output=True, text=True)
|
|
265
|
+
if r.returncode != 0:
|
|
266
|
+
return "installed, not running"
|
|
267
|
+
for line in r.stdout.splitlines():
|
|
268
|
+
s = line.strip()
|
|
269
|
+
if s.startswith("pid ="):
|
|
270
|
+
return f"running (pid {s.split('=')[1].strip()})"
|
|
271
|
+
return "running"
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _linux_unit_status(name: str) -> str:
|
|
275
|
+
if not _unit_path(name).exists():
|
|
276
|
+
return "not installed"
|
|
277
|
+
r = subprocess.run(["systemctl", "--user", "is-active", name],
|
|
278
|
+
capture_output=True, text=True)
|
|
279
|
+
state = r.stdout.strip()
|
|
280
|
+
if state == "active":
|
|
281
|
+
pid_r = subprocess.run(["systemctl", "--user", "show", "-p", "MainPID", name],
|
|
282
|
+
capture_output=True, text=True)
|
|
283
|
+
return f"running (pid {pid_r.stdout.strip().split('=')[-1]})"
|
|
284
|
+
return f"installed, {state}"
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def status() -> str:
|
|
288
|
+
rows = []
|
|
289
|
+
for label, name in _all_unit_labels():
|
|
290
|
+
key = "daemon" if label == DAEMON_LABEL else "dashboard"
|
|
291
|
+
st = _macos_unit_status(label) if _is_macos() else _linux_unit_status(name)
|
|
292
|
+
if key == "dashboard" and st.startswith("running"):
|
|
293
|
+
st += f" → http://localhost:{_dashboard_port()}"
|
|
294
|
+
rows.append(f" {key}: {st}")
|
|
295
|
+
if all("not installed" in r for r in rows):
|
|
296
|
+
return "Not installed. Run: memor service install"
|
|
297
|
+
return "Service status:\n" + "\n".join(rows)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memor-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.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
|
|
@@ -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
|
|
@@ -218,10 +218,13 @@ memor install-hook Install hook + download model (interactive
|
|
|
218
218
|
memor daemon Auto-ingest + distill (background watcher)
|
|
219
219
|
memor dashboard Web dashboard on localhost:8420
|
|
220
220
|
memor version Print installed version
|
|
221
|
-
memor service install Run daemon as background
|
|
222
|
-
|
|
223
|
-
memor service
|
|
224
|
-
memor service
|
|
221
|
+
memor service install Run daemon + dashboard as background services (launchd/systemd)
|
|
222
|
+
--no-dashboard Install only the daemon
|
|
223
|
+
memor service restart Restart both services (use after `pipx upgrade`)
|
|
224
|
+
memor service stop Stop both background services
|
|
225
|
+
memor service uninstall Remove both background services
|
|
226
|
+
memor service status Show daemon + dashboard status
|
|
227
|
+
(dashboard port: set MEMOR_DASHBOARD_PORT, default 8420)
|
|
225
228
|
memor query <text> Search memories from the CLI
|
|
226
229
|
memor reingest Wipe DB and re-ingest everything
|
|
227
230
|
memor reingest --project <name> Re-ingest only one project
|
|
@@ -328,7 +331,7 @@ cd memor-ai
|
|
|
328
331
|
python3 -m venv .venv && source .venv/bin/activate
|
|
329
332
|
pip install -e ".[dev]"
|
|
330
333
|
|
|
331
|
-
pytest #
|
|
334
|
+
pytest # 298 tests
|
|
332
335
|
```
|
|
333
336
|
|
|
334
337
|
---
|