Skip to content

Commit 148e9cd

Browse files
Tomas Pflanzerclaude
andcommitted
sync: v2.4.16 — CLI UX hygiene bundle (validate · search · bar doctor)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 62da030 commit 148e9cd

6 files changed

Lines changed: 205 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99

1010

11+
## [2.4.16] — 2026-05-29
12+
13+
**Three CLI UX hygiene fixes bundled.**
14+
15+
### Fixed
16+
17+
* **`memee validate --project <unregistered>` no longer crashes.** The
18+
helper was named `_get_or_create_project` but never created anything —
19+
any unregistered path produced `AttributeError: 'NoneType' object has
20+
no attribute 'id'`. Renamed to `_get_project_by_path` (honest
21+
contract) and the caller now raises a clean `click.ClickException`
22+
pointing at `memee project add`.
23+
24+
* **`memee search` now prints `query_event_id`.** The `memee feedback`
25+
docstring promises EVENT_ID is "printed by `memee search`", but
26+
search never passed `return_event_id=True` and never printed it.
27+
`search` now surfaces the event id (and a hint about the
28+
`memee feedback` command) after the results, so the documented
29+
retrieval-feedback loop is one keystroke away. Suppressed when
30+
`MEMEE_TELEMETRY=0` to avoid an empty line.
31+
32+
* **`test_doctor_returns_ok_on_macos_with_deps` skips without rumps.**
33+
The test asserted `ok=True` even when the optional `[bar]` extra
34+
wasn't installed — precisely the case `diagnose()` rightfully reports
35+
as not-ok. Now `pytest.importorskip("rumps")` /
36+
`pytest.importorskip("watchdog")` skip the test in those environments.
37+
38+
### Migration
39+
40+
None. Behavioural-only changes; no schema, no API.
41+
1142
## [2.4.15] — 2026-05-29
1243

1344
**Alembic schema parity: `alembic upgrade head` now produces the same

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "memee"
7-
version = "2.4.15"
7+
version = "2.4.16"
88
description = "Cross-model shared memory for AI agents. One canon. Every model, every project, every teammate."
99
readme = "README.md"
1010
requires-python = ">=3.11"

