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.
- {memory_seed-2.1.2 → memory_seed-2.2.0}/PKG-INFO +32 -2
- {memory_seed-2.1.2 → memory_seed-2.2.0}/README.md +31 -1
- {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/core.py +152 -111
- {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/agent-rules.md +24 -19
- {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/hooks/memory-retrieval-check.py +22 -6
- {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/hooks/session-log-check.py +4 -1
- {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/project-bootstrap.md +2 -2
- {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/memory_consolidation.md +4 -4
- {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed.egg-info/PKG-INFO +32 -2
- {memory_seed-2.1.2 → memory_seed-2.2.0}/pyproject.toml +1 -1
- {memory_seed-2.1.2 → memory_seed-2.2.0}/tests/test_memory_seed.py +220 -0
- {memory_seed-2.1.2 → memory_seed-2.2.0}/tests/test_session_schema.py +8 -8
- {memory_seed-2.1.2 → memory_seed-2.2.0}/LICENSE +0 -0
- {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/__init__.py +0 -0
- {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/cli.py +0 -0
- {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/mcp_server.py +0 -0
- {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/mcp_validate.py +0 -0
- {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/archive/.gitkeep +0 -0
- {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/sessions/.gitkeep +0 -0
- {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/code_search.md +0 -0
- {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/data_architecture.md +0 -0
- {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/index.md +0 -0
- {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/local_compilation.md +0 -0
- {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/memory_doctor.md +0 -0
- {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/release_publishing.md +0 -0
- {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/security_triage.md +0 -0
- {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/AGENTS.md +0 -0
- {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/CLAUDE.md +0 -0
- {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/GEMINI.md +0 -0
- {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/semantic_cache.py +0 -0
- {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed.egg-info/SOURCES.txt +0 -0
- {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed.egg-info/dependency_links.txt +0 -0
- {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed.egg-info/entry_points.txt +0 -0
- {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed.egg-info/requires.txt +0 -0
- {memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed.egg-info/top_level.txt +0 -0
- {memory_seed-2.1.2 → memory_seed-2.2.0}/setup.cfg +0 -0
- {memory_seed-2.1.2 → memory_seed-2.2.0}/tests/test_mcp_server.py +0 -0
- {memory_seed-2.1.2 → memory_seed-2.2.0}/tests/test_mcp_validation.py +0 -0
- {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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"""
|
|
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:
|
|
@@ -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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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.2 → 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.
|
|
@@ -284,7 +284,7 @@ Include:
|
|
|
284
284
|
- inheritance choices
|
|
285
285
|
- follow-up gaps
|
|
286
286
|
|
|
287
|
-
Record
|
|
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
|
|
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
|
|
{memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/memory_consolidation.md
RENAMED
|
@@ -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
|
-
##
|
|
23
|
+
## Reason Boundary
|
|
24
24
|
|
|
25
|
-
- sessions preserve
|
|
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
|
|
30
|
-
- Preserve alternatives, rejected paths, inferred
|
|
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.
|
|
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
|
-
|
|
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
|
|
@@ -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 =
|
|
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
|
|
21
|
-
"Inferred
|
|
22
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{memory_seed-2.1.2 → 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.2 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/local_compilation.md
RENAMED
|
File without changes
|
{memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/memory_doctor.md
RENAMED
|
File without changes
|
{memory_seed-2.1.2 → memory_seed-2.2.0}/memory_seed/seed/.memory-seed/skills/release_publishing.md
RENAMED
|
File without changes
|
{memory_seed-2.1.2 → 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
|