memory-seed 2.1.2__tar.gz → 2.2.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.
Files changed (39) hide show
  1. {memory_seed-2.1.2 → memory_seed-2.2.0}/PKG-INFO +32 -2
  2. {memory_seed-2.1.2 → memory_seed-2.2.0}/README.md +31 -1
  3. {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/core.py +152 -111
  4. {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/agent-rules.md +24 -19
  5. {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/hooks/memory-retrieval-check.py +22 -6
  6. {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/hooks/session-log-check.py +4 -1
  7. {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/project-bootstrap.md +2 -2
  8. {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/memory_consolidation.md +4 -4
  9. {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed.egg-info/PKG-INFO +32 -2
  10. {memory_seed-2.1.2 → memory_seed-2.2.0}/pyproject.toml +1 -1
  11. {memory_seed-2.1.2 → memory_seed-2.2.0}/tests/test_memory_seed.py +220 -0
  12. {memory_seed-2.1.2 → memory_seed-2.2.0}/tests/test_session_schema.py +8 -8
  13. {memory_seed-2.1.2 → memory_seed-2.2.0}/LICENSE +0 -0
  14. {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/__init__.py +0 -0
  15. {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/cli.py +0 -0
  16. {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/mcp_server.py +0 -0
  17. {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/mcp_validate.py +0 -0
  18. {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/archive/.gitkeep +0 -0
  19. {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/sessions/.gitkeep +0 -0
  20. {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/code_search.md +0 -0
  21. {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/data_architecture.md +0 -0
  22. {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/index.md +0 -0
  23. {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/local_compilation.md +0 -0
  24. {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/memory_doctor.md +0 -0
  25. {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/release_publishing.md +0 -0
  26. {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/security_triage.md +0 -0
  27. {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/AGENTS.md +0 -0
  28. {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/CLAUDE.md +0 -0
  29. {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/GEMINI.md +0 -0
  30. {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/semantic_cache.py +0 -0
  31. {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed.egg-info/SOURCES.txt +0 -0
  32. {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed.egg-info/dependency_links.txt +0 -0
  33. {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed.egg-info/entry_points.txt +0 -0
  34. {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed.egg-info/requires.txt +0 -0
  35. {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed.egg-info/top_level.txt +0 -0
  36. {memory_seed-2.1.2 → memory_seed-2.2.0}/setup.cfg +0 -0
  37. {memory_seed-2.1.2 → memory_seed-2.2.0}/tests/test_mcp_server.py +0 -0
  38. {memory_seed-2.1.2 → memory_seed-2.2.0}/tests/test_mcp_validation.py +0 -0
  39. {memory_seed-2.1.2 → memory_seed-2.2.0}/tests/test_semantic_cache.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memory-seed
3
- Version: 2.1.2
3
+ Version: 2.2.0
4
4
  Summary: Portable local memory seed for file-reading AI coding agents
5
5
  Author: Jean Nathan Tshibuyi
6
6
  License: MIT
@@ -30,6 +30,12 @@ Memory Seed is a portable local memory system for AI coding agents. It plants a
30
30
 
31
31
  It is built first for solo developers who move between Codex, Claude Code, Gemini CLI, and other file-reading agents. Teams can also use it to standardize local agent memory across repositories without introducing a database or hosted memory service.
32
32
 
33
+ ## Demo
34
+
35
+ https://github.com/user-attachments/assets/b1c64d9e-4a67-4bc2-a030-d8ba7f17ccfe
36
+
37
+ [▶ Watch the 30-second demo](https://github.com/jnl-tshi/memory-seed/releases/download/v2.1.3/memory-seed-demo.mp4) if the player above does not load.
38
+
33
39
  ## Quickstart
34
40
 
35
41
  From the root of a project where you want local agent memory:
@@ -135,6 +141,24 @@ The result is a lightweight memory workflow you can understand, commit, review,
135
141
  | Other file-reading agents | Start from `AGENTS.md` and follow nearest `.memory-seed/` runtime discovery. |
136
142
  | MCP-capable clients | Use `memory_search` and `memory_get_chunk` through `memory-seed-mcp --stdio`. |
137
143
 
144
+ ## Agent Hooks
145
+
146
+ `memory-seed init` and `memory-seed update` install lifecycle hooks that keep memory current without relying on the agent to remember. Each hook is merged into the agent's own config file (existing settings are preserved), so it works regardless of which agent opens the project:
147
+
148
+ | Agent | Config file | Session-log reminder | Memory-retrieval reminder |
149
+ | --- | --- | --- | --- |
150
+ | Claude Code | `.claude/settings.json` | `Stop` | `UserPromptSubmit` |
151
+ | Codex CLI | `.codex/hooks.json` | `Stop` | `UserPromptSubmit` |
152
+ | Gemini CLI | `.gemini/settings.json` | `Stop` | `UserPromptSubmit` |
153
+ | Cursor | `.cursor/hooks.json` | `afterAgentResponse` | `sessionStart` |
154
+
155
+ Both reminders are cross-platform Python scripts in `.memory-seed/hooks/`:
156
+
157
+ - `session-log-check.py` — after a turn, reminds the agent to append a session-log entry if none was written in the last 15 minutes, and warns if the day's entries are out of ascending time order.
158
+ - `memory-retrieval-check.py` — before substantive work, reminds the agent to retrieve prior context (`memory_search` or the most recent session files). Gated by an 8-hour marker file so it fires about once per working session.
159
+
160
+ The hooks nudge; they never block. The scripts use Python 3.11+, which Memory Seed already requires.
161
+
138
162
  ## Reusable Seed Files
139
163
 
140
164
  ```text
@@ -144,6 +168,9 @@ GEMINI.md
144
168
  .memory-seed/
145
169
  agent-rules.md
146
170
  project-bootstrap.md
171
+ hooks/
172
+ session-log-check.py
173
+ memory-retrieval-check.py
147
174
  skills/
148
175
  index.md
149
176
  code_search.md
@@ -165,6 +192,7 @@ GEMINI.md
165
192
  project-bootstrap.md
166
193
  index.md
167
194
  policy.md
195
+ hooks/
168
196
  skills/
169
197
  sessions/
170
198
  archive/
@@ -337,7 +365,9 @@ For the version distinction (`pip show memory-seed` reports the package version;
337
365
 
338
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.
339
367
 
340
- Run it over stdio:
368
+ **Auto-registration:** `memory-seed init` and `memory-seed update` automatically register `memory-seed-mcp --stdio` in each supported vendor's config — `.claude/settings.json` (Claude Code), `.cursor/mcp.json` (Cursor), and `.gemini/settings.json` (Gemini CLI). No manual config is needed for projects initialised with Memory Seed.
369
+
370
+ If you are configuring the server manually, run it over stdio:
341
371
 
342
372
  ```powershell
343
373
  uvx --from memory-seed memory-seed-mcp --stdio
@@ -9,6 +9,12 @@ Memory Seed is a portable local memory system for AI coding agents. It plants a
9
9
 
10
10
  It is built first for solo developers who move between Codex, Claude Code, Gemini CLI, and other file-reading agents. Teams can also use it to standardize local agent memory across repositories without introducing a database or hosted memory service.
11
11
 
12
+ ## Demo
13
+
14
+ https://github.com/user-attachments/assets/b1c64d9e-4a67-4bc2-a030-d8ba7f17ccfe
15
+
16
+ [▶ Watch the 30-second demo](https://github.com/jnl-tshi/memory-seed/releases/download/v2.1.3/memory-seed-demo.mp4) if the player above does not load.
17
+
12
18
  ## Quickstart
13
19
 
14
20
  From the root of a project where you want local agent memory:
@@ -114,6 +120,24 @@ The result is a lightweight memory workflow you can understand, commit, review,
114
120
  | Other file-reading agents | Start from `AGENTS.md` and follow nearest `.memory-seed/` runtime discovery. |
115
121
  | MCP-capable clients | Use `memory_search` and `memory_get_chunk` through `memory-seed-mcp --stdio`. |
116
122
 
123
+ ## Agent Hooks
124
+
125
+ `memory-seed init` and `memory-seed update` install lifecycle hooks that keep memory current without relying on the agent to remember. Each hook is merged into the agent's own config file (existing settings are preserved), so it works regardless of which agent opens the project:
126
+
127
+ | Agent | Config file | Session-log reminder | Memory-retrieval reminder |
128
+ | --- | --- | --- | --- |
129
+ | Claude Code | `.claude/settings.json` | `Stop` | `UserPromptSubmit` |
130
+ | Codex CLI | `.codex/hooks.json` | `Stop` | `UserPromptSubmit` |
131
+ | Gemini CLI | `.gemini/settings.json` | `Stop` | `UserPromptSubmit` |
132
+ | Cursor | `.cursor/hooks.json` | `afterAgentResponse` | `sessionStart` |
133
+
134
+ Both reminders are cross-platform Python scripts in `.memory-seed/hooks/`:
135
+
136
+ - `session-log-check.py` — after a turn, reminds the agent to append a session-log entry if none was written in the last 15 minutes, and warns if the day's entries are out of ascending time order.
137
+ - `memory-retrieval-check.py` — before substantive work, reminds the agent to retrieve prior context (`memory_search` or the most recent session files). Gated by an 8-hour marker file so it fires about once per working session.
138
+
139
+ The hooks nudge; they never block. The scripts use Python 3.11+, which Memory Seed already requires.
140
+
117
141
  ## Reusable Seed Files
118
142
 
119
143
  ```text
@@ -123,6 +147,9 @@ GEMINI.md
123
147
  .memory-seed/
124
148
  agent-rules.md
125
149
  project-bootstrap.md
150
+ hooks/
151
+ session-log-check.py
152
+ memory-retrieval-check.py
126
153
  skills/
127
154
  index.md
128
155
  code_search.md
@@ -144,6 +171,7 @@ GEMINI.md
144
171
  project-bootstrap.md
145
172
  index.md
146
173
  policy.md
174
+ hooks/
147
175
  skills/
148
176
  sessions/
149
177
  archive/
@@ -316,7 +344,9 @@ For the version distinction (`pip show memory-seed` reports the package version;
316
344
 
317
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.
318
346
 
319
- Run it over stdio:
347
+ **Auto-registration:** `memory-seed init` and `memory-seed update` automatically register `memory-seed-mcp --stdio` in each supported vendor's config — `.claude/settings.json` (Claude Code), `.cursor/mcp.json` (Cursor), and `.gemini/settings.json` (Gemini CLI). No manual config is needed for projects initialised with Memory Seed.
348
+
349
+ If you are configuring the server manually, run it over stdio:
320
350
 
321
351
  ```powershell
322
352
  uvx --from memory-seed memory-seed-mcp --stdio
@@ -128,6 +128,10 @@ _CODEX_RETRIEVAL_COMMAND = "python3 .memory-seed/hooks/memory-retrieval-check.py
128
128
  _CURSOR_RETRIEVAL_COMMAND = "python3 .memory-seed/hooks/memory-retrieval-check.py --cursor"
129
129
  _GEMINI_RETRIEVAL_COMMAND = "python3 .memory-seed/hooks/memory-retrieval-check.py --gemini"
130
130
 
131
+ _MCP_SERVER_COMMAND = "memory-seed-mcp"
132
+ _MCP_SERVER_ARGS = ["--stdio"]
133
+ _MCP_SERVER_KEY = "memory-seed"
134
+
131
135
  BOOTSTRAP_GENERATED_FILES = [
132
136
  ".memory-seed/index.md",
133
137
  ".memory-seed/policy.md",
@@ -191,132 +195,54 @@ def resolve_runtime(cwd: str | Path = ".") -> Runtime:
191
195
 
192
196
 
193
197
  def _merge_cursor_hook(target_root: Path) -> bool:
194
- """Add the session-log afterAgentResponse hook to .cursor/hooks.json."""
195
- hooks_path = target_root / ".cursor" / "hooks.json"
196
-
197
- data: dict = {}
198
- if hooks_path.exists():
199
- try:
200
- with open(hooks_path) as f:
201
- data = json.load(f)
202
- except (json.JSONDecodeError, OSError):
203
- data = {}
204
-
205
- data.setdefault("version", 1)
206
- for entry in data.get("hooks", {}).get("afterAgentResponse", []):
207
- if entry.get("command") == _CURSOR_HOOK_COMMAND:
208
- return False
209
-
210
- data.setdefault("hooks", {}).setdefault("afterAgentResponse", []).append(
211
- {"command": _CURSOR_HOOK_COMMAND}
198
+ """Upsert the session-log afterAgentResponse hook in .cursor/hooks.json."""
199
+ return _merge_cursor_event_hook(
200
+ target_root / ".cursor" / "hooks.json",
201
+ "afterAgentResponse",
202
+ _CURSOR_HOOK_COMMAND,
203
+ "session-log-check.py",
212
204
  )
213
205
 
214
- hooks_path.parent.mkdir(parents=True, exist_ok=True)
215
- with open(hooks_path, "w") as f:
216
- json.dump(data, f, indent=2)
217
- f.write("\n")
218
-
219
- return True
220
-
221
206
 
222
207
  def _merge_gemini_hook(target_root: Path) -> bool:
223
- """Add the session-log Stop hook to .gemini/settings.json."""
224
- settings_path = target_root / ".gemini" / "settings.json"
225
-
226
- data: dict = {}
227
- if settings_path.exists():
228
- try:
229
- with open(settings_path) as f:
230
- data = json.load(f)
231
- except (json.JSONDecodeError, OSError):
232
- data = {}
233
-
234
- for group in data.get("hooks", {}).get("Stop", []):
235
- for hook in group.get("hooks", []):
236
- if hook.get("command") == _GEMINI_HOOK_COMMAND:
237
- return False
238
-
239
- data.setdefault("hooks", {}).setdefault("Stop", []).append(
240
- {"hooks": [{"type": "command", "command": _GEMINI_HOOK_COMMAND}]}
208
+ """Upsert the session-log Stop hook in .gemini/settings.json."""
209
+ return _merge_grouped_hook(
210
+ target_root / ".gemini" / "settings.json",
211
+ "Stop",
212
+ _GEMINI_HOOK_COMMAND,
213
+ "session-log-check.py",
241
214
  )
242
215
 
243
- settings_path.parent.mkdir(parents=True, exist_ok=True)
244
- with open(settings_path, "w") as f:
245
- json.dump(data, f, indent=2)
246
- f.write("\n")
247
-
248
- return True
249
-
250
216
 
251
217
  def _merge_codex_hook(target_root: Path) -> bool:
252
- """Add the session-log Stop hook to .codex/hooks.json, merging with existing content.
253
-
254
- Returns True if the file was created or modified, False if the hook was already present.
255
- """
256
- hooks_path = target_root / ".codex" / "hooks.json"
257
-
258
- data: dict = {}
259
- if hooks_path.exists():
260
- try:
261
- with open(hooks_path) as f:
262
- data = json.load(f)
263
- except (json.JSONDecodeError, OSError):
264
- data = {}
265
-
266
- for group in data.get("hooks", {}).get("Stop", []):
267
- for hook in group.get("hooks", []):
268
- if hook.get("command") == _CODEX_HOOK_COMMAND:
269
- return False
270
-
271
- data.setdefault("hooks", {}).setdefault("Stop", []).append(
272
- {"hooks": [{"type": "command", "command": _CODEX_HOOK_COMMAND}]}
218
+ """Upsert the session-log Stop hook in .codex/hooks.json."""
219
+ return _merge_grouped_hook(
220
+ target_root / ".codex" / "hooks.json",
221
+ "Stop",
222
+ _CODEX_HOOK_COMMAND,
223
+ "session-log-check.py",
273
224
  )
274
225
 
275
- hooks_path.parent.mkdir(parents=True, exist_ok=True)
276
- with open(hooks_path, "w") as f:
277
- json.dump(data, f, indent=2)
278
- f.write("\n")
279
-
280
- return True
281
-
282
226
 
283
227
  def _merge_claude_hook(target_root: Path) -> bool:
284
- """Add the session-log Stop hook to .claude/settings.json, merging with existing content.
285
-
286
- Returns True if the file was created or modified, False if the hook was already present.
287
- """
288
- settings_path = target_root / ".claude" / "settings.json"
289
-
290
- data: dict = {}
291
- if settings_path.exists():
292
- try:
293
- with open(settings_path) as f:
294
- data = json.load(f)
295
- except (json.JSONDecodeError, OSError):
296
- data = {}
297
-
298
- for group in data.get("hooks", {}).get("Stop", []):
299
- for hook in group.get("hooks", []):
300
- if hook.get("command") == _CLAUDE_HOOK_COMMAND:
301
- return False
302
-
303
- data.setdefault("hooks", {}).setdefault("Stop", []).append(
304
- {"hooks": [{"type": "command", "command": _CLAUDE_HOOK_COMMAND}]}
228
+ """Upsert the session-log Stop hook in .claude/settings.json."""
229
+ return _merge_grouped_hook(
230
+ target_root / ".claude" / "settings.json",
231
+ "Stop",
232
+ _CLAUDE_HOOK_COMMAND,
233
+ "session-log-check.py",
305
234
  )
306
235
 
307
- settings_path.parent.mkdir(parents=True, exist_ok=True)
308
- with open(settings_path, "w") as f:
309
- json.dump(data, f, indent=2)
310
- f.write("\n")
311
-
312
- return True
313
-
314
236
 
315
- def _merge_grouped_hook(config_path: Path, event: str, command: str) -> bool:
316
- """Add a command hook under hooks.<event> in matcher-group form.
237
+ def _merge_grouped_hook(config_path: Path, event: str, command: str, script_name: str) -> bool:
238
+ """Upsert a command hook under hooks.<event> in matcher-group form.
317
239
 
318
240
  Used for Claude Code, Codex, and Gemini, which share the
319
- hooks.<event>[].hooks[].{type, command} shape. Idempotent.
241
+ hooks.<event>[].hooks[].{type, command} shape.
242
+
243
+ Identifies our entry by script_name (the stable filename). If an entry
244
+ with that script is found with a different command, updates it in place.
245
+ Returns True if the file was written, False if already current.
320
246
  """
321
247
  data: dict = {}
322
248
  if config_path.exists():
@@ -330,6 +256,13 @@ def _merge_grouped_hook(config_path: Path, event: str, command: str) -> bool:
330
256
  for hook in group.get("hooks", []):
331
257
  if hook.get("command") == command:
332
258
  return False
259
+ if script_name in (hook.get("command") or ""):
260
+ hook["command"] = command
261
+ config_path.parent.mkdir(parents=True, exist_ok=True)
262
+ with open(config_path, "w") as f:
263
+ json.dump(data, f, indent=2)
264
+ f.write("\n")
265
+ return True
333
266
 
334
267
  data.setdefault("hooks", {}).setdefault(event, []).append(
335
268
  {"hooks": [{"type": "command", "command": command}]}
@@ -343,8 +276,12 @@ def _merge_grouped_hook(config_path: Path, event: str, command: str) -> bool:
343
276
  return True
344
277
 
345
278
 
346
- def _merge_cursor_event_hook(config_path: Path, event: str, command: str) -> bool:
347
- """Add a command hook under hooks.<event> in Cursor's flat list form."""
279
+ def _merge_cursor_event_hook(config_path: Path, event: str, command: str, script_name: str) -> bool:
280
+ """Upsert a command hook under hooks.<event> in Cursor's flat list form.
281
+
282
+ Identifies our entry by script_name. Updates in place if command changed.
283
+ Returns True if the file was written, False if already current.
284
+ """
348
285
  data: dict = {}
349
286
  if config_path.exists():
350
287
  try:
@@ -357,6 +294,13 @@ def _merge_cursor_event_hook(config_path: Path, event: str, command: str) -> boo
357
294
  for entry in data.get("hooks", {}).get(event, []):
358
295
  if entry.get("command") == command:
359
296
  return False
297
+ if script_name in (entry.get("command") or ""):
298
+ entry["command"] = command
299
+ config_path.parent.mkdir(parents=True, exist_ok=True)
300
+ with open(config_path, "w") as f:
301
+ json.dump(data, f, indent=2)
302
+ f.write("\n")
303
+ return True
360
304
 
361
305
  data.setdefault("hooks", {}).setdefault(event, []).append({"command": command})
362
306
 
@@ -373,6 +317,7 @@ def _merge_claude_retrieval_hook(target_root: Path) -> bool:
373
317
  target_root / ".claude" / "settings.json",
374
318
  "UserPromptSubmit",
375
319
  _CLAUDE_RETRIEVAL_COMMAND,
320
+ "memory-retrieval-check.py",
376
321
  )
377
322
 
378
323
 
@@ -381,6 +326,7 @@ def _merge_codex_retrieval_hook(target_root: Path) -> bool:
381
326
  target_root / ".codex" / "hooks.json",
382
327
  "UserPromptSubmit",
383
328
  _CODEX_RETRIEVAL_COMMAND,
329
+ "memory-retrieval-check.py",
384
330
  )
385
331
 
386
332
 
@@ -389,6 +335,7 @@ def _merge_gemini_retrieval_hook(target_root: Path) -> bool:
389
335
  target_root / ".gemini" / "settings.json",
390
336
  "UserPromptSubmit",
391
337
  _GEMINI_RETRIEVAL_COMMAND,
338
+ "memory-retrieval-check.py",
392
339
  )
393
340
 
394
341
 
@@ -397,9 +344,97 @@ def _merge_cursor_retrieval_hook(target_root: Path) -> bool:
397
344
  target_root / ".cursor" / "hooks.json",
398
345
  "sessionStart",
399
346
  _CURSOR_RETRIEVAL_COMMAND,
347
+ "memory-retrieval-check.py",
400
348
  )
401
349
 
402
350
 
351
+ def _merge_claude_mcp(target_root: Path) -> bool:
352
+ """Upsert the memory-seed-mcp stdio server entry in .claude/settings.json."""
353
+ settings_path = target_root / ".claude" / "settings.json"
354
+ expected = {"command": _MCP_SERVER_COMMAND, "args": _MCP_SERVER_ARGS, "type": "stdio"}
355
+
356
+ data: dict = {}
357
+ if settings_path.exists():
358
+ try:
359
+ with open(settings_path) as f:
360
+ data = json.load(f)
361
+ except (json.JSONDecodeError, OSError):
362
+ data = {}
363
+
364
+ existing = data.get("mcpServers", {}).get(_MCP_SERVER_KEY, {})
365
+ if existing == expected:
366
+ return False
367
+ if existing and existing.get("command") != _MCP_SERVER_COMMAND:
368
+ return False # a different server is using this key; don't overwrite
369
+
370
+ data.setdefault("mcpServers", {})[_MCP_SERVER_KEY] = expected
371
+
372
+ settings_path.parent.mkdir(parents=True, exist_ok=True)
373
+ with open(settings_path, "w") as f:
374
+ json.dump(data, f, indent=2)
375
+ f.write("\n")
376
+
377
+ return True
378
+
379
+
380
+ def _merge_cursor_mcp(target_root: Path) -> bool:
381
+ """Upsert the memory-seed-mcp stdio server entry in .cursor/mcp.json."""
382
+ mcp_path = target_root / ".cursor" / "mcp.json"
383
+ expected = {"command": _MCP_SERVER_COMMAND, "args": _MCP_SERVER_ARGS}
384
+
385
+ data: dict = {}
386
+ if mcp_path.exists():
387
+ try:
388
+ with open(mcp_path) as f:
389
+ data = json.load(f)
390
+ except (json.JSONDecodeError, OSError):
391
+ data = {}
392
+
393
+ existing = data.get("mcpServers", {}).get(_MCP_SERVER_KEY, {})
394
+ if existing == expected:
395
+ return False
396
+ if existing and existing.get("command") != _MCP_SERVER_COMMAND:
397
+ return False # a different server is using this key; don't overwrite
398
+
399
+ data.setdefault("mcpServers", {})[_MCP_SERVER_KEY] = expected
400
+
401
+ mcp_path.parent.mkdir(parents=True, exist_ok=True)
402
+ with open(mcp_path, "w") as f:
403
+ json.dump(data, f, indent=2)
404
+ f.write("\n")
405
+
406
+ return True
407
+
408
+
409
+ def _merge_gemini_mcp(target_root: Path) -> bool:
410
+ """Upsert the memory-seed-mcp stdio server entry in .gemini/settings.json."""
411
+ settings_path = target_root / ".gemini" / "settings.json"
412
+ expected = {"command": _MCP_SERVER_COMMAND, "args": _MCP_SERVER_ARGS}
413
+
414
+ data: dict = {}
415
+ if settings_path.exists():
416
+ try:
417
+ with open(settings_path) as f:
418
+ data = json.load(f)
419
+ except (json.JSONDecodeError, OSError):
420
+ data = {}
421
+
422
+ existing = data.get("mcpServers", {}).get(_MCP_SERVER_KEY, {})
423
+ if existing == expected:
424
+ return False
425
+ if existing and existing.get("command") != _MCP_SERVER_COMMAND:
426
+ return False # a different server is using this key; don't overwrite
427
+
428
+ data.setdefault("mcpServers", {})[_MCP_SERVER_KEY] = expected
429
+
430
+ settings_path.parent.mkdir(parents=True, exist_ok=True)
431
+ with open(settings_path, "w") as f:
432
+ json.dump(data, f, indent=2)
433
+ f.write("\n")
434
+
435
+ return True
436
+
437
+
403
438
  def init_project(cwd: str | Path = ".", dry_run: bool = False, force: bool = False) -> InitResult:
404
439
  target_root = Path(cwd).resolve()
405
440
  planned = [seed_file.destination for seed_file in SEED_FILES]
@@ -445,6 +480,9 @@ def init_project(cwd: str | Path = ".", dry_run: bool = False, force: bool = Fal
445
480
  (_merge_codex_retrieval_hook, ".codex/hooks.json"),
446
481
  (_merge_cursor_retrieval_hook, ".cursor/hooks.json"),
447
482
  (_merge_gemini_retrieval_hook, ".gemini/settings.json"),
483
+ (_merge_claude_mcp, ".claude/settings.json"),
484
+ (_merge_cursor_mcp, ".cursor/mcp.json"),
485
+ (_merge_gemini_mcp, ".gemini/settings.json"),
448
486
  )
449
487
  for merge, destination in hook_merges:
450
488
  if merge(target_root) and destination not in created:
@@ -507,6 +545,9 @@ def update_project(cwd: str | Path = ".", dry_run: bool = False) -> InitResult:
507
545
  (_merge_codex_retrieval_hook, ".codex/hooks.json"),
508
546
  (_merge_cursor_retrieval_hook, ".cursor/hooks.json"),
509
547
  (_merge_gemini_retrieval_hook, ".gemini/settings.json"),
548
+ (_merge_claude_mcp, ".claude/settings.json"),
549
+ (_merge_cursor_mcp, ".cursor/mcp.json"),
550
+ (_merge_gemini_mcp, ".gemini/settings.json"),
510
551
  )
511
552
  for merge, destination in hook_merges:
512
553
  if merge(target_root) and destination not in created:
@@ -89,7 +89,7 @@ Do not read or apply `.memory-seed/project-bootstrap.md` during normal operating
89
89
 
90
90
  ## History Retrieval And Conflict Resolution
91
91
 
92
- Use MCP history retrieval when prior decisions, rationale, unresolved risks, architecture, policy, bootstrap behavior, release history, or "why was this done" matters. This is a quick-start for agents that can call MCP tools.
92
+ Use MCP history retrieval when prior decisions, reason, unresolved risks, architecture, policy, bootstrap behavior, release history, or "why was this done" matters. This is a quick-start for agents that can call MCP tools.
93
93
 
94
94
  ### When To Search
95
95
 
@@ -155,7 +155,7 @@ Use the fetched chunk text, not just the excerpt, when making or evaluating a co
155
155
 
156
156
  If MCP tools are unavailable, read recent and relevant `.memory-seed/sessions/YYYY-MM-DD.md` files directly. Start with the last two session files, then search older dated files by keyword if needed. Apply the same authority and conflict rules below.
157
157
 
158
- Current files are the active authority: `.memory-seed/index.md`, `.memory-seed/policy.md`, active `.memory-seed/skills/*.md`, and source/config files for implementation truth. Session history is evidence and rationale, not automatic authority.
158
+ Current files are the active authority: `.memory-seed/index.md`, `.memory-seed/policy.md`, active `.memory-seed/skills/*.md`, and source/config files for implementation truth. Session history is evidence and reason, not automatic authority.
159
159
 
160
160
  When history conflicts with current authority files, resolve by timeline only when all clear supersession criteria are met:
161
161
 
@@ -300,7 +300,7 @@ This is the invariant the "do not rewrite old session entries" rule protects: ou
300
300
 
301
301
  Detailed work logs belong in the nearest active runtime. Add a parent/root summary only when sub-project work changes parent-visible topology, shared design, release behavior, policy inheritance, cross-project dependencies, risks, or active priorities. Do not mirror sub-project logs into root memory.
302
302
 
303
- Session entries must capture rationale when it matters, without forcing ceremony for small work. Use rationale for durable decisions, architecture changes, policy changes, bootstrap choices, release decisions, non-obvious tradeoffs, or changes likely to confuse a future agent.
303
+ Session entries must capture reason when it matters, without forcing ceremony for small work. Use reason for durable decisions, architecture changes, policy changes, bootstrap choices, release decisions, non-obvious tradeoffs, or changes likely to confuse a future agent.
304
304
 
305
305
  ## Consolidation Review Triggers
306
306
 
@@ -390,6 +390,27 @@ subproject_path: null
390
390
 
391
391
  Keep session filenames date-only, such as `.memory-seed/sessions/2026-05-02.md`. Use minute-level timestamps in entry headings, taken from the current system clock at write time. Entries are appended in clock order and never backdated or reordered (see Append-Only Chronology). Generate `entry_id` as a deterministic short hash from metadata only: timestamp, title, user initials, agent type, project path, and subproject path. Use known user initials when available; otherwise ask during bootstrap or use a neutral placeholder until confirmed. Capture meaningful decisions, durable changes, follow-up risk, or handoff context. Do not log every command.
392
392
 
393
+ ### Reason Rules
394
+
395
+ **DRAFT** is the compact decision-record format used inside session entries. Use it whenever a meaningful decision was made or implemented.
396
+
397
+ A DRAFT decision record uses compact labels:
398
+
399
+ - D = Decision — what was chosen
400
+ - R = Reason — the decisive reason, 1–3 bullets; **required**
401
+ - A = Alternatives considered or rejected, with reason (optional unless it shaped the tradeoff)
402
+ - F = Files, artifacts, or behaviors changed (optional)
403
+ - T = Tests or validation outcome (optional; may appear inline as `- T:` or as a separate `### Validation` section)
404
+
405
+ `D` and `R` are required for every meaningful decision. `A`, `F`, and `T` are optional when not relevant.
406
+
407
+ - Do not invent reason.
408
+ - If reason is inferred, label it `Inferred reason`.
409
+ - If reason is unknown, write `Reason not recorded`.
410
+ - Alternatives are optional unless they affected the decision or tradeoff.
411
+ - Use `D1`, `D2`, and similar labels only inside a multi-decision entry; `entry_id` is the global reference.
412
+ - Do not rewrite old logs solely to match the newest schema unless the user explicitly asks.
413
+
393
414
  ### Entry Shapes
394
415
 
395
416
  Use the lightest entry shape that preserves future usefulness.
@@ -463,22 +484,6 @@ Use one entry when several decisions belong to one coherent task, plan, or user
463
484
  - Residual risks or next actions.
464
485
  ```
465
486
 
466
- ### Rationale Rules
467
-
468
- - A DRAFT decision record uses compact labels:
469
- - D = Decision
470
- - R = Rationale
471
- - A = Alternatives considered or rejected, with reason
472
- - F = Files, artifacts, or behaviors changed
473
- - T = Tests or validation
474
- - In a DRAFT decision record, `D` and `R` are required for meaningful decisions; `A`, `F`, and `T` are optional when not relevant.
475
- - Do not invent rationale.
476
- - If rationale is inferred, label it `Inferred rationale`.
477
- - If rationale is unknown, write `Rationale not recorded`.
478
- - Alternatives are optional unless they affected the decision or tradeoff.
479
- - Use `D1`, `D2`, and similar labels only inside a multi-decision entry; `entry_id` is the global reference.
480
- - Do not rewrite old logs solely to match the newest schema unless the user explicitly asks.
481
-
482
487
  ## Archive Policy
483
488
 
484
489
  Archive prior control-plane snapshots under `.memory-seed/archive/<version>/` before replacing reusable versioned artifacts. Archive snapshots are historical records and may preserve old path names.
@@ -1,4 +1,5 @@
1
1
  import json
2
+ import shutil
2
3
  import sys
3
4
  from pathlib import Path
4
5
  from datetime import datetime, timedelta
@@ -27,14 +28,29 @@ try:
27
28
  except OSError:
28
29
  pass
29
30
 
30
- reminder = (
31
- "MEMORY RETRIEVAL REMINDER: Before substantive work, retrieve relevant "
32
- "prior context. Call the memory_search MCP tool, or if MCP is "
33
- "unavailable read the two most recent .memory-seed/sessions/*.md files. "
34
- "Do this before editing code or making decisions so you build on past "
35
- "work instead of repeating it."
31
+ _draft = (
32
+ "Record durable decisions using DRAFT labels: "
33
+ "D (Decision, required), R (Reason, required), "
34
+ "A (Alternatives, optional), F (Files, optional), T (Tests, optional)."
36
35
  )
37
36
 
37
+ if shutil.which("memory-seed-mcp") is not None:
38
+ reminder = (
39
+ "MEMORY RETRIEVAL REMINDER: Before substantive work, retrieve relevant "
40
+ "prior context. Call the memory_search MCP tool, or if MCP is "
41
+ "unavailable read the two most recent .memory-seed/sessions/*.md files. "
42
+ "Do this before editing code or making decisions so you build on past "
43
+ f"work instead of repeating it. {_draft}"
44
+ )
45
+ else:
46
+ reminder = (
47
+ "MEMORY RETRIEVAL REMINDER: memory-seed-mcp is not on PATH — the "
48
+ "memory_search tool is unavailable. To fix: run "
49
+ "`uv tool install memory-seed` (or `pip install memory-seed`), then "
50
+ "restart your editor. For now, read the two most recent "
51
+ f".memory-seed/sessions/*.md files before substantive work. {_draft}"
52
+ )
53
+
38
54
  if agent == "codex":
39
55
  # Codex CLI UserPromptSubmit: systemMessage shown in UI
40
56
  print(json.dumps({"systemMessage": reminder, "continue": True}))
@@ -27,7 +27,10 @@ if not recent:
27
27
  f"SESSION LOG REMINDER: No .memory-seed/sessions/ entry has been "
28
28
  f"updated in the last 15 minutes. If you completed meaningful work "
29
29
  f"this turn, append an entry to .memory-seed/sessions/{today}.md "
30
- f"now — before this turn ends."
30
+ f"now — before this turn ends. "
31
+ f"For decisions, use DRAFT labels: "
32
+ f"D (Decision, required), R (Reason, required), "
33
+ f"A (Alternatives, optional), F (Files, optional), T (Tests, optional)."
31
34
  )
32
35
 
33
36
  # Chronology check: today's entry headings must be in non-decreasing time order.
@@ -284,7 +284,7 @@ Include:
284
284
  - inheritance choices
285
285
  - follow-up gaps
286
286
 
287
- Record rationale for bootstrap choices that shape future behavior:
287
+ Record reason for bootstrap choices that shape future behavior:
288
288
 
289
289
  - project classification
290
290
  - policy and risk posture
@@ -292,7 +292,7 @@ Record rationale for bootstrap choices that shape future behavior:
292
292
  - active skill selection
293
293
  - major assumptions
294
294
 
295
- Do not require rationale for obvious file discoveries. Do not invent rationale; mark inferred rationale explicitly or write `Rationale not recorded` when unknown.
295
+ Do not require reason for obvious file discoveries. Do not invent reason; mark inferred reason explicitly or write `Reason not recorded` when unknown.
296
296
 
297
297
  Keep sessions append-only.
298
298
 
@@ -20,14 +20,14 @@ Use this skill when compacting session history, reviewing recent work, or promot
20
20
  6. Promote reusable procedures into `.memory-seed/skills/*.md`.
21
21
  7. Leave one-off debugging traces, temporary hypotheses, superseded experiments, and raw command output in sessions only.
22
22
 
23
- ## Rationale Boundary
23
+ ## Reason Boundary
24
24
 
25
- - sessions preserve rationale and tradeoffs.
25
+ - sessions preserve reason and tradeoffs.
26
26
  - index.md receives only durable current conclusions.
27
27
  - policy.md receives only durable behavioral constraints.
28
28
  - Preserve DRAFT decision records in sessions.
29
- - Do not copy full rationale into index.md unless a short rationale note is needed to prevent likely misuse.
30
- - Preserve alternatives, rejected paths, inferred rationale, and unknown-rationale markers in session history.
29
+ - Do not copy full reason into index.md unless a short reason note is needed to prevent likely misuse.
30
+ - Preserve alternatives, rejected paths, inferred reason, and unknown-reason markers in session history.
31
31
 
32
32
  ## Commands
33
33
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memory-seed
3
- Version: 2.1.2
3
+ Version: 2.2.0
4
4
  Summary: Portable local memory seed for file-reading AI coding agents
5
5
  Author: Jean Nathan Tshibuyi
6
6
  License: MIT
@@ -30,6 +30,12 @@ Memory Seed is a portable local memory system for AI coding agents. It plants a
30
30
 
31
31
  It is built first for solo developers who move between Codex, Claude Code, Gemini CLI, and other file-reading agents. Teams can also use it to standardize local agent memory across repositories without introducing a database or hosted memory service.
32
32
 
33
+ ## Demo
34
+
35
+ https://github.com/user-attachments/assets/b1c64d9e-4a67-4bc2-a030-d8ba7f17ccfe
36
+
37
+ [▶ Watch the 30-second demo](https://github.com/jnl-tshi/memory-seed/releases/download/v2.1.3/memory-seed-demo.mp4) if the player above does not load.
38
+
33
39
  ## Quickstart
34
40
 
35
41
  From the root of a project where you want local agent memory:
@@ -135,6 +141,24 @@ The result is a lightweight memory workflow you can understand, commit, review,
135
141
  | Other file-reading agents | Start from `AGENTS.md` and follow nearest `.memory-seed/` runtime discovery. |
136
142
  | MCP-capable clients | Use `memory_search` and `memory_get_chunk` through `memory-seed-mcp --stdio`. |
137
143
 
144
+ ## Agent Hooks
145
+
146
+ `memory-seed init` and `memory-seed update` install lifecycle hooks that keep memory current without relying on the agent to remember. Each hook is merged into the agent's own config file (existing settings are preserved), so it works regardless of which agent opens the project:
147
+
148
+ | Agent | Config file | Session-log reminder | Memory-retrieval reminder |
149
+ | --- | --- | --- | --- |
150
+ | Claude Code | `.claude/settings.json` | `Stop` | `UserPromptSubmit` |
151
+ | Codex CLI | `.codex/hooks.json` | `Stop` | `UserPromptSubmit` |
152
+ | Gemini CLI | `.gemini/settings.json` | `Stop` | `UserPromptSubmit` |
153
+ | Cursor | `.cursor/hooks.json` | `afterAgentResponse` | `sessionStart` |
154
+
155
+ Both reminders are cross-platform Python scripts in `.memory-seed/hooks/`:
156
+
157
+ - `session-log-check.py` — after a turn, reminds the agent to append a session-log entry if none was written in the last 15 minutes, and warns if the day's entries are out of ascending time order.
158
+ - `memory-retrieval-check.py` — before substantive work, reminds the agent to retrieve prior context (`memory_search` or the most recent session files). Gated by an 8-hour marker file so it fires about once per working session.
159
+
160
+ The hooks nudge; they never block. The scripts use Python 3.11+, which Memory Seed already requires.
161
+
138
162
  ## Reusable Seed Files
139
163
 
140
164
  ```text
@@ -144,6 +168,9 @@ GEMINI.md
144
168
  .memory-seed/
145
169
  agent-rules.md
146
170
  project-bootstrap.md
171
+ hooks/
172
+ session-log-check.py
173
+ memory-retrieval-check.py
147
174
  skills/
148
175
  index.md
149
176
  code_search.md
@@ -165,6 +192,7 @@ GEMINI.md
165
192
  project-bootstrap.md
166
193
  index.md
167
194
  policy.md
195
+ hooks/
168
196
  skills/
169
197
  sessions/
170
198
  archive/
@@ -337,7 +365,9 @@ For the version distinction (`pip show memory-seed` reports the package version;
337
365
 
338
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.
339
367
 
340
- Run it over stdio:
368
+ **Auto-registration:** `memory-seed init` and `memory-seed update` automatically register `memory-seed-mcp --stdio` in each supported vendor's config — `.claude/settings.json` (Claude Code), `.cursor/mcp.json` (Cursor), and `.gemini/settings.json` (Gemini CLI). No manual config is needed for projects initialised with Memory Seed.
369
+
370
+ If you are configuring the server manually, run it over stdio:
341
371
 
342
372
  ```powershell
343
373
  uvx --from memory-seed memory-seed-mcp --stdio
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "memory-seed"
7
- version = "2.1.2"
7
+ version = "2.2.0"
8
8
  description = "Portable local memory seed for file-reading AI coding agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -493,6 +493,63 @@ class HookMergeTests(unittest.TestCase):
493
493
  self.assertTrue(_merge_cursor_retrieval_hook(cwd))
494
494
  self.assertFalse(_merge_cursor_retrieval_hook(cwd))
495
495
 
496
+ def test_grouped_hook_updates_stale_command(self):
497
+ import json
498
+ from memory_seed.core import _merge_grouped_hook
499
+
500
+ cwd = self.make_project()
501
+ config = cwd / ".claude" / "settings.json"
502
+ config.parent.mkdir(parents=True, exist_ok=True)
503
+ config.write_text(
504
+ json.dumps({
505
+ "hooks": {
506
+ "UserPromptSubmit": [
507
+ {"hooks": [{"type": "command", "command": "python3 .memory-seed/hooks/memory-retrieval-check.py --old-flag"}]}
508
+ ]
509
+ }
510
+ }),
511
+ encoding="utf-8",
512
+ )
513
+
514
+ new_command = "python3 .memory-seed/hooks/memory-retrieval-check.py"
515
+ result = _merge_grouped_hook(config, "UserPromptSubmit", new_command, "memory-retrieval-check.py")
516
+ self.assertTrue(result)
517
+
518
+ data = json.loads(config.read_text())
519
+ commands = [
520
+ h["command"]
521
+ for g in data["hooks"]["UserPromptSubmit"]
522
+ for h in g.get("hooks", [])
523
+ ]
524
+ self.assertEqual(commands, [new_command]) # updated in place, no duplicate
525
+
526
+ def test_cursor_event_hook_updates_stale_command(self):
527
+ import json
528
+ from memory_seed.core import _merge_cursor_event_hook
529
+
530
+ cwd = self.make_project()
531
+ config = cwd / ".cursor" / "hooks.json"
532
+ config.parent.mkdir(parents=True, exist_ok=True)
533
+ config.write_text(
534
+ json.dumps({
535
+ "version": 1,
536
+ "hooks": {
537
+ "sessionStart": [
538
+ {"command": "python3 .memory-seed/hooks/memory-retrieval-check.py --cursor --old-flag"}
539
+ ]
540
+ }
541
+ }),
542
+ encoding="utf-8",
543
+ )
544
+
545
+ new_command = "python3 .memory-seed/hooks/memory-retrieval-check.py --cursor"
546
+ result = _merge_cursor_event_hook(config, "sessionStart", new_command, "memory-retrieval-check.py")
547
+ self.assertTrue(result)
548
+
549
+ data = json.loads(config.read_text())
550
+ commands = [e["command"] for e in data["hooks"]["sessionStart"]]
551
+ self.assertEqual(commands, [new_command]) # updated in place, no duplicate
552
+
496
553
 
497
554
  class SessionLogOrderingHookTests(unittest.TestCase):
498
555
  SCRIPT = Path("memory_seed/seed/.memory-seed/hooks/session-log-check.py").resolve()
@@ -537,6 +594,169 @@ class SessionLogOrderingHookTests(unittest.TestCase):
537
594
  self.assertNotIn("ORDER WARNING", self._run(cwd))
538
595
 
539
596
 
597
+ class McpMergeTests(unittest.TestCase):
598
+ def make_project(self):
599
+ path = Path(tempfile.mkdtemp(prefix="memory-seed-mcp-"))
600
+ self.addCleanup(lambda: shutil.rmtree(path, ignore_errors=True))
601
+ return path
602
+
603
+ def test_init_installs_mcp_for_claude(self):
604
+ import json
605
+
606
+ cwd = self.make_project()
607
+ init_project(cwd=cwd)
608
+
609
+ data = json.loads((cwd / ".claude" / "settings.json").read_text())
610
+ self.assertIn("memory-seed", data["mcpServers"])
611
+ entry = data["mcpServers"]["memory-seed"]
612
+ self.assertEqual(entry["command"], "memory-seed-mcp")
613
+ self.assertEqual(entry["args"], ["--stdio"])
614
+ self.assertEqual(entry["type"], "stdio")
615
+
616
+ def test_init_installs_mcp_for_cursor(self):
617
+ import json
618
+
619
+ cwd = self.make_project()
620
+ init_project(cwd=cwd)
621
+
622
+ data = json.loads((cwd / ".cursor" / "mcp.json").read_text())
623
+ self.assertIn("memory-seed", data["mcpServers"])
624
+ entry = data["mcpServers"]["memory-seed"]
625
+ self.assertEqual(entry["command"], "memory-seed-mcp")
626
+ self.assertEqual(entry["args"], ["--stdio"])
627
+ self.assertNotIn("type", entry)
628
+
629
+ def test_init_installs_mcp_for_gemini(self):
630
+ import json
631
+
632
+ cwd = self.make_project()
633
+ init_project(cwd=cwd)
634
+
635
+ data = json.loads((cwd / ".gemini" / "settings.json").read_text())
636
+ self.assertIn("memory-seed", data["mcpServers"])
637
+ entry = data["mcpServers"]["memory-seed"]
638
+ self.assertEqual(entry["command"], "memory-seed-mcp")
639
+ self.assertEqual(entry["args"], ["--stdio"])
640
+
641
+ def test_mcp_merges_are_idempotent(self):
642
+ from memory_seed.core import (
643
+ _merge_claude_mcp,
644
+ _merge_cursor_mcp,
645
+ _merge_gemini_mcp,
646
+ )
647
+
648
+ cwd = self.make_project()
649
+ self.assertTrue(_merge_claude_mcp(cwd))
650
+ self.assertFalse(_merge_claude_mcp(cwd))
651
+ self.assertTrue(_merge_cursor_mcp(cwd))
652
+ self.assertFalse(_merge_cursor_mcp(cwd))
653
+ self.assertTrue(_merge_gemini_mcp(cwd))
654
+ self.assertFalse(_merge_gemini_mcp(cwd))
655
+
656
+ def test_mcp_merge_updates_stale_args(self):
657
+ import json
658
+
659
+ cwd = self.make_project()
660
+ settings = cwd / ".claude" / "settings.json"
661
+ settings.parent.mkdir(parents=True, exist_ok=True)
662
+ settings.write_text(
663
+ json.dumps({"mcpServers": {"memory-seed": {"command": "memory-seed-mcp", "args": ["--old"], "type": "stdio"}}}),
664
+ encoding="utf-8",
665
+ )
666
+
667
+ from memory_seed.core import _merge_claude_mcp
668
+ result = _merge_claude_mcp(cwd)
669
+ self.assertTrue(result)
670
+
671
+ data = json.loads(settings.read_text())
672
+ self.assertEqual(data["mcpServers"]["memory-seed"]["args"], ["--stdio"])
673
+
674
+ def test_mcp_merge_preserves_unrelated_mcp_server(self):
675
+ import json
676
+
677
+ cwd = self.make_project()
678
+ settings = cwd / ".claude" / "settings.json"
679
+ settings.parent.mkdir(parents=True, exist_ok=True)
680
+ settings.write_text(
681
+ json.dumps({"mcpServers": {"other-server": {"command": "other-cmd", "args": []}}}),
682
+ encoding="utf-8",
683
+ )
684
+
685
+ from memory_seed.core import _merge_claude_mcp
686
+ _merge_claude_mcp(cwd)
687
+
688
+ data = json.loads(settings.read_text())
689
+ self.assertIn("other-server", data["mcpServers"])
690
+ self.assertEqual(data["mcpServers"]["other-server"]["command"], "other-cmd")
691
+
692
+ def test_gemini_mcp_merge_preserves_existing_hooks(self):
693
+ import json
694
+
695
+ cwd = self.make_project()
696
+ gemini_path = cwd / ".gemini" / "settings.json"
697
+ gemini_path.parent.mkdir(parents=True, exist_ok=True)
698
+ gemini_path.write_text(
699
+ json.dumps({"hooks": {"Stop": [{"hooks": [{"type": "command", "command": "existing"}]}]}}),
700
+ encoding="utf-8",
701
+ )
702
+
703
+ from memory_seed.core import _merge_gemini_mcp
704
+ _merge_gemini_mcp(cwd)
705
+
706
+ data = json.loads(gemini_path.read_text())
707
+ self.assertIn("memory-seed", data["mcpServers"])
708
+ self.assertIn("Stop", data["hooks"])
709
+ self.assertEqual(data["hooks"]["Stop"][0]["hooks"][0]["command"], "existing")
710
+
711
+
712
+ class RetrievalCheckPathTests(unittest.TestCase):
713
+ SCRIPT = Path("memory_seed/seed/.memory-seed/hooks/memory-retrieval-check.py").resolve()
714
+
715
+ def make_project(self):
716
+ path = Path(tempfile.mkdtemp(prefix="memory-seed-retrieval-"))
717
+ self.addCleanup(lambda: shutil.rmtree(path, ignore_errors=True))
718
+ (path / ".memory-seed").mkdir()
719
+ return path
720
+
721
+ def _run(self, cwd, extra_env=None):
722
+ import subprocess
723
+ import sys
724
+ import os
725
+
726
+ env = os.environ.copy()
727
+ if extra_env:
728
+ env.update(extra_env)
729
+ return subprocess.run(
730
+ [sys.executable, str(self.SCRIPT)],
731
+ cwd=cwd,
732
+ capture_output=True,
733
+ text=True,
734
+ env=env,
735
+ ).stdout
736
+
737
+ def test_mcp_found_message_mentions_memory_search(self):
738
+ import os
739
+ import stat
740
+
741
+ cwd = self.make_project()
742
+ # Create a dummy memory-seed-mcp binary on PATH
743
+ bin_dir = cwd / "bin"
744
+ bin_dir.mkdir()
745
+ fake_bin = bin_dir / "memory-seed-mcp"
746
+ fake_bin.write_text("#!/usr/bin/env python3\n")
747
+ fake_bin.chmod(fake_bin.stat().st_mode | stat.S_IEXEC)
748
+
749
+ out = self._run(cwd, extra_env={"PATH": str(bin_dir) + os.pathsep + os.environ.get("PATH", "")})
750
+ self.assertIn("memory_search", out)
751
+ self.assertNotIn("uv tool install", out)
752
+
753
+ def test_mcp_missing_message_mentions_install(self):
754
+ cwd = self.make_project()
755
+ out = self._run(cwd, extra_env={"PATH": ""})
756
+ self.assertIn("uv tool install", out)
757
+ self.assertNotIn("memory_search MCP tool", out)
758
+
759
+
540
760
  class CliHelpTests(unittest.TestCase):
541
761
  def _run(self, argv):
542
762
  import contextlib
@@ -13,13 +13,13 @@ class SessionSchemaTests(unittest.TestCase):
13
13
  "Multi-decision session entry",
14
14
  "DRAFT decision record",
15
15
  "D = Decision",
16
- "R = Rationale",
16
+ "R = Reason",
17
17
  "A = Alternatives considered or rejected",
18
18
  "F = Files, artifacts, or behaviors changed",
19
19
  "T = Tests or validation",
20
- "Do not invent rationale",
21
- "Inferred rationale",
22
- "Rationale not recorded",
20
+ "Do not invent reason",
21
+ "Inferred reason",
22
+ "Reason not recorded",
23
23
  "Alternatives are optional",
24
24
  ):
25
25
  self.assertIn(phrase, content)
@@ -49,7 +49,7 @@ class SessionSchemaTests(unittest.TestCase):
49
49
  "clear supersession criteria",
50
50
  "newer dated session entry or current authority file",
51
51
  "no later reversal or unresolved disagreement is found",
52
- "Session history is evidence and rationale",
52
+ "Session history is evidence and reason",
53
53
  "ask the user before changing durable design",
54
54
  ):
55
55
  self.assertIn(phrase, content)
@@ -187,7 +187,7 @@ class SessionSchemaTests(unittest.TestCase):
187
187
  "active skill selection",
188
188
  "major assumptions",
189
189
  "DRAFT decision records",
190
- "Do not require rationale for obvious file discoveries",
190
+ "Do not require reason for obvious file discoveries",
191
191
  ):
192
192
  self.assertIn(phrase, content)
193
193
 
@@ -208,11 +208,11 @@ class SessionSchemaTests(unittest.TestCase):
208
208
  content = Path(".memory-seed/skills/memory_consolidation.md").read_text(encoding="utf-8")
209
209
 
210
210
  for phrase in (
211
- "sessions preserve rationale and tradeoffs",
211
+ "sessions preserve reason and tradeoffs",
212
212
  "index.md receives only durable current conclusions",
213
213
  "policy.md receives only durable behavioral constraints",
214
214
  "Preserve DRAFT decision records",
215
- "Do not copy full rationale into index.md",
215
+ "Do not copy full reason into index.md",
216
216
  ):
217
217
  self.assertIn(phrase, content)
218
218
 
File without changes
File without changes