src/memee/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Memee — Institutional memory for AI agent companies."""
22

3-
__version__ = "2.4.15"
3+
__version__ = "2.4.16"

src/memee/cli.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -422,12 +422,15 @@ def search(query, memory_type, tags, limit):
422422

423423
tag_list = [t.strip() for t in tags.split(",") if t.strip()] if tags else None
424424

425-
results = search_memories(
426-
session, query, tags=tag_list, memory_type=memory_type, limit=limit
425+
results, event_id = search_memories(
426+
session, query, tags=tag_list, memory_type=memory_type, limit=limit,
427+
return_event_id=True,
427428
)
428429

429430
if not results:
430431
click.echo("No memories found.")
432+
if event_id:
433+
click.echo(f" query_event_id: {event_id}")
431434
return
432435

433436
for i, r in enumerate(results, 1):
@@ -441,6 +444,17 @@ def search(query, memory_type, tags, limit):
441444
if tags_str:
442445
click.echo(f" Tags: {tags_str}")
443446

447+
# v2.4.16: surface the event id so `memee feedback <event_id> <memory_id>`
448+
# is one keystroke away — the docstring on `feedback` already promises
449+
# this id is "printed by memee search". Suppressed when telemetry is
450+
# off (event_id is None) to avoid printing a confusing blank line.
451+
if event_id:
452+
click.echo(f"\n query_event_id: {event_id}")
453+
click.echo(
454+
" (use `memee feedback {event_id} {memory_id}` to mark which "
455+
"you used)"
456+
)
457+
444458

445459
# ── Suggest ──
446460

@@ -632,7 +646,12 @@ def validate(memory_id, evidence, project):
632646

633647
project_id = None
634648
if project:
635-
proj = _get_or_create_project(session, project)
649+
proj = _get_project_by_path(session, project)
650+
if proj is None:
651+
raise click.ClickException(
652+
f"Project not registered: {project}. "
653+
"Run `memee project add <path>` first."
654+
)
636655
project_id = proj.id
637656

638657
validation = MemoryValidation(
@@ -2529,8 +2548,16 @@ def _link_memory_to_project(session, memory, project_path: str):
25292548
session.add(pm)
25302549

25312550

2532-
def _get_or_create_project(session, project_path: str):
2533-
"""Get project by path, or return None if not registered."""
2551+
def _get_project_by_path(session, project_path: str):
2552+
"""Look up a Project by absolute path. Returns ``None`` when not registered.
2553+
2554+
v2.4.16 rename. The previous name (``_get_or_create_project``) lied —
2555+
nothing ever got created, so callers that did ``proj.id`` after this
2556+
blew up with ``AttributeError: 'NoneType' object has no attribute 'id'``
2557+
for any unregistered path. The contract is now declared by the name:
2558+
you get the row or you get None, and the caller surfaces a clean
2559+
error to the user.
2560+
"""
25342561
from memee.storage.models import Project
25352562

25362563
abs_path = str(Path(project_path).resolve())

tests/test_bar.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,13 @@ def test_doctor_returns_ok_on_macos_with_deps(tmp_path, monkeypatch):
192192
expected-and-not-fatal)."""
193193
if sys.platform != "darwin":
194194
pytest.skip("doctor's ok-flag is only meaningful on macOS")
195+
# The optional ``[bar]`` extra (rumps + watchdog) isn't installed in
196+
# every dev / CI environment. Pre-v2.4.16 this test asserted
197+
# ``ok=True`` even when rumps was missing — precisely the case
198+
# ``diagnose()`` rightfully reports as not-ok. Skip when the deps
199+
# the assertion depends on aren't actually importable.
200+
pytest.importorskip("rumps")
201+
pytest.importorskip("watchdog")
195202
monkeypatch.setenv("MEMEE_HOME", str(tmp_path))
196203
from memee.bar.doctor import diagnose
197204

