memgit 0.1.1__py3-none-any.whl
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.
- memgit/__init__.py +3 -0
- memgit/cli.py +1267 -0
- memgit/graph.py +486 -0
- memgit/http_server.py +231 -0
- memgit/importer.py +121 -0
- memgit/mcp_server.py +418 -0
- memgit/models.py +80 -0
- memgit/repo.py +714 -0
- memgit/scorer.py +123 -0
- memgit/store.py +176 -0
- memgit/tokens.py +48 -0
- memgit/toon.py +356 -0
- memgit-0.1.1.dist-info/METADATA +457 -0
- memgit-0.1.1.dist-info/RECORD +18 -0
- memgit-0.1.1.dist-info/WHEEL +5 -0
- memgit-0.1.1.dist-info/entry_points.txt +2 -0
- memgit-0.1.1.dist-info/licenses/LICENSE +21 -0
- memgit-0.1.1.dist-info/top_level.txt +1 -0
memgit/mcp_server.py
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
"""memgit MCP server — exposes memory search/get/list/save over stdio MCP protocol."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import sys
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import mcp.server.stdio
|
|
10
|
+
from mcp.server import Server
|
|
11
|
+
from mcp.types import (
|
|
12
|
+
TextContent,
|
|
13
|
+
Tool,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from .models import Mnemonic
|
|
17
|
+
from .repo import Repository
|
|
18
|
+
from .scorer import score as bm25_score
|
|
19
|
+
from .toon import serialize_mnemonic
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_SERVER_DESCRIPTION = (
|
|
23
|
+
"memgit is a version-controlled memory store — git for AI memory. "
|
|
24
|
+
"It stores typed, prioritized facts, rules, preferences, and lessons learned, "
|
|
25
|
+
"then serves only the most relevant ones per query via BM25 scoring. "
|
|
26
|
+
"CRITICAL INSTRUCTIONS: "
|
|
27
|
+
"(1) Call search_memories at the START of every session before answering the first question — "
|
|
28
|
+
"this loads your persistent context and prevents repeating questions the user has already answered. "
|
|
29
|
+
"(2) Call save_memory whenever you learn something durable — a rule the user corrected you on, "
|
|
30
|
+
"a preference they stated, a project decision, a lesson from a mistake. "
|
|
31
|
+
"Do NOT wait for the user to ask you to remember — save proactively. "
|
|
32
|
+
"(3) memgit is cross-tool: memories saved here are also available in Cursor, Windsurf, GPT, and Gemini."
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
_TYPE_DESCRIPTIONS = (
|
|
36
|
+
"fb=feedback (corrections, preferences, how the user likes to work), "
|
|
37
|
+
"us=user (who the user is, their role, expertise, goals), "
|
|
38
|
+
"pj=project (active projects, goals, decisions, deadlines), "
|
|
39
|
+
"rf=reference (pointers to external systems, URLs, tools), "
|
|
40
|
+
"cn=convention (code style, naming, architecture rules), "
|
|
41
|
+
"lx=lesson (lessons learned, post-mortems, 'we got burned by X')"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _default_store() -> Path:
|
|
46
|
+
return Path.home() / ".claude" / "memgit-store"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _load_repo(store_path: Path | None) -> Repository | None:
|
|
50
|
+
path = store_path or _default_store()
|
|
51
|
+
memgit_dir = path / ".memgit"
|
|
52
|
+
if not memgit_dir.is_dir():
|
|
53
|
+
return None
|
|
54
|
+
return Repository(memgit_dir)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _mnem_to_dict(m: Mnemonic, score: float | None = None) -> dict[str, Any]:
|
|
58
|
+
d: dict[str, Any] = {
|
|
59
|
+
"slug": m.slug,
|
|
60
|
+
"type": m.type_code,
|
|
61
|
+
"priority": m.priority,
|
|
62
|
+
"rule": m.rule,
|
|
63
|
+
}
|
|
64
|
+
if m.why:
|
|
65
|
+
d["why"] = m.why
|
|
66
|
+
if m.when:
|
|
67
|
+
d["when"] = m.when
|
|
68
|
+
if m.tags:
|
|
69
|
+
d["tags"] = m.tags
|
|
70
|
+
if m.desc:
|
|
71
|
+
d["desc"] = m.desc
|
|
72
|
+
if score is not None:
|
|
73
|
+
d["score"] = round(score, 4)
|
|
74
|
+
return d
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def run_server(store_path: Path | None = None) -> None:
|
|
78
|
+
"""Run the MCP server on stdio."""
|
|
79
|
+
server = Server(
|
|
80
|
+
"memgit",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
@server.list_tools()
|
|
84
|
+
async def list_tools() -> list[Tool]:
|
|
85
|
+
return [
|
|
86
|
+
Tool(
|
|
87
|
+
name="search_memories",
|
|
88
|
+
description=(
|
|
89
|
+
"Search your persistent memory store for facts, rules, and lessons relevant to the current task. "
|
|
90
|
+
"CALL THIS: at the start of every session, before answering questions about past work, "
|
|
91
|
+
"before applying preferences the user may have expressed before, or whenever you are unsure "
|
|
92
|
+
"whether you have prior context on a topic. "
|
|
93
|
+
"Returns memories ranked by relevance — only what matters, not everything. "
|
|
94
|
+
"This is faster and more relevant than reading individual memory files."
|
|
95
|
+
),
|
|
96
|
+
inputSchema={
|
|
97
|
+
"type": "object",
|
|
98
|
+
"properties": {
|
|
99
|
+
"query": {
|
|
100
|
+
"type": "string",
|
|
101
|
+
"description": (
|
|
102
|
+
"Natural-language description of what you want to recall. "
|
|
103
|
+
"Examples: 'user preferences for code style', "
|
|
104
|
+
"'how does the Instagram pipeline work', "
|
|
105
|
+
"'what trading rules should I follow'"
|
|
106
|
+
),
|
|
107
|
+
},
|
|
108
|
+
"top_k": {
|
|
109
|
+
"type": "integer",
|
|
110
|
+
"description": "Max results to return (default 8, max 30). Use 15-20 for broad topic discovery.",
|
|
111
|
+
"default": 8,
|
|
112
|
+
},
|
|
113
|
+
"type_filter": {
|
|
114
|
+
"type": "string",
|
|
115
|
+
"enum": ["fb", "us", "pj", "rf", "cn", "lx"],
|
|
116
|
+
"description": _TYPE_DESCRIPTIONS,
|
|
117
|
+
},
|
|
118
|
+
"format": {
|
|
119
|
+
"type": "string",
|
|
120
|
+
"enum": ["json", "toon"],
|
|
121
|
+
"description": (
|
|
122
|
+
"Output format. 'json' (default) is universal and works with all LLMs. "
|
|
123
|
+
"'toon' is a token-efficient sigil format — use only if you know the TOON spec."
|
|
124
|
+
),
|
|
125
|
+
"default": "json",
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
"required": ["query"],
|
|
129
|
+
},
|
|
130
|
+
),
|
|
131
|
+
Tool(
|
|
132
|
+
name="get_memory",
|
|
133
|
+
description=(
|
|
134
|
+
"Fetch a single memory by its exact slug identifier. "
|
|
135
|
+
"CALL THIS: when search_memories returned a relevant slug and you need "
|
|
136
|
+
"the full details (why, when to apply, tags). "
|
|
137
|
+
"Slugs are kebab-case identifiers like 'ig-pipeline-no-fallback' or 'trading-capital-track'."
|
|
138
|
+
),
|
|
139
|
+
inputSchema={
|
|
140
|
+
"type": "object",
|
|
141
|
+
"properties": {
|
|
142
|
+
"slug": {
|
|
143
|
+
"type": "string",
|
|
144
|
+
"description": "The kebab-case memory slug (e.g. 'ig-pipeline-no-fallback')",
|
|
145
|
+
},
|
|
146
|
+
"format": {
|
|
147
|
+
"type": "string",
|
|
148
|
+
"enum": ["json", "toon"],
|
|
149
|
+
"default": "json",
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
"required": ["slug"],
|
|
153
|
+
},
|
|
154
|
+
),
|
|
155
|
+
Tool(
|
|
156
|
+
name="list_memories",
|
|
157
|
+
description=(
|
|
158
|
+
"List all memories, returning slug + rule for each. "
|
|
159
|
+
"CALL THIS: to browse what's stored, discover memory slugs for get_memory, "
|
|
160
|
+
"or audit the full memory set. For finding relevant memories on a topic, "
|
|
161
|
+
"use search_memories instead — it is faster and ranked."
|
|
162
|
+
),
|
|
163
|
+
inputSchema={
|
|
164
|
+
"type": "object",
|
|
165
|
+
"properties": {
|
|
166
|
+
"type_filter": {
|
|
167
|
+
"type": "string",
|
|
168
|
+
"enum": ["fb", "us", "pj", "rf", "cn", "lx"],
|
|
169
|
+
"description": _TYPE_DESCRIPTIONS,
|
|
170
|
+
},
|
|
171
|
+
"min_priority": {
|
|
172
|
+
"type": "integer",
|
|
173
|
+
"description": (
|
|
174
|
+
"Only return memories at or above this priority. "
|
|
175
|
+
"1=low (all memories), 2=medium+, 3=critical only. Default 1."
|
|
176
|
+
),
|
|
177
|
+
"default": 1,
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
),
|
|
182
|
+
Tool(
|
|
183
|
+
name="save_memory",
|
|
184
|
+
description=(
|
|
185
|
+
"Persist a new fact, rule, preference, or lesson to the memory store for future sessions. "
|
|
186
|
+
"CALL THIS: whenever you learn something the user would want you to remember next time — "
|
|
187
|
+
"a preference they stated, a rule they corrected you on, a project decision, "
|
|
188
|
+
"a lesson from a mistake, or a reference to an external system. "
|
|
189
|
+
"Do NOT save ephemeral or task-specific details; save durable facts only. "
|
|
190
|
+
"If a memory with the same slug already exists, this updates it."
|
|
191
|
+
),
|
|
192
|
+
inputSchema={
|
|
193
|
+
"type": "object",
|
|
194
|
+
"properties": {
|
|
195
|
+
"slug": {
|
|
196
|
+
"type": "string",
|
|
197
|
+
"description": (
|
|
198
|
+
"Short kebab-case identifier, unique per memory. "
|
|
199
|
+
"Examples: 'user-prefers-terse-responses', 'ig-pipeline-no-fallback', "
|
|
200
|
+
"'trading-confirm-only-orders'. Use existing slug to update."
|
|
201
|
+
),
|
|
202
|
+
},
|
|
203
|
+
"rule": {
|
|
204
|
+
"type": "string",
|
|
205
|
+
"description": (
|
|
206
|
+
"The primary fact or rule to remember (max ~200 chars). "
|
|
207
|
+
"Write as a declarative statement: 'always X', 'never Y', 'Z is located at ...'"
|
|
208
|
+
),
|
|
209
|
+
},
|
|
210
|
+
"type_code": {
|
|
211
|
+
"type": "string",
|
|
212
|
+
"enum": ["fb", "us", "pj", "rf", "cn", "lx"],
|
|
213
|
+
"description": _TYPE_DESCRIPTIONS,
|
|
214
|
+
"default": "fb",
|
|
215
|
+
},
|
|
216
|
+
"why": {
|
|
217
|
+
"type": "string",
|
|
218
|
+
"description": "Why this rule exists — the incident, reason, or motivation. Optional but strongly recommended.",
|
|
219
|
+
},
|
|
220
|
+
"when": {
|
|
221
|
+
"type": "string",
|
|
222
|
+
"description": "When / where to apply this rule. Optional.",
|
|
223
|
+
},
|
|
224
|
+
"tags": {
|
|
225
|
+
"type": "array",
|
|
226
|
+
"items": {"type": "string"},
|
|
227
|
+
"description": "Topic tags for filtering. Examples: ['instagram', 'trading', 'admin']",
|
|
228
|
+
},
|
|
229
|
+
"priority": {
|
|
230
|
+
"type": "integer",
|
|
231
|
+
"enum": [1, 2, 3],
|
|
232
|
+
"description": (
|
|
233
|
+
"1=low (background context), "
|
|
234
|
+
"2=medium (default — apply when relevant), "
|
|
235
|
+
"3=critical (always loaded, applied in every session)"
|
|
236
|
+
),
|
|
237
|
+
"default": 2,
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
"required": ["slug", "rule"],
|
|
241
|
+
},
|
|
242
|
+
),
|
|
243
|
+
Tool(
|
|
244
|
+
name="get_checkpoint_log",
|
|
245
|
+
description=(
|
|
246
|
+
"Show recent checkpoint history for the memory store — when memories were last synced "
|
|
247
|
+
"and what changed. CALL THIS: to understand how fresh the memory store is, or to "
|
|
248
|
+
"debug sync issues."
|
|
249
|
+
),
|
|
250
|
+
inputSchema={
|
|
251
|
+
"type": "object",
|
|
252
|
+
"properties": {
|
|
253
|
+
"limit": {
|
|
254
|
+
"type": "integer",
|
|
255
|
+
"description": "Max checkpoints to return (default 5)",
|
|
256
|
+
"default": 5,
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
),
|
|
261
|
+
]
|
|
262
|
+
|
|
263
|
+
@server.call_tool()
|
|
264
|
+
async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
|
|
265
|
+
import json
|
|
266
|
+
|
|
267
|
+
repo = _load_repo(store_path)
|
|
268
|
+
if repo is None:
|
|
269
|
+
return [TextContent(
|
|
270
|
+
type="text",
|
|
271
|
+
text="Error: memgit store not found. Run `memgit init` in ~/.claude/memgit-store/",
|
|
272
|
+
)]
|
|
273
|
+
|
|
274
|
+
if name == "search_memories":
|
|
275
|
+
query = arguments.get("query", "")
|
|
276
|
+
top_k = min(int(arguments.get("top_k", 8)), 30)
|
|
277
|
+
type_filter = arguments.get("type_filter")
|
|
278
|
+
fmt = arguments.get("format", "json")
|
|
279
|
+
|
|
280
|
+
mnemonics = repo.list()
|
|
281
|
+
if type_filter:
|
|
282
|
+
mnemonics = [m for m in mnemonics if m.type_code == type_filter]
|
|
283
|
+
|
|
284
|
+
results = bm25_score(query, mnemonics, top_k=top_k)
|
|
285
|
+
|
|
286
|
+
if not results:
|
|
287
|
+
return [TextContent(type="text", text="No results found.")]
|
|
288
|
+
|
|
289
|
+
if fmt == "toon":
|
|
290
|
+
lines = [f"# search: {query!r} ({len(results)} results)\n"]
|
|
291
|
+
for r in results:
|
|
292
|
+
lines.append(f"# score={r.score:.2f} matched={r.matched_fields}")
|
|
293
|
+
lines.append(serialize_mnemonic(r.mnemonic))
|
|
294
|
+
lines.append("")
|
|
295
|
+
text = "\n".join(lines)
|
|
296
|
+
else:
|
|
297
|
+
out = [_mnem_to_dict(r.mnemonic, r.score) for r in results]
|
|
298
|
+
text = json.dumps(out, indent=2)
|
|
299
|
+
|
|
300
|
+
return [TextContent(type="text", text=text)]
|
|
301
|
+
|
|
302
|
+
elif name == "get_memory":
|
|
303
|
+
slug = arguments.get("slug", "")
|
|
304
|
+
fmt = arguments.get("format", "json")
|
|
305
|
+
m = repo.get(slug)
|
|
306
|
+
if m is None:
|
|
307
|
+
return [TextContent(type="text", text=f"No memory found: {slug}")]
|
|
308
|
+
|
|
309
|
+
if fmt == "toon":
|
|
310
|
+
text = serialize_mnemonic(m)
|
|
311
|
+
else:
|
|
312
|
+
text = json.dumps(_mnem_to_dict(m), indent=2)
|
|
313
|
+
|
|
314
|
+
return [TextContent(type="text", text=text)]
|
|
315
|
+
|
|
316
|
+
elif name == "list_memories":
|
|
317
|
+
type_filter = arguments.get("type_filter")
|
|
318
|
+
min_priority = int(arguments.get("min_priority", 1))
|
|
319
|
+
|
|
320
|
+
mnemonics = repo.list()
|
|
321
|
+
if type_filter:
|
|
322
|
+
mnemonics = [m for m in mnemonics if m.type_code == type_filter]
|
|
323
|
+
if min_priority > 1:
|
|
324
|
+
mnemonics = [m for m in mnemonics if m.priority >= min_priority]
|
|
325
|
+
mnemonics.sort(key=lambda m: (m.type_code, m.slug))
|
|
326
|
+
|
|
327
|
+
if not mnemonics:
|
|
328
|
+
return [TextContent(type="text", text="No memories found.")]
|
|
329
|
+
|
|
330
|
+
lines = [f"# {len(mnemonics)} memories"]
|
|
331
|
+
for m in mnemonics:
|
|
332
|
+
rule_preview = m.rule[:80] + ".." if len(m.rule) > 80 else m.rule
|
|
333
|
+
lines.append(f"{m.slug}\t[{m.type_code}p{m.priority}]\t{rule_preview}")
|
|
334
|
+
|
|
335
|
+
return [TextContent(type="text", text="\n".join(lines))]
|
|
336
|
+
|
|
337
|
+
elif name == "save_memory":
|
|
338
|
+
slug = arguments.get("slug", "").strip()
|
|
339
|
+
rule = arguments.get("rule", "").strip()
|
|
340
|
+
|
|
341
|
+
if not slug or not rule:
|
|
342
|
+
return [TextContent(type="text", text="Error: slug and rule are required.")]
|
|
343
|
+
|
|
344
|
+
type_code = arguments.get("type_code", "fb")
|
|
345
|
+
why = arguments.get("why")
|
|
346
|
+
when = arguments.get("when")
|
|
347
|
+
tags = arguments.get("tags", [])
|
|
348
|
+
priority = int(arguments.get("priority", 2))
|
|
349
|
+
|
|
350
|
+
existing = repo.get(slug)
|
|
351
|
+
now = datetime.now(timezone.utc)
|
|
352
|
+
|
|
353
|
+
m = Mnemonic(
|
|
354
|
+
type_code=type_code,
|
|
355
|
+
slug=slug,
|
|
356
|
+
timestamp=now,
|
|
357
|
+
rule=rule,
|
|
358
|
+
priority=priority,
|
|
359
|
+
tags=tags if isinstance(tags, list) else [],
|
|
360
|
+
why=why,
|
|
361
|
+
when=when,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
repo.add(m)
|
|
365
|
+
|
|
366
|
+
action = "updated" if existing else "saved"
|
|
367
|
+
return [TextContent(
|
|
368
|
+
type="text",
|
|
369
|
+
text=json.dumps({
|
|
370
|
+
"status": "ok",
|
|
371
|
+
"action": action,
|
|
372
|
+
"slug": slug,
|
|
373
|
+
"type": type_code,
|
|
374
|
+
"priority": priority,
|
|
375
|
+
}, indent=2),
|
|
376
|
+
)]
|
|
377
|
+
|
|
378
|
+
elif name == "get_checkpoint_log":
|
|
379
|
+
limit = int(arguments.get("limit", 5))
|
|
380
|
+
checkpoints = repo.log(limit=limit)
|
|
381
|
+
if not checkpoints:
|
|
382
|
+
return [TextContent(type="text", text="No checkpoints yet.")]
|
|
383
|
+
|
|
384
|
+
lines = []
|
|
385
|
+
for ck in checkpoints:
|
|
386
|
+
sha_s = ck.sha[:8] if ck.sha else "?"
|
|
387
|
+
ts = ck.timestamp.strftime("%Y-%m-%d %H:%M")
|
|
388
|
+
d = ck.diff_summary
|
|
389
|
+
delta = ""
|
|
390
|
+
if d:
|
|
391
|
+
parts = []
|
|
392
|
+
if d.added:
|
|
393
|
+
parts.append(f"+{len(d.added)}")
|
|
394
|
+
if d.modified:
|
|
395
|
+
parts.append(f"~{len(d.modified)}")
|
|
396
|
+
if d.removed:
|
|
397
|
+
parts.append(f"-{len(d.removed)}")
|
|
398
|
+
if parts:
|
|
399
|
+
delta = " " + " ".join(parts)
|
|
400
|
+
lines.append(f"{sha_s} {ts} {ck.message}{delta}")
|
|
401
|
+
|
|
402
|
+
return [TextContent(type="text", text="\n".join(lines))]
|
|
403
|
+
|
|
404
|
+
else:
|
|
405
|
+
return [TextContent(type="text", text=f"Unknown tool: {name}")]
|
|
406
|
+
|
|
407
|
+
# Run
|
|
408
|
+
import asyncio
|
|
409
|
+
|
|
410
|
+
async def _main():
|
|
411
|
+
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
|
|
412
|
+
await server.run(
|
|
413
|
+
read_stream,
|
|
414
|
+
write_stream,
|
|
415
|
+
server.create_initialization_options(),
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
asyncio.run(_main())
|
memgit/models.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Core data models for memgit."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Mnemonic:
|
|
11
|
+
"""A single atomic memory unit — one fact, rule, lesson, or reference."""
|
|
12
|
+
type_code: str # fb|us|pj|rf|cn|lx
|
|
13
|
+
slug: str # kebab-case unique identifier
|
|
14
|
+
timestamp: datetime
|
|
15
|
+
rule: str # primary rule/fact (required)
|
|
16
|
+
priority: int = 2 # 1=low, 2=medium, 3=critical
|
|
17
|
+
tags: list[str] = field(default_factory=list)
|
|
18
|
+
why: Optional[str] = None
|
|
19
|
+
when: Optional[str] = None
|
|
20
|
+
desc: Optional[str] = None
|
|
21
|
+
who: Optional[str] = None
|
|
22
|
+
where: Optional[str] = None
|
|
23
|
+
dl: Optional[str] = None
|
|
24
|
+
inc: Optional[str] = None
|
|
25
|
+
cost: Optional[str] = None
|
|
26
|
+
supersedes: list[str] = field(default_factory=list)
|
|
27
|
+
related: list[str] = field(default_factory=list)
|
|
28
|
+
source: Optional[str] = None
|
|
29
|
+
sha: Optional[str] = None # computed by store
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class MindStateEntry:
|
|
34
|
+
"""One entry in a MindState — maps slug to mnemonic SHA."""
|
|
35
|
+
slug: str
|
|
36
|
+
mnem_sha: str
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class MindState:
|
|
41
|
+
"""Snapshot of all active memories at a point in time (git tree equivalent)."""
|
|
42
|
+
timestamp: datetime
|
|
43
|
+
entries: list[MindStateEntry] = field(default_factory=list)
|
|
44
|
+
sha: Optional[str] = None # computed by store
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def count(self) -> int:
|
|
48
|
+
return len(self.entries)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class DiffSummary:
|
|
53
|
+
"""Summary of what changed between two MindStates."""
|
|
54
|
+
added: list[str] = field(default_factory=list)
|
|
55
|
+
removed: list[str] = field(default_factory=list)
|
|
56
|
+
modified: list[str] = field(default_factory=list)
|
|
57
|
+
unchanged: list[str] = field(default_factory=list)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class Checkpoint:
|
|
62
|
+
"""Immutable snapshot of a MindState (git commit equivalent)."""
|
|
63
|
+
mindstate_sha: str
|
|
64
|
+
timestamp: datetime
|
|
65
|
+
trigger: str # session_end|session_start|explicit|auto|merge|import
|
|
66
|
+
message: str
|
|
67
|
+
author: str
|
|
68
|
+
session_id: str
|
|
69
|
+
parent_sha: Optional[str] = None
|
|
70
|
+
diff_summary: Optional[DiffSummary] = None
|
|
71
|
+
sha: Optional[str] = None # computed by store
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class Thread:
|
|
76
|
+
"""Named pointer to a Checkpoint — an independent line of memory (git branch equivalent)."""
|
|
77
|
+
name: str
|
|
78
|
+
head_sha: str
|
|
79
|
+
created_at: datetime
|
|
80
|
+
description: str = ""
|