memory-seed 2.3.0__tar.gz → 2.4.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.
- {memory_seed-2.3.0 → memory_seed-2.4.0}/PKG-INFO +15 -3
- {memory_seed-2.3.0 → memory_seed-2.4.0}/README.md +14 -2
- {memory_seed-2.3.0 → memory_seed-2.4.0}/memory_seed/cli.py +2 -0
- {memory_seed-2.3.0 → memory_seed-2.4.0}/memory_seed/core.py +231 -9
- {memory_seed-2.3.0 → memory_seed-2.4.0}/memory_seed/seed/.memory-seed/agent-rules.md +44 -34
- {memory_seed-2.3.0 → memory_seed-2.4.0}/memory_seed/seed/.memory-seed/hooks/memory-retrieval-check.py +7 -2
- {memory_seed-2.3.0 → memory_seed-2.4.0}/memory_seed/seed/.memory-seed/project-bootstrap.md +1 -1
- {memory_seed-2.3.0 → memory_seed-2.4.0}/memory_seed/seed/.memory-seed/skills/code_search.md +1 -1
- {memory_seed-2.3.0 → memory_seed-2.4.0}/memory_seed/seed/.memory-seed/skills/data_architecture.md +1 -1
- {memory_seed-2.3.0 → memory_seed-2.4.0}/memory_seed/seed/.memory-seed/skills/index.md +1 -1
- {memory_seed-2.3.0 → memory_seed-2.4.0}/memory_seed/seed/.memory-seed/skills/local_compilation.md +1 -1
- {memory_seed-2.3.0 → memory_seed-2.4.0}/memory_seed/seed/.memory-seed/skills/memory_consolidation.md +1 -1
- {memory_seed-2.3.0 → memory_seed-2.4.0}/memory_seed/seed/.memory-seed/skills/memory_doctor.md +1 -1
- {memory_seed-2.3.0 → memory_seed-2.4.0}/memory_seed/seed/.memory-seed/skills/release_publishing.md +1 -1
- {memory_seed-2.3.0 → memory_seed-2.4.0}/memory_seed/seed/.memory-seed/skills/security_triage.md +1 -1
- {memory_seed-2.3.0 → memory_seed-2.4.0}/memory_seed/seed/AGENTS.md +1 -1
- {memory_seed-2.3.0 → memory_seed-2.4.0}/memory_seed/seed/CLAUDE.md +1 -1
- {memory_seed-2.3.0 → memory_seed-2.4.0}/memory_seed/seed/GEMINI.md +1 -1
- {memory_seed-2.3.0 → memory_seed-2.4.0}/memory_seed.egg-info/PKG-INFO +15 -3
- {memory_seed-2.3.0 → memory_seed-2.4.0}/pyproject.toml +1 -1
- {memory_seed-2.3.0 → memory_seed-2.4.0}/tests/test_memory_seed.py +264 -26
- {memory_seed-2.3.0 → memory_seed-2.4.0}/LICENSE +0 -0
- {memory_seed-2.3.0 → memory_seed-2.4.0}/memory_seed/__init__.py +0 -0
- {memory_seed-2.3.0 → memory_seed-2.4.0}/memory_seed/mcp_server.py +0 -0
- {memory_seed-2.3.0 → memory_seed-2.4.0}/memory_seed/mcp_validate.py +0 -0
- {memory_seed-2.3.0 → memory_seed-2.4.0}/memory_seed/seed/.memory-seed/archive/.gitkeep +0 -0
- {memory_seed-2.3.0 → memory_seed-2.4.0}/memory_seed/seed/.memory-seed/hooks/session-log-check.py +0 -0
- {memory_seed-2.3.0 → memory_seed-2.4.0}/memory_seed/seed/.memory-seed/sessions/.gitkeep +0 -0
- {memory_seed-2.3.0 → memory_seed-2.4.0}/memory_seed/semantic_cache.py +0 -0
- {memory_seed-2.3.0 → memory_seed-2.4.0}/memory_seed.egg-info/SOURCES.txt +0 -0
- {memory_seed-2.3.0 → memory_seed-2.4.0}/memory_seed.egg-info/dependency_links.txt +0 -0
- {memory_seed-2.3.0 → memory_seed-2.4.0}/memory_seed.egg-info/entry_points.txt +0 -0
- {memory_seed-2.3.0 → memory_seed-2.4.0}/memory_seed.egg-info/requires.txt +0 -0
- {memory_seed-2.3.0 → memory_seed-2.4.0}/memory_seed.egg-info/top_level.txt +0 -0
- {memory_seed-2.3.0 → memory_seed-2.4.0}/setup.cfg +0 -0
- {memory_seed-2.3.0 → memory_seed-2.4.0}/tests/test_mcp_server.py +0 -0
- {memory_seed-2.3.0 → memory_seed-2.4.0}/tests/test_mcp_validation.py +0 -0
- {memory_seed-2.3.0 → memory_seed-2.4.0}/tests/test_semantic_cache.py +0 -0
- {memory_seed-2.3.0 → memory_seed-2.4.0}/tests/test_session_schema.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memory-seed
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: Portable local memory seed for file-reading AI coding agents
|
|
5
5
|
Author: Jean Nathan Tshibuyi
|
|
6
6
|
License: MIT
|
|
@@ -135,7 +135,7 @@ The result is a lightweight memory workflow you can understand, commit, review,
|
|
|
135
135
|
|
|
136
136
|
| Agent or client | Support path |
|
|
137
137
|
| --- | --- |
|
|
138
|
-
| Codex | Starts from `AGENTS.md`;
|
|
138
|
+
| Codex | Starts from `AGENTS.md`; MCP server auto-registered in `.codex/config.toml` (loads once the project directory is trusted). |
|
|
139
139
|
| Claude Code | Starts from `CLAUDE.md`; MCP server auto-registered via `uvx --from memory-seed`. |
|
|
140
140
|
| Gemini CLI | Starts from `GEMINI.md`. |
|
|
141
141
|
| Other file-reading agents | Start from `AGENTS.md` and follow nearest `.memory-seed/` runtime discovery. |
|
|
@@ -365,7 +365,11 @@ For the version distinction (`pip show memory-seed` reports the package version;
|
|
|
365
365
|
|
|
366
366
|
Memory Seed also includes a lightweight MCP server that lets agents search local session memory through structured tool calls instead of shelling out to broad compact summaries.
|
|
367
367
|
|
|
368
|
-
**Auto-registration:** `memory-seed init` and `memory-seed update` automatically register `uvx --from memory-seed memory-seed-mcp --stdio` in each supported vendor's config — `.
|
|
368
|
+
**Auto-registration:** `memory-seed init` and `memory-seed update` automatically register `uvx --from memory-seed memory-seed-mcp --stdio` in each supported vendor's MCP config — `.mcp.json` at the project root (Claude Code), `.cursor/mcp.json` (Cursor), `.gemini/settings.json` (Gemini CLI), and `.codex/config.toml` (Codex CLI). No manual config is needed for projects initialised with Memory Seed. The `uvx --from` form is used so the command works regardless of whether `~/.local/bin` is on the agent's PATH.
|
|
369
|
+
|
|
370
|
+
> **Claude Code reads project-scope MCP servers from `.mcp.json`, not `.claude/settings.json`** — the latter is for hooks and permissions only. Versions 2.2.0–2.3.0 wrote the server into `.claude/settings.json`, where Claude Code silently ignored it; `memory-seed update` now writes `.mcp.json` and removes the dead entry. Restart Claude Code and approve the project server, then confirm with `claude mcp list`.
|
|
371
|
+
|
|
372
|
+
> **Codex loads a project `.codex/config.toml` only for *trusted* directories.** Memory Seed writes the `[mcp_servers.memory-seed]` table there, but Codex ignores it until you trust the project (Codex prompts on first use of a directory, or set trust in Codex settings). After trusting, confirm with `codex mcp list`. `memory-seed doctor` warns if Codex hooks are present without this registration. If you hand-wrote the `memory-seed` entry in a non-standard TOML form (dotted keys, an inline table, or a header with a trailing comment) and it is outdated, `memory-seed update` will not auto-migrate it — `memory-seed doctor` flags it as needing a manual fix instead of silently leaving stale settings in place.
|
|
369
373
|
|
|
370
374
|
If you are configuring the server manually, run it over stdio:
|
|
371
375
|
|
|
@@ -422,6 +426,14 @@ memory_get_chunk(chunk_id, cwd=".")
|
|
|
422
426
|
|
|
423
427
|
The ranking engine stays local and CPU-friendly. MCP search uses a Model2Vec static embedding provider by default with the general-purpose `minishlab/potion-base-8M` model, combines semantic score with lexical and metadata scoring, then applies recency. If Model2Vec or the model cannot load or score a query, the server falls back to lexical, metadata, and recency ranking without failing the request. Use `--no-semantic` on `memory-seed-mcp --stdio` or `semantic_enabled=false` in `memory_search` to force fallback behavior.
|
|
424
428
|
|
|
429
|
+
### Performance characteristics
|
|
430
|
+
|
|
431
|
+
`memory_search` is a relevance-and-recall tool, not a faster `grep`. A plain `grep` will out-scan it on raw exact-match throughput; the search wins instead on *semantic recall* over session history (surfacing relevant entries that lack the literal query words) and on *agent-token efficiency* (returning a small ranked set of self-contained chunks with stable `chunk_id`s, so an agent fetches only the one or two full entries worth reading). The two are complementary: use `memory_search` for "what did we decide and why," and `grep` for exact-string scans across the whole repo.
|
|
432
|
+
|
|
433
|
+
Per-query latency, measured in-process on this repo (81 chunks across the session logs), is roughly **30 ms**, of which about **22 ms is reading and parsing the session `.md` files** — the search re-reads and re-parses every `sessions/*.md` on each call, with no persistent chunk or vector cache — and the embed + cosine + rank step adds only a few ms on top. Cold start adds a one-time cost on the *first ever* call on a machine: the Model2Vec weights download into the local HuggingFace cache (tens of MB); afterwards the static model loads in a few ms. Because the static model has no transformer forward pass, the dominant cost is file I/O, so per-query time grows linearly with total session-log size rather than with model complexity.
|
|
434
|
+
|
|
435
|
+
When driving the server through an MCP client (Claude Code, Cursor, Gemini), the latency you actually perceive is dominated by one-time startup, not per-query work: spawning `uvx --from memory-seed memory-seed-mcp` resolves and may install the package into an ephemeral environment the first time the server launches in a session. Once the server is up, each `memory_search` is the ~30 ms compute above plus a small JSON-RPC round-trip. At current log sizes there is no need to optimize; should logs grow large enough that the ~22 ms parse cost becomes noticeable, caching parsed chunks and their vectors keyed by file modification time would remove most of the per-query cost.
|
|
436
|
+
|
|
425
437
|
Session entries should include a YAML metadata block with `entry_id`, `user_initials`, `agent_type`, `project_path`, and `subproject_path`. Session entry headings may include optional minute-level timestamps, such as `## 2026-05-19 20:42 - Durable memory consolidation`. Session filenames stay date-only. Timestamped headings are backward compatible with older untimed headings and are exposed as `entry_datetime` in MCP search results when present.
|
|
426
438
|
|
|
427
439
|
For human-validatable search behavior, see the fixture-style tests in `tests/test_mcp_server.py`. They assert that specific queries return expected dated session entries first and include enough evidence for manual review.
|
|
@@ -114,7 +114,7 @@ The result is a lightweight memory workflow you can understand, commit, review,
|
|
|
114
114
|
|
|
115
115
|
| Agent or client | Support path |
|
|
116
116
|
| --- | --- |
|
|
117
|
-
| Codex | Starts from `AGENTS.md`;
|
|
117
|
+
| Codex | Starts from `AGENTS.md`; MCP server auto-registered in `.codex/config.toml` (loads once the project directory is trusted). |
|
|
118
118
|
| Claude Code | Starts from `CLAUDE.md`; MCP server auto-registered via `uvx --from memory-seed`. |
|
|
119
119
|
| Gemini CLI | Starts from `GEMINI.md`. |
|
|
120
120
|
| Other file-reading agents | Start from `AGENTS.md` and follow nearest `.memory-seed/` runtime discovery. |
|
|
@@ -344,7 +344,11 @@ For the version distinction (`pip show memory-seed` reports the package version;
|
|
|
344
344
|
|
|
345
345
|
Memory Seed also includes a lightweight MCP server that lets agents search local session memory through structured tool calls instead of shelling out to broad compact summaries.
|
|
346
346
|
|
|
347
|
-
**Auto-registration:** `memory-seed init` and `memory-seed update` automatically register `uvx --from memory-seed memory-seed-mcp --stdio` in each supported vendor's config — `.
|
|
347
|
+
**Auto-registration:** `memory-seed init` and `memory-seed update` automatically register `uvx --from memory-seed memory-seed-mcp --stdio` in each supported vendor's MCP config — `.mcp.json` at the project root (Claude Code), `.cursor/mcp.json` (Cursor), `.gemini/settings.json` (Gemini CLI), and `.codex/config.toml` (Codex CLI). No manual config is needed for projects initialised with Memory Seed. The `uvx --from` form is used so the command works regardless of whether `~/.local/bin` is on the agent's PATH.
|
|
348
|
+
|
|
349
|
+
> **Claude Code reads project-scope MCP servers from `.mcp.json`, not `.claude/settings.json`** — the latter is for hooks and permissions only. Versions 2.2.0–2.3.0 wrote the server into `.claude/settings.json`, where Claude Code silently ignored it; `memory-seed update` now writes `.mcp.json` and removes the dead entry. Restart Claude Code and approve the project server, then confirm with `claude mcp list`.
|
|
350
|
+
|
|
351
|
+
> **Codex loads a project `.codex/config.toml` only for *trusted* directories.** Memory Seed writes the `[mcp_servers.memory-seed]` table there, but Codex ignores it until you trust the project (Codex prompts on first use of a directory, or set trust in Codex settings). After trusting, confirm with `codex mcp list`. `memory-seed doctor` warns if Codex hooks are present without this registration. If you hand-wrote the `memory-seed` entry in a non-standard TOML form (dotted keys, an inline table, or a header with a trailing comment) and it is outdated, `memory-seed update` will not auto-migrate it — `memory-seed doctor` flags it as needing a manual fix instead of silently leaving stale settings in place.
|
|
348
352
|
|
|
349
353
|
If you are configuring the server manually, run it over stdio:
|
|
350
354
|
|
|
@@ -401,6 +405,14 @@ memory_get_chunk(chunk_id, cwd=".")
|
|
|
401
405
|
|
|
402
406
|
The ranking engine stays local and CPU-friendly. MCP search uses a Model2Vec static embedding provider by default with the general-purpose `minishlab/potion-base-8M` model, combines semantic score with lexical and metadata scoring, then applies recency. If Model2Vec or the model cannot load or score a query, the server falls back to lexical, metadata, and recency ranking without failing the request. Use `--no-semantic` on `memory-seed-mcp --stdio` or `semantic_enabled=false` in `memory_search` to force fallback behavior.
|
|
403
407
|
|
|
408
|
+
### Performance characteristics
|
|
409
|
+
|
|
410
|
+
`memory_search` is a relevance-and-recall tool, not a faster `grep`. A plain `grep` will out-scan it on raw exact-match throughput; the search wins instead on *semantic recall* over session history (surfacing relevant entries that lack the literal query words) and on *agent-token efficiency* (returning a small ranked set of self-contained chunks with stable `chunk_id`s, so an agent fetches only the one or two full entries worth reading). The two are complementary: use `memory_search` for "what did we decide and why," and `grep` for exact-string scans across the whole repo.
|
|
411
|
+
|
|
412
|
+
Per-query latency, measured in-process on this repo (81 chunks across the session logs), is roughly **30 ms**, of which about **22 ms is reading and parsing the session `.md` files** — the search re-reads and re-parses every `sessions/*.md` on each call, with no persistent chunk or vector cache — and the embed + cosine + rank step adds only a few ms on top. Cold start adds a one-time cost on the *first ever* call on a machine: the Model2Vec weights download into the local HuggingFace cache (tens of MB); afterwards the static model loads in a few ms. Because the static model has no transformer forward pass, the dominant cost is file I/O, so per-query time grows linearly with total session-log size rather than with model complexity.
|
|
413
|
+
|
|
414
|
+
When driving the server through an MCP client (Claude Code, Cursor, Gemini), the latency you actually perceive is dominated by one-time startup, not per-query work: spawning `uvx --from memory-seed memory-seed-mcp` resolves and may install the package into an ephemeral environment the first time the server launches in a session. Once the server is up, each `memory_search` is the ~30 ms compute above plus a small JSON-RPC round-trip. At current log sizes there is no need to optimize; should logs grow large enough that the ~22 ms parse cost becomes noticeable, caching parsed chunks and their vectors keyed by file modification time would remove most of the per-query cost.
|
|
415
|
+
|
|
404
416
|
Session entries should include a YAML metadata block with `entry_id`, `user_initials`, `agent_type`, `project_path`, and `subproject_path`. Session entry headings may include optional minute-level timestamps, such as `## 2026-05-19 20:42 - Durable memory consolidation`. Session filenames stay date-only. Timestamped headings are backward compatible with older untimed headings and are exposed as `entry_datetime` in MCP search results when present.
|
|
405
417
|
|
|
406
418
|
For human-validatable search behavior, see the fixture-style tests in `tests/test_mcp_server.py`. They assert that specific queries return expected dated session entries first and include enough evidence for manual review.
|
|
@@ -118,6 +118,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
118
118
|
print("Memory Seed bootstrap is complete.")
|
|
119
119
|
else:
|
|
120
120
|
print("Memory Seed bootstrap is incomplete.")
|
|
121
|
+
for warning in result.warnings:
|
|
122
|
+
print(f"Warning: {warning}")
|
|
121
123
|
if result.ok:
|
|
122
124
|
return 0
|
|
123
125
|
for missing in result.missing:
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
import re
|
|
5
5
|
import hashlib
|
|
6
|
+
import tomllib
|
|
6
7
|
from dataclasses import dataclass, field
|
|
7
8
|
from datetime import datetime, timedelta
|
|
8
9
|
from pathlib import Path
|
|
@@ -10,7 +11,7 @@ from pathlib import Path
|
|
|
10
11
|
|
|
11
12
|
PACKAGE_ROOT = Path(__file__).resolve().parent
|
|
12
13
|
SEED_ROOT = PACKAGE_ROOT / "seed"
|
|
13
|
-
VERSION = "2.
|
|
14
|
+
VERSION = "2.4"
|
|
14
15
|
MEMORY_DIR_NAME = ".memory-seed"
|
|
15
16
|
LEGACY_MEMORY_DIR_NAME = ".AGENTS"
|
|
16
17
|
BACKUP_IGNORE_ENTRY = ".memory-seed/backups/"
|
|
@@ -49,6 +50,7 @@ class DoctorResult:
|
|
|
49
50
|
missing: list[str] = field(default_factory=list)
|
|
50
51
|
version_mismatches: list[dict[str, str]] = field(default_factory=list)
|
|
51
52
|
bootstrap_missing: list[str] = field(default_factory=list)
|
|
53
|
+
warnings: list[str] = field(default_factory=list)
|
|
52
54
|
|
|
53
55
|
|
|
54
56
|
@dataclass
|
|
@@ -350,14 +352,19 @@ def _merge_cursor_retrieval_hook(target_root: Path) -> bool:
|
|
|
350
352
|
|
|
351
353
|
|
|
352
354
|
def _merge_claude_mcp(target_root: Path) -> bool:
|
|
353
|
-
"""Upsert the memory-seed-mcp stdio server entry in .
|
|
354
|
-
|
|
355
|
-
|
|
355
|
+
"""Upsert the memory-seed-mcp stdio server entry in the project-root .mcp.json.
|
|
356
|
+
|
|
357
|
+
Claude Code discovers project-scope MCP servers from .mcp.json, not from
|
|
358
|
+
.claude/settings.json (that key is silently ignored by Claude Code).
|
|
359
|
+
_strip_claude_settings_mcp removes the legacy settings.json entry.
|
|
360
|
+
"""
|
|
361
|
+
mcp_path = target_root / ".mcp.json"
|
|
362
|
+
expected = {"command": _MCP_SERVER_COMMAND, "args": _MCP_SERVER_ARGS}
|
|
356
363
|
|
|
357
364
|
data: dict = {}
|
|
358
|
-
if
|
|
365
|
+
if mcp_path.exists():
|
|
359
366
|
try:
|
|
360
|
-
with open(
|
|
367
|
+
with open(mcp_path) as f:
|
|
361
368
|
data = json.load(f)
|
|
362
369
|
except (json.JSONDecodeError, OSError):
|
|
363
370
|
data = {}
|
|
@@ -371,7 +378,44 @@ def _merge_claude_mcp(target_root: Path) -> bool:
|
|
|
371
378
|
|
|
372
379
|
data.setdefault("mcpServers", {})[_MCP_SERVER_KEY] = expected
|
|
373
380
|
|
|
374
|
-
|
|
381
|
+
mcp_path.parent.mkdir(parents=True, exist_ok=True)
|
|
382
|
+
with open(mcp_path, "w") as f:
|
|
383
|
+
json.dump(data, f, indent=2)
|
|
384
|
+
f.write("\n")
|
|
385
|
+
|
|
386
|
+
return True
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _strip_claude_settings_mcp(target_root: Path) -> bool:
|
|
390
|
+
"""Remove the legacy memory-seed MCP entry from .claude/settings.json.
|
|
391
|
+
|
|
392
|
+
Versions 2.2.0-2.3.0 wrote the server into .claude/settings.json > mcpServers,
|
|
393
|
+
which Claude Code never reads. Now that the server lives in .mcp.json, drop the
|
|
394
|
+
dead entry so it does not mislead. Only our own entry is removed; a foreign
|
|
395
|
+
server under the same key is left untouched.
|
|
396
|
+
"""
|
|
397
|
+
settings_path = target_root / ".claude" / "settings.json"
|
|
398
|
+
if not settings_path.exists():
|
|
399
|
+
return False
|
|
400
|
+
try:
|
|
401
|
+
with open(settings_path) as f:
|
|
402
|
+
data = json.load(f)
|
|
403
|
+
except (json.JSONDecodeError, OSError):
|
|
404
|
+
return False
|
|
405
|
+
|
|
406
|
+
servers = data.get("mcpServers")
|
|
407
|
+
if not isinstance(servers, dict) or _MCP_SERVER_KEY not in servers:
|
|
408
|
+
return False
|
|
409
|
+
|
|
410
|
+
existing = servers.get(_MCP_SERVER_KEY, {})
|
|
411
|
+
is_ours = existing.get("command") in _OWN_MCP_COMMANDS or "memory-seed-mcp" in existing.get("args", [])
|
|
412
|
+
if not is_ours:
|
|
413
|
+
return False # foreign server under the same key; leave it alone
|
|
414
|
+
|
|
415
|
+
del servers[_MCP_SERVER_KEY]
|
|
416
|
+
if not servers:
|
|
417
|
+
del data["mcpServers"]
|
|
418
|
+
|
|
375
419
|
with open(settings_path, "w") as f:
|
|
376
420
|
json.dump(data, f, indent=2)
|
|
377
421
|
f.write("\n")
|
|
@@ -439,6 +483,120 @@ def _merge_gemini_mcp(target_root: Path) -> bool:
|
|
|
439
483
|
return True
|
|
440
484
|
|
|
441
485
|
|
|
486
|
+
# Header line for our entry in .codex/config.toml. Codex accepts both the bare
|
|
487
|
+
# and quoted single-segment table-key forms; we write the bare form.
|
|
488
|
+
_CODEX_MCP_HEADER_RE = re.compile(
|
|
489
|
+
r'^\[mcp_servers\.(?:memory-seed|"memory-seed")\]\s*$'
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _codex_expected() -> dict:
|
|
494
|
+
"""The MCP table we want present under [mcp_servers.memory-seed]."""
|
|
495
|
+
return {"command": _MCP_SERVER_COMMAND, "args": _MCP_SERVER_ARGS}
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _codex_standard_header_index(lines: list[str]) -> int | None:
|
|
499
|
+
"""Index of the standard ``[mcp_servers.memory-seed]`` header line, or None.
|
|
500
|
+
|
|
501
|
+
The in-place stale-update path can only rewrite an entry written with this
|
|
502
|
+
header form. Shared by _merge_codex_mcp (to decide whether it can migrate)
|
|
503
|
+
and _codex_mcp_status (to decide stale-fixable vs stale-manual), so the two
|
|
504
|
+
always agree on what counts as auto-fixable.
|
|
505
|
+
"""
|
|
506
|
+
return next(
|
|
507
|
+
(i for i, ln in enumerate(lines) if _CODEX_MCP_HEADER_RE.match(ln)),
|
|
508
|
+
None,
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _render_codex_mcp_block() -> str:
|
|
513
|
+
"""Render our fixed [mcp_servers.memory-seed] TOML table.
|
|
514
|
+
|
|
515
|
+
args is a TOML array of strings, which is JSON-compatible, so json.dumps
|
|
516
|
+
produces valid TOML for it.
|
|
517
|
+
"""
|
|
518
|
+
return (
|
|
519
|
+
f"[mcp_servers.{_MCP_SERVER_KEY}]\n"
|
|
520
|
+
f'command = "{_MCP_SERVER_COMMAND}"\n'
|
|
521
|
+
f"args = {json.dumps(_MCP_SERVER_ARGS)}\n"
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _merge_codex_mcp(target_root: Path) -> bool:
|
|
526
|
+
"""Upsert the memory-seed-mcp stdio server in the project .codex/config.toml.
|
|
527
|
+
|
|
528
|
+
Codex reads project-scoped MCP servers from .codex/config.toml under
|
|
529
|
+
[mcp_servers.<name>] (trusted projects only). This is a zero-dependency text
|
|
530
|
+
upsert: tomllib (stdlib, Python >=3.11) is used only to *inspect* current
|
|
531
|
+
state; writes are line-based so existing content and comments are preserved.
|
|
532
|
+
|
|
533
|
+
Returns True if the file was written, False if already current.
|
|
534
|
+
|
|
535
|
+
Known limitation (in-place stale-entry update only): rewriting a present-but-
|
|
536
|
+
outdated entry while preserving comments relies on finding the standard
|
|
537
|
+
``[mcp_servers.memory-seed]`` header line. Detection itself is robust (tomllib
|
|
538
|
+
parses semantically), but if a user *hand-wrote* the entry in a form that has
|
|
539
|
+
no such header line — dotted keys (``mcp_servers.memory-seed.command = ...``),
|
|
540
|
+
an inline subtable under ``[mcp_servers]``, a fully inline
|
|
541
|
+
``mcp_servers = { ... }``, or a header with a trailing comment / leading
|
|
542
|
+
indentation — and the entry is stale, this no-ops (returns False) rather than
|
|
543
|
+
risk a duplicate-key / invalid-TOML write. The no-op is intentionally not
|
|
544
|
+
silent: ``doctor`` classifies this case via _codex_mcp_status as a
|
|
545
|
+
``stale-manual`` warning telling the user to fix it by hand. Memory Seed only
|
|
546
|
+
ever writes the standard header form, so this path is only reachable through
|
|
547
|
+
manual edits.
|
|
548
|
+
"""
|
|
549
|
+
config_path = target_root / ".codex" / "config.toml"
|
|
550
|
+
block = _render_codex_mcp_block()
|
|
551
|
+
|
|
552
|
+
text = ""
|
|
553
|
+
parsed: dict = {}
|
|
554
|
+
if config_path.exists():
|
|
555
|
+
try:
|
|
556
|
+
text = config_path.read_text(encoding="utf-8")
|
|
557
|
+
parsed = tomllib.loads(text)
|
|
558
|
+
except (tomllib.TOMLDecodeError, OSError):
|
|
559
|
+
text = ""
|
|
560
|
+
parsed = {}
|
|
561
|
+
|
|
562
|
+
existing = parsed.get("mcp_servers", {}).get(_MCP_SERVER_KEY, {})
|
|
563
|
+
if existing == _codex_expected():
|
|
564
|
+
return False
|
|
565
|
+
if existing:
|
|
566
|
+
is_ours = (
|
|
567
|
+
existing.get("command") in _OWN_MCP_COMMANDS
|
|
568
|
+
or "memory-seed-mcp" in existing.get("args", [])
|
|
569
|
+
)
|
|
570
|
+
if not is_ours:
|
|
571
|
+
return False # a different server holds this key; don't overwrite
|
|
572
|
+
|
|
573
|
+
if not existing:
|
|
574
|
+
# Append our block, preserving everything above it.
|
|
575
|
+
new_text = text
|
|
576
|
+
if new_text and not new_text.endswith("\n"):
|
|
577
|
+
new_text += "\n"
|
|
578
|
+
if new_text:
|
|
579
|
+
new_text += "\n"
|
|
580
|
+
new_text += block
|
|
581
|
+
else:
|
|
582
|
+
# Stale entry: replace just our table's lines (header to next table/EOF).
|
|
583
|
+
lines = text.splitlines(keepends=True)
|
|
584
|
+
start = _codex_standard_header_index(lines)
|
|
585
|
+
if start is None:
|
|
586
|
+
# No standard header to anchor the rewrite. Don't risk a duplicate
|
|
587
|
+
# key; leave it for the user. doctor() flags this as stale-manual.
|
|
588
|
+
return False
|
|
589
|
+
end = start + 1
|
|
590
|
+
while end < len(lines) and not lines[end].lstrip().startswith("["):
|
|
591
|
+
end += 1
|
|
592
|
+
replacement = block if block.endswith("\n") else block + "\n"
|
|
593
|
+
new_text = "".join(lines[:start]) + replacement + "".join(lines[end:])
|
|
594
|
+
|
|
595
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
596
|
+
config_path.write_text(new_text, encoding="utf-8")
|
|
597
|
+
return True
|
|
598
|
+
|
|
599
|
+
|
|
442
600
|
def init_project(cwd: str | Path = ".", dry_run: bool = False, force: bool = False) -> InitResult:
|
|
443
601
|
target_root = Path(cwd).resolve()
|
|
444
602
|
planned = [seed_file.destination for seed_file in SEED_FILES]
|
|
@@ -484,9 +642,11 @@ def init_project(cwd: str | Path = ".", dry_run: bool = False, force: bool = Fal
|
|
|
484
642
|
(_merge_codex_retrieval_hook, ".codex/hooks.json"),
|
|
485
643
|
(_merge_cursor_retrieval_hook, ".cursor/hooks.json"),
|
|
486
644
|
(_merge_gemini_retrieval_hook, ".gemini/settings.json"),
|
|
487
|
-
(_merge_claude_mcp, ".
|
|
645
|
+
(_merge_claude_mcp, ".mcp.json"),
|
|
646
|
+
(_strip_claude_settings_mcp, ".claude/settings.json"),
|
|
488
647
|
(_merge_cursor_mcp, ".cursor/mcp.json"),
|
|
489
648
|
(_merge_gemini_mcp, ".gemini/settings.json"),
|
|
649
|
+
(_merge_codex_mcp, ".codex/config.toml"),
|
|
490
650
|
)
|
|
491
651
|
for merge, destination in hook_merges:
|
|
492
652
|
if merge(target_root) and destination not in created:
|
|
@@ -551,9 +711,11 @@ def update_project(cwd: str | Path = ".", dry_run: bool = False) -> InitResult:
|
|
|
551
711
|
(_merge_codex_retrieval_hook, ".codex/hooks.json"),
|
|
552
712
|
(_merge_cursor_retrieval_hook, ".cursor/hooks.json"),
|
|
553
713
|
(_merge_gemini_retrieval_hook, ".gemini/settings.json"),
|
|
554
|
-
(_merge_claude_mcp, ".
|
|
714
|
+
(_merge_claude_mcp, ".mcp.json"),
|
|
715
|
+
(_strip_claude_settings_mcp, ".claude/settings.json"),
|
|
555
716
|
(_merge_cursor_mcp, ".cursor/mcp.json"),
|
|
556
717
|
(_merge_gemini_mcp, ".gemini/settings.json"),
|
|
718
|
+
(_merge_codex_mcp, ".codex/config.toml"),
|
|
557
719
|
)
|
|
558
720
|
for merge, destination in hook_merges:
|
|
559
721
|
if merge(target_root) and destination not in created:
|
|
@@ -600,6 +762,28 @@ def doctor(cwd: str | Path = ".") -> DoctorResult:
|
|
|
600
762
|
control_plane_ok = not missing and not version_mismatches
|
|
601
763
|
bootstrap_complete = not bootstrap_missing
|
|
602
764
|
|
|
765
|
+
warnings: list[str] = []
|
|
766
|
+
codex_status = _codex_mcp_status(target_root)
|
|
767
|
+
if (target_root / ".codex" / "hooks.json").exists() and codex_status == "absent":
|
|
768
|
+
warnings.append(
|
|
769
|
+
"Codex hooks are installed but .codex/config.toml has no memory-seed MCP "
|
|
770
|
+
"entry. Run `memory-seed update`, then trust this directory in Codex so it "
|
|
771
|
+
"loads the project MCP server (memory_search / memory_get_chunk)."
|
|
772
|
+
)
|
|
773
|
+
elif codex_status == "stale-fixable":
|
|
774
|
+
warnings.append(
|
|
775
|
+
"Codex .codex/config.toml has an outdated memory-seed MCP entry. Run "
|
|
776
|
+
"`memory-seed update` to migrate it to `uvx --from memory-seed "
|
|
777
|
+
"memory-seed-mcp --stdio`."
|
|
778
|
+
)
|
|
779
|
+
elif codex_status == "stale-manual":
|
|
780
|
+
warnings.append(
|
|
781
|
+
"Codex .codex/config.toml has an outdated memory-seed MCP entry written in "
|
|
782
|
+
"a non-standard TOML form that `memory-seed update` cannot safely auto-fix. "
|
|
783
|
+
'Set it by hand to: command = "uvx", args = ["--from", "memory-seed", '
|
|
784
|
+
'"memory-seed-mcp", "--stdio"].'
|
|
785
|
+
)
|
|
786
|
+
|
|
603
787
|
return DoctorResult(
|
|
604
788
|
ok=control_plane_ok and bootstrap_complete,
|
|
605
789
|
control_plane_ok=control_plane_ok,
|
|
@@ -607,7 +791,45 @@ def doctor(cwd: str | Path = ".") -> DoctorResult:
|
|
|
607
791
|
missing=missing,
|
|
608
792
|
version_mismatches=version_mismatches,
|
|
609
793
|
bootstrap_missing=bootstrap_missing,
|
|
794
|
+
warnings=warnings,
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def _codex_mcp_status(target_root: Path) -> str:
|
|
799
|
+
"""Classify our memory-seed entry in .codex/config.toml.
|
|
800
|
+
|
|
801
|
+
Returns one of:
|
|
802
|
+
"absent" - no entry (or no/unparseable file)
|
|
803
|
+
"current" - present and matches the expected uvx command + args
|
|
804
|
+
"foreign" - present but owned by a different server
|
|
805
|
+
"stale-fixable" - ours but outdated, written with a standard header that
|
|
806
|
+
`memory-seed update` can auto-migrate
|
|
807
|
+
"stale-manual" - ours but outdated, written in a form with no standard
|
|
808
|
+
header line, so update no-ops and the user must edit it
|
|
809
|
+
"""
|
|
810
|
+
config_path = target_root / ".codex" / "config.toml"
|
|
811
|
+
if not config_path.exists():
|
|
812
|
+
return "absent"
|
|
813
|
+
try:
|
|
814
|
+
text = config_path.read_text(encoding="utf-8")
|
|
815
|
+
parsed = tomllib.loads(text)
|
|
816
|
+
except (tomllib.TOMLDecodeError, OSError):
|
|
817
|
+
return "absent"
|
|
818
|
+
|
|
819
|
+
existing = parsed.get("mcp_servers", {}).get(_MCP_SERVER_KEY, {})
|
|
820
|
+
if not existing:
|
|
821
|
+
return "absent"
|
|
822
|
+
if existing == _codex_expected():
|
|
823
|
+
return "current"
|
|
824
|
+
is_ours = (
|
|
825
|
+
existing.get("command") in _OWN_MCP_COMMANDS
|
|
826
|
+
or "memory-seed-mcp" in existing.get("args", [])
|
|
610
827
|
)
|
|
828
|
+
if not is_ours:
|
|
829
|
+
return "foreign"
|
|
830
|
+
if _codex_standard_header_index(text.splitlines()) is not None:
|
|
831
|
+
return "stale-fixable"
|
|
832
|
+
return "stale-manual"
|
|
611
833
|
|
|
612
834
|
|
|
613
835
|
def compact_sessions(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
memory-system-version: 2.
|
|
2
|
+
memory-system-version: 2.4
|
|
3
3
|
tags:
|
|
4
4
|
- memory-seed
|
|
5
5
|
- agent-rules
|
|
@@ -366,7 +366,7 @@ tags:
|
|
|
366
366
|
session_date: 2026-05-02
|
|
367
367
|
---
|
|
368
368
|
|
|
369
|
-
## 2026-05-02 14:35 -
|
|
369
|
+
## 2026-05-02 14:35 - Switch cache key to content hash
|
|
370
370
|
|
|
371
371
|
```yaml
|
|
372
372
|
entry_id: ms-8charhash
|
|
@@ -375,14 +375,17 @@ agent_type: codex
|
|
|
375
375
|
project_path: .
|
|
376
376
|
subproject_path: null
|
|
377
377
|
```
|
|
378
|
-
|
|
379
378
|
### Summary
|
|
380
379
|
|
|
381
|
-
-
|
|
380
|
+
- What changed or what was checked.
|
|
382
381
|
|
|
383
|
-
###
|
|
382
|
+
### Decision
|
|
384
383
|
|
|
385
|
-
-
|
|
384
|
+
- D: State the decision that was made or implemented. (mandatory)
|
|
385
|
+
- R: Explain the decisive reason in 1-3 bullets. (mandatory)
|
|
386
|
+
- A: Alternative considered or rejected, with reason, if it mattered. (optional)
|
|
387
|
+
- F: Files, artifacts, or behaviors changed. (optional)
|
|
388
|
+
- T: Tests or validation outcome. (optional)
|
|
386
389
|
|
|
387
390
|
### Follow-up
|
|
388
391
|
|
|
@@ -393,13 +396,13 @@ Keep session filenames date-only, such as `.memory-seed/sessions/2026-05-02.md`.
|
|
|
393
396
|
|
|
394
397
|
### Reason Rules
|
|
395
398
|
|
|
396
|
-
**DRAFT** is the
|
|
399
|
+
**DRAFT** is the baseline decision-record format for session entries: default to it whenever a turn produced a decision or a durable change. Route to a simpler or richer shape only as the exception (see Entry Shapes), and never invent a decision just to fill the baseline.
|
|
397
400
|
|
|
398
401
|
A DRAFT decision record uses compact labels:
|
|
399
402
|
|
|
400
|
-
- D = Decision — what was chosen
|
|
401
|
-
- R = Reason — the decisive reason, 1–3 bullets
|
|
402
|
-
- A = Alternatives considered or rejected
|
|
403
|
+
- D = Decision (mandatory) — what was chosen
|
|
404
|
+
- R = Reason (mandatory) — the decisive reason, 1–3 bullets
|
|
405
|
+
- A = Alternatives considered or rejected (optional) — with reason, when it shaped the tradeoff
|
|
403
406
|
- F = Files, artifacts, or behaviors changed (optional)
|
|
404
407
|
- T = Tests or validation outcome (optional; may appear inline as `- T:` or as a separate `### Validation` section)
|
|
405
408
|
|
|
@@ -414,11 +417,32 @@ A DRAFT decision record uses compact labels:
|
|
|
414
417
|
|
|
415
418
|
### Entry Shapes
|
|
416
419
|
|
|
417
|
-
|
|
420
|
+
Default to the **Meaningful decision entry** — a single DRAFT record. Route away from it only when the work does not fit:
|
|
421
|
+
|
|
422
|
+
- down to the **Small work entry** for routine edits, small fixes, or verification-only work with no real decision (do not invent reason — see Reason Rules);
|
|
423
|
+
- up to the **Multi-decision session entry** when one coherent task produced several decisions.
|
|
424
|
+
|
|
425
|
+
#### Meaningful decision entry
|
|
426
|
+
|
|
427
|
+
The baseline shape: use when a turn produced one durable decision.
|
|
428
|
+
|
|
429
|
+
```markdown
|
|
430
|
+
### Summary
|
|
431
|
+
|
|
432
|
+
- Summarize the coherent task.
|
|
433
|
+
|
|
434
|
+
### Decision
|
|
435
|
+
|
|
436
|
+
- D: State the decision. (mandatory)
|
|
437
|
+
- R: Explain the decisive reason in 1-3 bullets. (mandatory)
|
|
438
|
+
- A: Alternative considered or rejected, with reason, if it mattered. (optional)
|
|
439
|
+
- F: Files, artifacts, or behaviors changed. (optional)
|
|
440
|
+
- T: Tests or validation outcome. (optional)
|
|
441
|
+
```
|
|
418
442
|
|
|
419
443
|
#### Small work entry
|
|
420
444
|
|
|
421
|
-
|
|
445
|
+
Simpler alternative — for routine edits, small fixes, or verification-only work with no real decision.
|
|
422
446
|
|
|
423
447
|
```markdown
|
|
424
448
|
### Summary
|
|
@@ -434,23 +458,9 @@ Use for routine edits, small fixes, or verification-only work.
|
|
|
434
458
|
- Only include if there is residual risk or a next action.
|
|
435
459
|
```
|
|
436
460
|
|
|
437
|
-
#### Meaningful decision entry
|
|
438
|
-
|
|
439
|
-
Use when one durable decision was made or implemented.
|
|
440
|
-
|
|
441
|
-
```markdown
|
|
442
|
-
### Decision
|
|
443
|
-
|
|
444
|
-
- D: State the decision.
|
|
445
|
-
- R: Explain the decisive reason in 1-3 bullets.
|
|
446
|
-
- A: Alternative considered or rejected, with reason, if it mattered.
|
|
447
|
-
- F: Files, artifacts, or behaviors changed.
|
|
448
|
-
- T: Tests or validation outcome.
|
|
449
|
-
```
|
|
450
|
-
|
|
451
461
|
#### Multi-decision session entry
|
|
452
462
|
|
|
453
|
-
|
|
463
|
+
Richer alternative — use one entry when several decisions belong to one coherent task, plan, or user goal. Split entries when decisions affect unrelated subsystems, sub-projects, or goals.
|
|
454
464
|
|
|
455
465
|
```markdown
|
|
456
466
|
### Summary
|
|
@@ -461,16 +471,16 @@ Use one entry when several decisions belong to one coherent task, plan, or user
|
|
|
461
471
|
|
|
462
472
|
#### D1 - Short decision name
|
|
463
473
|
|
|
464
|
-
- D: State the choice.
|
|
465
|
-
- R: Explain the decisive reason in 1-3 bullets.
|
|
466
|
-
- A: Alternative considered or rejected, with reason, if it mattered.
|
|
467
|
-
- F: Files, artifacts, or behaviors changed.
|
|
468
|
-
- T: Tests or validation outcome.
|
|
474
|
+
- D: State the choice. (mandatory)
|
|
475
|
+
- R: Explain the decisive reason in 1-3 bullets. (mandatory)
|
|
476
|
+
- A: Alternative considered or rejected, with reason, if it mattered. (optional)
|
|
477
|
+
- F: Files, artifacts, or behaviors changed. (optional)
|
|
478
|
+
- T: Tests or validation outcome. (optional)
|
|
469
479
|
|
|
470
480
|
#### D2 - Short decision name
|
|
471
481
|
|
|
472
|
-
- D: State the choice.
|
|
473
|
-
- R: Explain the decisive reason in 1-3 bullets.
|
|
482
|
+
- D: State the choice. (mandatory)
|
|
483
|
+
- R: Explain the decisive reason in 1-3 bullets. (mandatory)
|
|
474
484
|
|
|
475
485
|
### Implementation
|
|
476
486
|
|
|
@@ -52,8 +52,13 @@ else:
|
|
|
52
52
|
)
|
|
53
53
|
|
|
54
54
|
if agent == "codex":
|
|
55
|
-
# Codex CLI UserPromptSubmit: systemMessage shown in UI
|
|
56
|
-
|
|
55
|
+
# Codex CLI UserPromptSubmit: systemMessage shown in UI.
|
|
56
|
+
# Project .codex/config.toml MCP servers load only for trusted directories.
|
|
57
|
+
codex_reminder = (
|
|
58
|
+
reminder + " (Codex loads the project memory_search MCP server from "
|
|
59
|
+
".codex/config.toml only if this directory is trusted.)"
|
|
60
|
+
)
|
|
61
|
+
print(json.dumps({"systemMessage": codex_reminder, "continue": True}))
|
|
57
62
|
elif agent == "cursor":
|
|
58
63
|
# Cursor sessionStart: additional_context (snake_case) injects into the
|
|
59
64
|
# conversation's initial system context. beforeSubmitPrompt cannot inject.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memory-seed
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: Portable local memory seed for file-reading AI coding agents
|
|
5
5
|
Author: Jean Nathan Tshibuyi
|
|
6
6
|
License: MIT
|
|
@@ -135,7 +135,7 @@ The result is a lightweight memory workflow you can understand, commit, review,
|
|
|
135
135
|
|
|
136
136
|
| Agent or client | Support path |
|
|
137
137
|
| --- | --- |
|
|
138
|
-
| Codex | Starts from `AGENTS.md`;
|
|
138
|
+
| Codex | Starts from `AGENTS.md`; MCP server auto-registered in `.codex/config.toml` (loads once the project directory is trusted). |
|
|
139
139
|
| Claude Code | Starts from `CLAUDE.md`; MCP server auto-registered via `uvx --from memory-seed`. |
|
|
140
140
|
| Gemini CLI | Starts from `GEMINI.md`. |
|
|
141
141
|
| Other file-reading agents | Start from `AGENTS.md` and follow nearest `.memory-seed/` runtime discovery. |
|
|
@@ -365,7 +365,11 @@ For the version distinction (`pip show memory-seed` reports the package version;
|
|
|
365
365
|
|
|
366
366
|
Memory Seed also includes a lightweight MCP server that lets agents search local session memory through structured tool calls instead of shelling out to broad compact summaries.
|
|
367
367
|
|
|
368
|
-
**Auto-registration:** `memory-seed init` and `memory-seed update` automatically register `uvx --from memory-seed memory-seed-mcp --stdio` in each supported vendor's config — `.
|
|
368
|
+
**Auto-registration:** `memory-seed init` and `memory-seed update` automatically register `uvx --from memory-seed memory-seed-mcp --stdio` in each supported vendor's MCP config — `.mcp.json` at the project root (Claude Code), `.cursor/mcp.json` (Cursor), `.gemini/settings.json` (Gemini CLI), and `.codex/config.toml` (Codex CLI). No manual config is needed for projects initialised with Memory Seed. The `uvx --from` form is used so the command works regardless of whether `~/.local/bin` is on the agent's PATH.
|
|
369
|
+
|
|
370
|
+
> **Claude Code reads project-scope MCP servers from `.mcp.json`, not `.claude/settings.json`** — the latter is for hooks and permissions only. Versions 2.2.0–2.3.0 wrote the server into `.claude/settings.json`, where Claude Code silently ignored it; `memory-seed update` now writes `.mcp.json` and removes the dead entry. Restart Claude Code and approve the project server, then confirm with `claude mcp list`.
|
|
371
|
+
|
|
372
|
+
> **Codex loads a project `.codex/config.toml` only for *trusted* directories.** Memory Seed writes the `[mcp_servers.memory-seed]` table there, but Codex ignores it until you trust the project (Codex prompts on first use of a directory, or set trust in Codex settings). After trusting, confirm with `codex mcp list`. `memory-seed doctor` warns if Codex hooks are present without this registration. If you hand-wrote the `memory-seed` entry in a non-standard TOML form (dotted keys, an inline table, or a header with a trailing comment) and it is outdated, `memory-seed update` will not auto-migrate it — `memory-seed doctor` flags it as needing a manual fix instead of silently leaving stale settings in place.
|
|
369
373
|
|
|
370
374
|
If you are configuring the server manually, run it over stdio:
|
|
371
375
|
|
|
@@ -422,6 +426,14 @@ memory_get_chunk(chunk_id, cwd=".")
|
|
|
422
426
|
|
|
423
427
|
The ranking engine stays local and CPU-friendly. MCP search uses a Model2Vec static embedding provider by default with the general-purpose `minishlab/potion-base-8M` model, combines semantic score with lexical and metadata scoring, then applies recency. If Model2Vec or the model cannot load or score a query, the server falls back to lexical, metadata, and recency ranking without failing the request. Use `--no-semantic` on `memory-seed-mcp --stdio` or `semantic_enabled=false` in `memory_search` to force fallback behavior.
|
|
424
428
|
|
|
429
|
+
### Performance characteristics
|
|
430
|
+
|
|
431
|
+
`memory_search` is a relevance-and-recall tool, not a faster `grep`. A plain `grep` will out-scan it on raw exact-match throughput; the search wins instead on *semantic recall* over session history (surfacing relevant entries that lack the literal query words) and on *agent-token efficiency* (returning a small ranked set of self-contained chunks with stable `chunk_id`s, so an agent fetches only the one or two full entries worth reading). The two are complementary: use `memory_search` for "what did we decide and why," and `grep` for exact-string scans across the whole repo.
|
|
432
|
+
|
|
433
|
+
Per-query latency, measured in-process on this repo (81 chunks across the session logs), is roughly **30 ms**, of which about **22 ms is reading and parsing the session `.md` files** — the search re-reads and re-parses every `sessions/*.md` on each call, with no persistent chunk or vector cache — and the embed + cosine + rank step adds only a few ms on top. Cold start adds a one-time cost on the *first ever* call on a machine: the Model2Vec weights download into the local HuggingFace cache (tens of MB); afterwards the static model loads in a few ms. Because the static model has no transformer forward pass, the dominant cost is file I/O, so per-query time grows linearly with total session-log size rather than with model complexity.
|
|
434
|
+
|
|
435
|
+
When driving the server through an MCP client (Claude Code, Cursor, Gemini), the latency you actually perceive is dominated by one-time startup, not per-query work: spawning `uvx --from memory-seed memory-seed-mcp` resolves and may install the package into an ephemeral environment the first time the server launches in a session. Once the server is up, each `memory_search` is the ~30 ms compute above plus a small JSON-RPC round-trip. At current log sizes there is no need to optimize; should logs grow large enough that the ~22 ms parse cost becomes noticeable, caching parsed chunks and their vectors keyed by file modification time would remove most of the per-query cost.
|
|
436
|
+
|
|
425
437
|
Session entries should include a YAML metadata block with `entry_id`, `user_initials`, `agent_type`, `project_path`, and `subproject_path`. Session entry headings may include optional minute-level timestamps, such as `## 2026-05-19 20:42 - Durable memory consolidation`. Session filenames stay date-only. Timestamped headings are backward compatible with older untimed headings and are exposed as `entry_datetime` in MCP search results when present.
|
|
426
438
|
|
|
427
439
|
For human-validatable search behavior, see the fixture-style tests in `tests/test_mcp_server.py`. They assert that specific queries return expected dated session entries first and include enough evidence for manual review.
|
|
@@ -23,7 +23,7 @@ class MemorySeedTests(unittest.TestCase):
|
|
|
23
23
|
return path
|
|
24
24
|
|
|
25
25
|
def test_version_reads_reusable_control_plane_version(self):
|
|
26
|
-
self.assertEqual(get_version(), "2.
|
|
26
|
+
self.assertEqual(get_version(), "2.4")
|
|
27
27
|
|
|
28
28
|
def test_version_at_least_orders_versions_numerically(self):
|
|
29
29
|
from memory_seed.core import _version_at_least
|
|
@@ -100,7 +100,7 @@ class MemorySeedTests(unittest.TestCase):
|
|
|
100
100
|
self.assertTrue(result.backed_up[0].startswith(".memory-seed/backups/"))
|
|
101
101
|
self.assertEqual((cwd / result.backed_up[0]).read_text(encoding="utf-8"), "existing")
|
|
102
102
|
self.assertIn(
|
|
103
|
-
"memory-system-version:
|
|
103
|
+
f"memory-system-version: {get_version()}",
|
|
104
104
|
(cwd / "AGENTS.md").read_text(encoding="utf-8"),
|
|
105
105
|
)
|
|
106
106
|
self.assertIn(".memory-seed/backups/", (cwd / ".gitignore").read_text(encoding="utf-8"))
|
|
@@ -121,7 +121,7 @@ class MemorySeedTests(unittest.TestCase):
|
|
|
121
121
|
init_project(cwd=cwd)
|
|
122
122
|
gemini = cwd / "GEMINI.md"
|
|
123
123
|
gemini.write_text(
|
|
124
|
-
gemini.read_text(encoding="utf-8").replace(
|
|
124
|
+
gemini.read_text(encoding="utf-8").replace(get_version(), "1.1"),
|
|
125
125
|
encoding="utf-8",
|
|
126
126
|
)
|
|
127
127
|
(cwd / "CLAUDE.md").unlink()
|
|
@@ -138,7 +138,7 @@ class MemorySeedTests(unittest.TestCase):
|
|
|
138
138
|
)
|
|
139
139
|
self.assertEqual(
|
|
140
140
|
result.version_mismatches,
|
|
141
|
-
[{"file": "GEMINI.md", "expected":
|
|
141
|
+
[{"file": "GEMINI.md", "expected": get_version(), "actual": "1.1"}],
|
|
142
142
|
)
|
|
143
143
|
|
|
144
144
|
def test_doctor_distinguishes_bootstrap_completeness_from_control_plane_health(self):
|
|
@@ -221,11 +221,11 @@ class MemorySeedTests(unittest.TestCase):
|
|
|
221
221
|
self.assertIn("CLAUDE.md", result.created)
|
|
222
222
|
self.assertTrue(any(path.endswith("/AGENTS.md") for path in result.backed_up))
|
|
223
223
|
self.assertIn(
|
|
224
|
-
"memory-system-version:
|
|
224
|
+
f"memory-system-version: {get_version()}",
|
|
225
225
|
(cwd / "AGENTS.md").read_text(encoding="utf-8"),
|
|
226
226
|
)
|
|
227
227
|
self.assertIn(
|
|
228
|
-
"memory-system-version:
|
|
228
|
+
f"memory-system-version: {get_version()}",
|
|
229
229
|
(cwd / "CLAUDE.md").read_text(encoding="utf-8"),
|
|
230
230
|
)
|
|
231
231
|
self.assertEqual(
|
|
@@ -239,7 +239,7 @@ class MemorySeedTests(unittest.TestCase):
|
|
|
239
239
|
init_project(cwd=cwd)
|
|
240
240
|
agents = cwd / "AGENTS.md"
|
|
241
241
|
agents.write_text(
|
|
242
|
-
agents.read_text(encoding="utf-8").replace(
|
|
242
|
+
agents.read_text(encoding="utf-8").replace(get_version(), "1.4"),
|
|
243
243
|
encoding="utf-8",
|
|
244
244
|
)
|
|
245
245
|
|
|
@@ -343,14 +343,29 @@ class MemorySeedTests(unittest.TestCase):
|
|
|
343
343
|
self.assertTrue(
|
|
344
344
|
any(path.endswith("/.memory-seed/agent-rules.md") for path in result.backed_up)
|
|
345
345
|
)
|
|
346
|
-
self.assertIn("memory-system-version:
|
|
346
|
+
self.assertIn(f"memory-system-version: {get_version()}", rules.read_text(encoding="utf-8"))
|
|
347
347
|
|
|
348
348
|
def test_control_plane_files_report_current_version(self):
|
|
349
349
|
for seed_file in SEED_FILES:
|
|
350
350
|
if not seed_file.source.suffix == ".md":
|
|
351
351
|
continue
|
|
352
352
|
content = seed_file.source.read_text(encoding="utf-8")
|
|
353
|
-
self.assertIn("memory-system-version:
|
|
353
|
+
self.assertIn(f"memory-system-version: {get_version()}", content, seed_file.destination)
|
|
354
|
+
|
|
355
|
+
def test_repo_root_control_plane_files_match_version(self):
|
|
356
|
+
# Guards the recurring release trap: the frontmatter version-bump sed is
|
|
357
|
+
# scoped to memory_seed/seed/ and .memory-seed/, so it silently skips
|
|
358
|
+
# this self-hosting repo's own root routing files (AGENTS/CLAUDE/GEMINI.md).
|
|
359
|
+
# doctor() catches the drift at runtime; this pins it in the suite so a
|
|
360
|
+
# missed root file fails CI instead of shipping (happened in 2.2.3 / 2.3.0).
|
|
361
|
+
repo_root = Path(__file__).resolve().parent.parent
|
|
362
|
+
expected = f"memory-system-version: {get_version()}"
|
|
363
|
+
for seed_file in SEED_FILES:
|
|
364
|
+
if not seed_file.source.suffix == ".md":
|
|
365
|
+
continue
|
|
366
|
+
live = repo_root / seed_file.destination
|
|
367
|
+
self.assertTrue(live.exists(), f"missing live control-plane file: {seed_file.destination}")
|
|
368
|
+
self.assertIn(expected, live.read_text(encoding="utf-8"), seed_file.destination)
|
|
354
369
|
|
|
355
370
|
def test_seed_files_use_memory_seed_runtime(self):
|
|
356
371
|
destinations = sorted(seed_file.destination for seed_file in SEED_FILES)
|
|
@@ -632,9 +647,14 @@ class SessionLogOrderingHookTests(unittest.TestCase):
|
|
|
632
647
|
import datetime
|
|
633
648
|
|
|
634
649
|
cwd = self.make_project()
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
650
|
+
# Use a timestamp 30 min in the past (> the 15 min staleness threshold)
|
|
651
|
+
# relative to the actual clock, so the test is not brittle near midnight
|
|
652
|
+
# where a hardcoded early-morning time would read as a future entry.
|
|
653
|
+
old = datetime.datetime.now() - datetime.timedelta(minutes=30)
|
|
654
|
+
day = old.strftime("%Y-%m-%d")
|
|
655
|
+
stamp = old.strftime("%H:%M")
|
|
656
|
+
(cwd / ".memory-seed" / "sessions" / f"{day}.md").write_text(
|
|
657
|
+
f"## {day} {stamp} - old entry\n\ntext\n",
|
|
638
658
|
encoding="utf-8",
|
|
639
659
|
)
|
|
640
660
|
self.assertIn("SESSION LOG REMINDER", self._run(cwd))
|
|
@@ -657,10 +677,12 @@ class SessionLogOrderingHookTests(unittest.TestCase):
|
|
|
657
677
|
import os
|
|
658
678
|
|
|
659
679
|
cwd = self.make_project()
|
|
660
|
-
|
|
661
|
-
|
|
680
|
+
old = datetime.datetime.now() - datetime.timedelta(minutes=30)
|
|
681
|
+
day = old.strftime("%Y-%m-%d")
|
|
682
|
+
stamp = old.strftime("%H:%M")
|
|
683
|
+
session_file = cwd / ".memory-seed" / "sessions" / f"{day}.md"
|
|
662
684
|
session_file.write_text(
|
|
663
|
-
f"## {
|
|
685
|
+
f"## {day} {stamp} - old entry\n\ntext\n",
|
|
664
686
|
encoding="utf-8",
|
|
665
687
|
)
|
|
666
688
|
# Touch the file to update mtime to now — simulating what git commit does.
|
|
@@ -681,12 +703,17 @@ class McpMergeTests(unittest.TestCase):
|
|
|
681
703
|
cwd = self.make_project()
|
|
682
704
|
init_project(cwd=cwd)
|
|
683
705
|
|
|
684
|
-
|
|
706
|
+
# Claude Code reads project-scope MCP servers from .mcp.json, not settings.json.
|
|
707
|
+
data = json.loads((cwd / ".mcp.json").read_text())
|
|
685
708
|
self.assertIn("memory-seed", data["mcpServers"])
|
|
686
709
|
entry = data["mcpServers"]["memory-seed"]
|
|
687
710
|
self.assertEqual(entry["command"], "uvx")
|
|
688
711
|
self.assertEqual(entry["args"], ["--from", "memory-seed", "memory-seed-mcp", "--stdio"])
|
|
689
|
-
self.
|
|
712
|
+
self.assertNotIn("type", entry)
|
|
713
|
+
|
|
714
|
+
# The dead settings.json mcpServers block must not be created.
|
|
715
|
+
settings = json.loads((cwd / ".claude" / "settings.json").read_text())
|
|
716
|
+
self.assertNotIn("mcpServers", settings)
|
|
690
717
|
|
|
691
718
|
def test_init_installs_mcp_for_cursor(self):
|
|
692
719
|
import json
|
|
@@ -718,6 +745,7 @@ class McpMergeTests(unittest.TestCase):
|
|
|
718
745
|
_merge_claude_mcp,
|
|
719
746
|
_merge_cursor_mcp,
|
|
720
747
|
_merge_gemini_mcp,
|
|
748
|
+
_merge_codex_mcp,
|
|
721
749
|
)
|
|
722
750
|
|
|
723
751
|
cwd = self.make_project()
|
|
@@ -727,15 +755,17 @@ class McpMergeTests(unittest.TestCase):
|
|
|
727
755
|
self.assertFalse(_merge_cursor_mcp(cwd))
|
|
728
756
|
self.assertTrue(_merge_gemini_mcp(cwd))
|
|
729
757
|
self.assertFalse(_merge_gemini_mcp(cwd))
|
|
758
|
+
self.assertTrue(_merge_codex_mcp(cwd))
|
|
759
|
+
self.assertFalse(_merge_codex_mcp(cwd))
|
|
730
760
|
|
|
731
761
|
def test_mcp_merge_updates_stale_args(self):
|
|
732
762
|
import json
|
|
733
763
|
|
|
734
764
|
cwd = self.make_project()
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
json.dumps({"mcpServers": {"memory-seed": {"command": "memory-seed-mcp", "args": ["--old"]
|
|
765
|
+
mcp_path = cwd / ".mcp.json"
|
|
766
|
+
# Legacy bare-command form (pre-uvx) under our key must migrate forward.
|
|
767
|
+
mcp_path.write_text(
|
|
768
|
+
json.dumps({"mcpServers": {"memory-seed": {"command": "memory-seed-mcp", "args": ["--old"]}}}),
|
|
739
769
|
encoding="utf-8",
|
|
740
770
|
)
|
|
741
771
|
|
|
@@ -743,7 +773,7 @@ class McpMergeTests(unittest.TestCase):
|
|
|
743
773
|
result = _merge_claude_mcp(cwd)
|
|
744
774
|
self.assertTrue(result)
|
|
745
775
|
|
|
746
|
-
data = json.loads(
|
|
776
|
+
data = json.loads(mcp_path.read_text())
|
|
747
777
|
entry = data["mcpServers"]["memory-seed"]
|
|
748
778
|
self.assertEqual(entry["command"], "uvx")
|
|
749
779
|
self.assertEqual(entry["args"], ["--from", "memory-seed", "memory-seed-mcp", "--stdio"])
|
|
@@ -752,9 +782,8 @@ class McpMergeTests(unittest.TestCase):
|
|
|
752
782
|
import json
|
|
753
783
|
|
|
754
784
|
cwd = self.make_project()
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
settings.write_text(
|
|
785
|
+
mcp_path = cwd / ".mcp.json"
|
|
786
|
+
mcp_path.write_text(
|
|
758
787
|
json.dumps({"mcpServers": {"other-server": {"command": "other-cmd", "args": []}}}),
|
|
759
788
|
encoding="utf-8",
|
|
760
789
|
)
|
|
@@ -762,9 +791,58 @@ class McpMergeTests(unittest.TestCase):
|
|
|
762
791
|
from memory_seed.core import _merge_claude_mcp
|
|
763
792
|
_merge_claude_mcp(cwd)
|
|
764
793
|
|
|
765
|
-
data = json.loads(
|
|
794
|
+
data = json.loads(mcp_path.read_text())
|
|
766
795
|
self.assertIn("other-server", data["mcpServers"])
|
|
767
796
|
self.assertEqual(data["mcpServers"]["other-server"]["command"], "other-cmd")
|
|
797
|
+
self.assertIn("memory-seed", data["mcpServers"])
|
|
798
|
+
|
|
799
|
+
def test_strip_removes_legacy_claude_settings_mcp(self):
|
|
800
|
+
import json
|
|
801
|
+
|
|
802
|
+
cwd = self.make_project()
|
|
803
|
+
settings = cwd / ".claude" / "settings.json"
|
|
804
|
+
settings.parent.mkdir(parents=True, exist_ok=True)
|
|
805
|
+
# A project seeded by 2.2.0-2.3.0: dead mcpServers block alongside a real hook.
|
|
806
|
+
settings.write_text(
|
|
807
|
+
json.dumps(
|
|
808
|
+
{
|
|
809
|
+
"hooks": {"Stop": [{"hooks": [{"type": "command", "command": "keep-me"}]}]},
|
|
810
|
+
"mcpServers": {
|
|
811
|
+
"memory-seed": {
|
|
812
|
+
"command": "uvx",
|
|
813
|
+
"args": ["--from", "memory-seed", "memory-seed-mcp", "--stdio"],
|
|
814
|
+
"type": "stdio",
|
|
815
|
+
}
|
|
816
|
+
},
|
|
817
|
+
}
|
|
818
|
+
),
|
|
819
|
+
encoding="utf-8",
|
|
820
|
+
)
|
|
821
|
+
|
|
822
|
+
from memory_seed.core import _strip_claude_settings_mcp
|
|
823
|
+
self.assertTrue(_strip_claude_settings_mcp(cwd))
|
|
824
|
+
|
|
825
|
+
data = json.loads(settings.read_text())
|
|
826
|
+
self.assertNotIn("mcpServers", data) # dead block removed, empty parent pruned
|
|
827
|
+
self.assertEqual(data["hooks"]["Stop"][0]["hooks"][0]["command"], "keep-me") # rest preserved
|
|
828
|
+
self.assertFalse(_strip_claude_settings_mcp(cwd)) # idempotent
|
|
829
|
+
|
|
830
|
+
def test_strip_preserves_foreign_settings_mcp(self):
|
|
831
|
+
import json
|
|
832
|
+
|
|
833
|
+
cwd = self.make_project()
|
|
834
|
+
settings = cwd / ".claude" / "settings.json"
|
|
835
|
+
settings.parent.mkdir(parents=True, exist_ok=True)
|
|
836
|
+
# A different server squatting our key must not be deleted.
|
|
837
|
+
settings.write_text(
|
|
838
|
+
json.dumps({"mcpServers": {"memory-seed": {"command": "some-other-server", "args": []}}}),
|
|
839
|
+
encoding="utf-8",
|
|
840
|
+
)
|
|
841
|
+
|
|
842
|
+
from memory_seed.core import _strip_claude_settings_mcp
|
|
843
|
+
self.assertFalse(_strip_claude_settings_mcp(cwd))
|
|
844
|
+
data = json.loads(settings.read_text())
|
|
845
|
+
self.assertEqual(data["mcpServers"]["memory-seed"]["command"], "some-other-server")
|
|
768
846
|
|
|
769
847
|
def test_gemini_mcp_merge_preserves_existing_hooks(self):
|
|
770
848
|
import json
|
|
@@ -785,6 +863,166 @@ class McpMergeTests(unittest.TestCase):
|
|
|
785
863
|
self.assertIn("Stop", data["hooks"])
|
|
786
864
|
self.assertEqual(data["hooks"]["Stop"][0]["hooks"][0]["command"], "existing")
|
|
787
865
|
|
|
866
|
+
def test_init_installs_mcp_for_codex(self):
|
|
867
|
+
import tomllib
|
|
868
|
+
|
|
869
|
+
cwd = self.make_project()
|
|
870
|
+
init_project(cwd=cwd)
|
|
871
|
+
|
|
872
|
+
# Codex reads project-scope MCP servers from .codex/config.toml.
|
|
873
|
+
data = tomllib.loads((cwd / ".codex" / "config.toml").read_text(encoding="utf-8"))
|
|
874
|
+
self.assertIn("memory-seed", data["mcp_servers"])
|
|
875
|
+
entry = data["mcp_servers"]["memory-seed"]
|
|
876
|
+
self.assertEqual(entry["command"], "uvx")
|
|
877
|
+
self.assertEqual(entry["args"], ["--from", "memory-seed", "memory-seed-mcp", "--stdio"])
|
|
878
|
+
|
|
879
|
+
def test_codex_mcp_merge_updates_stale_args(self):
|
|
880
|
+
import tomllib
|
|
881
|
+
|
|
882
|
+
cwd = self.make_project()
|
|
883
|
+
config_path = cwd / ".codex" / "config.toml"
|
|
884
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
885
|
+
# Legacy bare-command form (pre-uvx) under our key must migrate forward.
|
|
886
|
+
config_path.write_text(
|
|
887
|
+
'[mcp_servers.memory-seed]\n'
|
|
888
|
+
'command = "memory-seed-mcp"\n'
|
|
889
|
+
'args = ["--old"]\n',
|
|
890
|
+
encoding="utf-8",
|
|
891
|
+
)
|
|
892
|
+
|
|
893
|
+
from memory_seed.core import _merge_codex_mcp
|
|
894
|
+
self.assertTrue(_merge_codex_mcp(cwd))
|
|
895
|
+
|
|
896
|
+
data = tomllib.loads(config_path.read_text(encoding="utf-8"))
|
|
897
|
+
entry = data["mcp_servers"]["memory-seed"]
|
|
898
|
+
self.assertEqual(entry["command"], "uvx")
|
|
899
|
+
self.assertEqual(entry["args"], ["--from", "memory-seed", "memory-seed-mcp", "--stdio"])
|
|
900
|
+
|
|
901
|
+
def test_codex_mcp_merge_preserves_existing_config(self):
|
|
902
|
+
import tomllib
|
|
903
|
+
|
|
904
|
+
cwd = self.make_project()
|
|
905
|
+
config_path = cwd / ".codex" / "config.toml"
|
|
906
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
907
|
+
# Unrelated setting + comment + a foreign MCP server must all survive.
|
|
908
|
+
config_path.write_text(
|
|
909
|
+
"# my codex config\n"
|
|
910
|
+
'model = "gpt-5-codex"\n'
|
|
911
|
+
"\n"
|
|
912
|
+
"[mcp_servers.other]\n"
|
|
913
|
+
'command = "other-cmd"\n'
|
|
914
|
+
'args = []\n',
|
|
915
|
+
encoding="utf-8",
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
from memory_seed.core import _merge_codex_mcp
|
|
919
|
+
self.assertTrue(_merge_codex_mcp(cwd))
|
|
920
|
+
|
|
921
|
+
text = config_path.read_text(encoding="utf-8")
|
|
922
|
+
self.assertIn("# my codex config", text) # comment preserved
|
|
923
|
+
data = tomllib.loads(text)
|
|
924
|
+
self.assertEqual(data["model"], "gpt-5-codex") # unrelated setting preserved
|
|
925
|
+
self.assertEqual(data["mcp_servers"]["other"]["command"], "other-cmd") # foreign server kept
|
|
926
|
+
self.assertIn("memory-seed", data["mcp_servers"]) # ours appended
|
|
927
|
+
|
|
928
|
+
def test_codex_mcp_merge_preserves_foreign_server_on_our_key(self):
|
|
929
|
+
import tomllib
|
|
930
|
+
|
|
931
|
+
cwd = self.make_project()
|
|
932
|
+
config_path = cwd / ".codex" / "config.toml"
|
|
933
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
934
|
+
# A different server squatting our key must not be overwritten.
|
|
935
|
+
config_path.write_text(
|
|
936
|
+
"[mcp_servers.memory-seed]\n"
|
|
937
|
+
'command = "some-other-server"\n'
|
|
938
|
+
"args = []\n",
|
|
939
|
+
encoding="utf-8",
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
from memory_seed.core import _merge_codex_mcp
|
|
943
|
+
self.assertFalse(_merge_codex_mcp(cwd))
|
|
944
|
+
data = tomllib.loads(config_path.read_text(encoding="utf-8"))
|
|
945
|
+
self.assertEqual(data["mcp_servers"]["memory-seed"]["command"], "some-other-server")
|
|
946
|
+
|
|
947
|
+
def test_doctor_warns_when_codex_hooks_without_mcp(self):
|
|
948
|
+
from memory_seed.core import doctor, _merge_codex_mcp
|
|
949
|
+
|
|
950
|
+
cwd = self.make_project()
|
|
951
|
+
init_project(cwd=cwd)
|
|
952
|
+
# Simulate a project that has Codex hooks but no MCP registration yet.
|
|
953
|
+
(cwd / ".codex" / "config.toml").unlink()
|
|
954
|
+
|
|
955
|
+
result = doctor(cwd=cwd)
|
|
956
|
+
self.assertTrue(any("Codex" in w for w in result.warnings))
|
|
957
|
+
self.assertTrue(result.control_plane_ok) # warning is non-fatal
|
|
958
|
+
|
|
959
|
+
# After re-registering, the warning clears.
|
|
960
|
+
_merge_codex_mcp(cwd)
|
|
961
|
+
self.assertFalse(any("Codex" in w for w in doctor(cwd=cwd).warnings))
|
|
962
|
+
|
|
963
|
+
def test_doctor_warns_on_stale_manual_codex_mcp(self):
|
|
964
|
+
from memory_seed.core import doctor, _merge_codex_mcp, _codex_mcp_status
|
|
965
|
+
|
|
966
|
+
cwd = self.make_project()
|
|
967
|
+
init_project(cwd=cwd) # healthy control plane, so the non-fatal check is meaningful
|
|
968
|
+
config_path = cwd / ".codex" / "config.toml"
|
|
969
|
+
# Ours but stale, written as dotted keys -> no standard header to anchor a
|
|
970
|
+
# rewrite. Update must no-op, and doctor must NOT stay silent about it.
|
|
971
|
+
config_path.write_text(
|
|
972
|
+
'mcp_servers.memory-seed.command = "memory-seed-mcp"\n'
|
|
973
|
+
'mcp_servers.memory-seed.args = ["--old"]\n',
|
|
974
|
+
encoding="utf-8",
|
|
975
|
+
)
|
|
976
|
+
|
|
977
|
+
self.assertEqual(_codex_mcp_status(cwd), "stale-manual")
|
|
978
|
+
self.assertFalse(_merge_codex_mcp(cwd)) # safe no-op, not a corruption
|
|
979
|
+
|
|
980
|
+
result = doctor(cwd=cwd)
|
|
981
|
+
self.assertTrue(any("non-standard TOML form" in w for w in result.warnings))
|
|
982
|
+
self.assertTrue(result.control_plane_ok) # non-fatal
|
|
983
|
+
|
|
984
|
+
def test_doctor_warns_on_stale_fixable_codex_mcp(self):
|
|
985
|
+
from memory_seed.core import doctor, _merge_codex_mcp, _codex_mcp_status
|
|
986
|
+
|
|
987
|
+
cwd = self.make_project()
|
|
988
|
+
config_path = cwd / ".codex" / "config.toml"
|
|
989
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
990
|
+
# Ours but stale, standard header form -> update can migrate it.
|
|
991
|
+
config_path.write_text(
|
|
992
|
+
"[mcp_servers.memory-seed]\n"
|
|
993
|
+
'command = "memory-seed-mcp"\n'
|
|
994
|
+
'args = ["--old"]\n',
|
|
995
|
+
encoding="utf-8",
|
|
996
|
+
)
|
|
997
|
+
|
|
998
|
+
self.assertEqual(_codex_mcp_status(cwd), "stale-fixable")
|
|
999
|
+
result = doctor(cwd=cwd)
|
|
1000
|
+
self.assertTrue(any("outdated memory-seed MCP entry" in w for w in result.warnings))
|
|
1001
|
+
|
|
1002
|
+
# update migrates it; warning then clears and status is current.
|
|
1003
|
+
self.assertTrue(_merge_codex_mcp(cwd))
|
|
1004
|
+
self.assertEqual(_codex_mcp_status(cwd), "current")
|
|
1005
|
+
self.assertFalse(any("Codex" in w for w in doctor(cwd=cwd).warnings))
|
|
1006
|
+
|
|
1007
|
+
def test_codex_mcp_status_current_and_foreign_are_quiet(self):
|
|
1008
|
+
from memory_seed.core import doctor, _merge_codex_mcp, _codex_mcp_status
|
|
1009
|
+
|
|
1010
|
+
cwd = self.make_project()
|
|
1011
|
+
# current
|
|
1012
|
+
_merge_codex_mcp(cwd)
|
|
1013
|
+
self.assertEqual(_codex_mcp_status(cwd), "current")
|
|
1014
|
+
self.assertFalse(any("Codex" in w for w in doctor(cwd=cwd).warnings))
|
|
1015
|
+
|
|
1016
|
+
# foreign: a different server squatting our key
|
|
1017
|
+
(cwd / ".codex" / "config.toml").write_text(
|
|
1018
|
+
"[mcp_servers.memory-seed]\n"
|
|
1019
|
+
'command = "some-other-server"\n'
|
|
1020
|
+
"args = []\n",
|
|
1021
|
+
encoding="utf-8",
|
|
1022
|
+
)
|
|
1023
|
+
self.assertEqual(_codex_mcp_status(cwd), "foreign")
|
|
1024
|
+
self.assertFalse(any("Codex" in w for w in doctor(cwd=cwd).warnings))
|
|
1025
|
+
|
|
788
1026
|
|
|
789
1027
|
class RetrievalCheckPathTests(unittest.TestCase):
|
|
790
1028
|
SCRIPT = Path("memory_seed/seed/.memory-seed/hooks/memory-retrieval-check.py").resolve()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{memory_seed-2.3.0 → memory_seed-2.4.0}/memory_seed/seed/.memory-seed/hooks/session-log-check.py
RENAMED
|
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
|