tests/test_cli_ux_hygiene.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
"""v2.4.16 — CLI UX hygiene bundle.
2+
3+
Three small, unrelated fixes ride in one patch:
4+
5+
* ``memee validate --project <unregistered>`` used to crash with
6+
``AttributeError: 'NoneType' object has no attribute 'id'`` because
7+
``_get_or_create_project`` lied — it never created anything. Renamed
8+
to ``_get_project_by_path`` and the caller now raises a clean
9+
``click.ClickException``.
10+
11+
* ``memee feedback`` says EVENT_ID is "printed by ``memee search``", but
12+
search never passed ``return_event_id=True`` to the underlying
13+
``search_memories`` call and never printed the id. ``search`` now
14+
surfaces ``query_event_id`` after the results so the documented
15+
feedback loop is actually one keystroke away.
16+
17+
The third member of the bundle — ``test_doctor_returns_ok_on_macos_with_deps``
18+
gaining ``pytest.importorskip("rumps")`` — lives in ``tests/test_bar.py``
19+
where the offending assertion is.
20+
"""
21+
22+
from __future__ import annotations
23+
24+
import re
25+
26+
from click.testing import CliRunner
27+
28+
from memee.cli import cli
29+
from memee.storage.models import MaturityLevel, Memory, MemoryType
30+
31+
32+
def test_validate_unregistered_project_returns_clean_error(tmp_path, monkeypatch):
33+
"""No more AttributeError — a click error explains the next step."""
34+
monkeypatch.setenv("MEMEE_HOME", str(tmp_path / ".memee"))
35+
runner = CliRunner()
36+
runner.invoke(cli, ["init"])
37+
38+
# Seed one memory so the lookup half of `validate` succeeds.
39+
from memee.storage.database import get_session, init_db
40+
engine = init_db()
41+
session = get_session(engine)
42+
m = Memory(
43+
type=MemoryType.PATTERN.value,
44+
title="Some pattern",
45+
content="content",
46+
maturity=MaturityLevel.HYPOTHESIS.value,
47+
)
48+
session.add(m)
49+
session.commit()
50+
short = m.id[:8]
51+
session.close()
52+
53+
bogus = str(tmp_path / "never-registered")
54+
result = runner.invoke(cli, ["validate", short, "-p", bogus])
55+
assert result.exit_code != 0
56+
combined = result.output + (result.stderr_bytes or b"").decode(errors="ignore")
57+
assert "not registered" in combined.lower(), combined
58+
# Concrete next step is mentioned so the user isn't stuck.
59+
assert "memee project add" in combined
60+
61+
62+
def test_search_prints_query_event_id(tmp_path, monkeypatch):
63+
"""`memee search` surfaces the event_id so `memee feedback` works."""
64+
monkeypatch.setenv("MEMEE_HOME", str(tmp_path / ".memee"))
65+
monkeypatch.setenv("MEMEE_TELEMETRY", "1")
66+
runner = CliRunner()
67+
runner.invoke(cli, ["init"])
68+
69+
from memee.storage.database import get_session, init_db
70+
engine = init_db()
71+
session = get_session(engine)
72+
session.add(
73+
Memory(
74+
type=MemoryType.PATTERN.value,
75+
title="Use timeout on HTTP requests",
76+
content="Always pass timeout=10 to requests.get.",
77+
tags=["python", "http"],
78+
maturity=MaturityLevel.VALIDATED.value,
79+
confidence_score=0.9,
80+
)
81+
)
82+
session.commit()
83+
session.close()
84+
85+
result = runner.invoke(cli, ["search", "timeout HTTP"])
86+
assert result.exit_code == 0, result.output
87+
assert "query_event_id:" in result.output
88+
# The id is a UUID-shaped string — check the format so a future
89+
# change of the print template doesn't silently break the feedback
90+
# loop the user is following.
91+
match = re.search(r"query_event_id:\s+([0-9a-f-]{8,})", result.output)
92+
assert match, result.output
93+
94+
95+
def test_search_no_results_still_prints_event_id(tmp_path, monkeypatch):
96+
"""Empty result sets still log a SearchEvent — surface its id too so
97+
`memee feedback` works after an exploratory zero-hit query."""
98+
monkeypatch.setenv("MEMEE_HOME", str(tmp_path / ".memee"))
99+
monkeypatch.setenv("MEMEE_TELEMETRY", "1")
100+
runner = CliRunner()
101+
runner.invoke(cli, ["init"])
102+
result = runner.invoke(cli, ["search", "zzz-no-match-zzz"])
103+
assert result.exit_code == 0
104+
assert "No memories found" in result.output
105+
assert "query_event_id:" in result.output
106+
107+
108+
def test_search_silent_when_telemetry_disabled(tmp_path, monkeypatch):
109+
"""With telemetry off, no event id to print → don't print a misleading
110+
empty 'query_event_id:' line."""
111+
monkeypatch.setenv("MEMEE_HOME", str(tmp_path / ".memee"))
112+
monkeypatch.setenv("MEMEE_TELEMETRY", "0")
113+
runner = CliRunner()
114+
runner.invoke(cli, ["init"])
115+
116+
from memee.storage.database import get_session, init_db
117+
engine = init_db()
118+
session = get_session(engine)
119+
session.add(
120+
Memory(
121+
type=MemoryType.PATTERN.value,
122+
title="Some pattern",
123+
content="content",
124+
tags=["any"],
125+
maturity=MaturityLevel.HYPOTHESIS.value,
126+
)
127+
)
128+
session.commit()
129+
session.close()
130+
131+
result = runner.invoke(cli, ["search", "pattern"])
132+
assert result.exit_code == 0
133+
assert "query_event_id:" not in result.output

0 commit comments

Comments
 (0)