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.
- {memory_seed-2.1.3 → memory_seed-2.2.0}/PKG-INFO +10 -2
- {memory_seed-2.1.3 → memory_seed-2.2.0}/README.md +9 -1
- {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/core.py +152 -111
- {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/agent-rules.md +21 -16
- {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/hooks/memory-retrieval-check.py +22 -6
- {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/hooks/session-log-check.py +4 -1
- {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed.egg-info/PKG-INFO +10 -2
- {memory_seed-2.1.3 → memory_seed-2.2.0}/pyproject.toml +1 -1
- {memory_seed-2.1.3 → memory_seed-2.2.0}/tests/test_memory_seed.py +220 -0
- {memory_seed-2.1.3 → memory_seed-2.2.0}/LICENSE +0 -0
- {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/__init__.py +0 -0
- {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/cli.py +0 -0
- {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/mcp_server.py +0 -0
- {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/mcp_validate.py +0 -0
- {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/archive/.gitkeep +0 -0
- {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/project-bootstrap.md +0 -0
- {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/sessions/.gitkeep +0 -0
- {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/code_search.md +0 -0
- {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/data_architecture.md +0 -0
- {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/index.md +0 -0
- {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/local_compilation.md +0 -0
- {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/memory_consolidation.md +0 -0
- {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/memory_doctor.md +0 -0
- {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/release_publishing.md +0 -0
- {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/security_triage.md +0 -0
- {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/AGENTS.md +0 -0
- {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/CLAUDE.md +0 -0
- {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/GEMINI.md +0 -0
- {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/semantic_cache.py +0 -0
- {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed.egg-info/SOURCES.txt +0 -0
- {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed.egg-info/dependency_links.txt +0 -0
- {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed.egg-info/entry_points.txt +0 -0
- {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed.egg-info/requires.txt +0 -0
- {memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed.egg-info/top_level.txt +0 -0
- {memory_seed-2.1.3 → memory_seed-2.2.0}/setup.cfg +0 -0
- {memory_seed-2.1.3 → memory_seed-2.2.0}/tests/test_mcp_server.py +0 -0
- {memory_seed-2.1.3 → memory_seed-2.2.0}/tests/test_mcp_validation.py +0 -0
- {memory_seed-2.1.3 → memory_seed-2.2.0}/tests/test_semantic_cache.py +0 -0
- {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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"""
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
"""
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
"""
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
"""
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
"""
|
|
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.
|
|
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
|
-
"""
|
|
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
|
-
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
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}))
|
{memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/hooks/session-log-check.py
RENAMED
|
@@ -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.
|
|
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
|
-
|
|
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
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/data_architecture.md
RENAMED
|
File without changes
|
|
File without changes
|
{memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/local_compilation.md
RENAMED
|
File without changes
|
{memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/memory_consolidation.md
RENAMED
|
File without changes
|
{memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/memory_doctor.md
RENAMED
|
File without changes
|
{memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/release_publishing.md
RENAMED
|
File without changes
|
{memory_seed-2.1.3 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/security_triage.md
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
|
|
File without changes
|
|
File without changes
|