memory-seed 2.1.3__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.3 → memory_seed-2.2.0}/PKG-INFO +10 -2
  2. {memory_seed-2.1.3 → memory_seed-2.2.0}/README.md +9 -1
  3. {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/core.py +152 -111
  4. {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/agent-rules.md +21 -16
  5. {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/hooks/memory-retrieval-check.py +22 -6
  6. {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/hooks/session-log-check.py +4 -1
  7. {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed.egg-info/PKG-INFO +10 -2
  8. {memory_seed-2.1.3 → memory_seed-2.2.0}/pyproject.toml +1 -1
  9. {memory_seed-2.1.3 → memory_seed-2.2.0}/tests/test_memory_seed.py +220 -0
  10. {memory_seed-2.1.3 → memory_seed-2.2.0}/LICENSE +0 -0
  11. {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/__init__.py +0 -0
  12. {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/cli.py +0 -0
  13. {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/mcp_server.py +0 -0
  14. {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/mcp_validate.py +0 -0
  15. {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/archive/.gitkeep +0 -0
  16. {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/project-bootstrap.md +0 -0
  17. {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/sessions/.gitkeep +0 -0
  18. {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/code_search.md +0 -0
  19. {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/data_architecture.md +0 -0
  20. {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/index.md +0 -0
  21. {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/local_compilation.md +0 -0
  22. {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/memory_consolidation.md +0 -0
  23. {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/memory_doctor.md +0 -0
  24. {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/release_publishing.md +0 -0
  25. {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/security_triage.md +0 -0
  26. {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/AGENTS.md +0 -0
  27. {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/CLAUDE.md +0 -0
  28. {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/GEMINI.md +0 -0
  29. {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/semantic_cache.py +0 -0
  30. {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed.egg-info/SOURCES.txt +0 -0
  31. {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed.egg-info/dependency_links.txt +0 -0
  32. {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed.egg-info/entry_points.txt +0 -0
  33. {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed.egg-info/requires.txt +0 -0
  34. {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed.egg-info/top_level.txt +0 -0
  35. {memory_seed-2.1.3 → memory_seed-2.2.0}/setup.cfg +0 -0
  36. {memory_seed-2.1.3 → memory_seed-2.2.0}/tests/test_mcp_server.py +0 -0
  37. {memory_seed-2.1.3 → memory_seed-2.2.0}/tests/test_mcp_validation.py +0 -0
  38. {memory_seed-2.1.3 → memory_seed-2.2.0}/tests/test_semantic_cache.py +0 -0
  39. {memory_seed-2.1.3 → memory_seed-2.2.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.1.3
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:
@@ -359,7 +365,9 @@ For the version distinction (`pip show memory-seed` reports the package version;
359
365
 
360
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.
361
367
 
362
- 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:
363
371
 
364
372
  ```powershell
365
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:
@@ -338,7 +344,9 @@ For the version distinction (`pip show memory-seed` reports the package version;
338
344
 
339
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.
340
346
 
341
- 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:
342
350
 
343
351
  ```powershell
344
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:
@@ -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
- ### Reason Rules
467
-
468
- - A DRAFT decision record uses compact labels:
469
- - D = Decision
470
- - R = Reason
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 reason.
476
- - If reason is inferred, label it `Inferred reason`.
477
- - If reason is unknown, write `Reason 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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memory-seed
3
- Version: 2.1.3
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:
@@ -359,7 +365,9 @@ For the version distinction (`pip show memory-seed` reports the package version;
359
365
 
360
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.
361
367
 
362
- 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:
363
371
 
364
372
  ```powershell
365
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.3"
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
File without changes
File without changes