memctrl 1.0.0__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.
- memctrl/__init__.py +19 -0
- memctrl/cli.py +443 -0
- memctrl/extractor.py +261 -0
- memctrl/installer.py +122 -0
- memctrl/integrations/langgraph.py +269 -0
- memctrl/mcp_server.py +231 -0
- memctrl/retriever.py +267 -0
- memctrl/rules.py +330 -0
- memctrl/store.py +461 -0
- memctrl/templates/SKILL.md +63 -0
- memctrl/templates/__init__.py +0 -0
- memctrl/tree.py +257 -0
- memctrl-1.0.0.dist-info/METADATA +356 -0
- memctrl-1.0.0.dist-info/RECORD +17 -0
- memctrl-1.0.0.dist-info/WHEEL +4 -0
- memctrl-1.0.0.dist-info/entry_points.txt +2 -0
- memctrl-1.0.0.dist-info/licenses/LICENSE +21 -0
memctrl/store.py
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
"""MemCtrl — SQLite data layer.
|
|
2
|
+
|
|
3
|
+
Implements the core storage for memories, tree nodes, and trigger logs.
|
|
4
|
+
Tree node format adapted from PageIndex (VectifyAI):
|
|
5
|
+
{node_id, title, start_index, end_index, summary, sub_nodes[]}
|
|
6
|
+
We replace page references with memory metadata (layer, source, confidence).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import sqlite3
|
|
13
|
+
import uuid
|
|
14
|
+
from contextlib import contextmanager
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from datetime import datetime, timedelta
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import List, Optional
|
|
19
|
+
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
# Data models
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Memory:
|
|
26
|
+
"""A single memory fact stored in the system."""
|
|
27
|
+
|
|
28
|
+
id: str
|
|
29
|
+
layer: str # 'project' | 'session' | 'user'
|
|
30
|
+
content: str # the memory fact
|
|
31
|
+
source: str # where it came from
|
|
32
|
+
confidence: float # 1.0=explicit, 0.7=inferred, 0.5=mentioned
|
|
33
|
+
created_at: datetime
|
|
34
|
+
expires_at: Optional[datetime]
|
|
35
|
+
tags: List[str] = field(default_factory=list)
|
|
36
|
+
|
|
37
|
+
def to_dict(self) -> dict:
|
|
38
|
+
return {
|
|
39
|
+
"id": self.id,
|
|
40
|
+
"layer": self.layer,
|
|
41
|
+
"content": self.content,
|
|
42
|
+
"source": self.source,
|
|
43
|
+
"confidence": self.confidence,
|
|
44
|
+
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
45
|
+
"expires_at": self.expires_at.isoformat() if self.expires_at else None,
|
|
46
|
+
"tags": self.tags,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def from_row(cls, row: sqlite3.Row) -> "Memory":
|
|
51
|
+
return cls(
|
|
52
|
+
id=row["id"],
|
|
53
|
+
layer=row["layer"],
|
|
54
|
+
content=row["content"],
|
|
55
|
+
source=row["source"],
|
|
56
|
+
confidence=row["confidence"],
|
|
57
|
+
created_at=_parse_dt(row["created_at"]),
|
|
58
|
+
expires_at=_parse_dt(row["expires_at"]) if row["expires_at"] else None,
|
|
59
|
+
tags=json.loads(row["tags"]) if row["tags"] else [],
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class TreeNode:
|
|
65
|
+
"""Hierarchical tree node — PageIndex-adapted for memory.
|
|
66
|
+
|
|
67
|
+
PageIndex node format (VectifyAI):
|
|
68
|
+
{node_id, title, start_index, end_index, summary, sub_nodes[]}
|
|
69
|
+
Adaptation: replace page refs with (layer, memory_ids, confidence).
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
id: str
|
|
73
|
+
title: str # e.g. "tech_stack"
|
|
74
|
+
layer: str # project / session / user
|
|
75
|
+
summary: str # LLM-generated summary of this branch
|
|
76
|
+
memory_ids: List[str] = field(default_factory=list)
|
|
77
|
+
children: List["TreeNode"] = field(default_factory=list)
|
|
78
|
+
confidence: float = 1.0
|
|
79
|
+
last_updated: datetime = field(default_factory=datetime.now)
|
|
80
|
+
|
|
81
|
+
def is_leaf(self) -> bool:
|
|
82
|
+
return len(self.children) == 0
|
|
83
|
+
|
|
84
|
+
def all_memory_ids(self) -> List[str]:
|
|
85
|
+
"""Collect all memory IDs in this subtree."""
|
|
86
|
+
result = list(self.memory_ids)
|
|
87
|
+
for child in self.children:
|
|
88
|
+
result.extend(child.all_memory_ids())
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
def find_node(self, node_id: str) -> Optional["TreeNode"]:
|
|
92
|
+
if self.id == node_id:
|
|
93
|
+
return self
|
|
94
|
+
for child in self.children:
|
|
95
|
+
found = child.find_node(node_id)
|
|
96
|
+
if found:
|
|
97
|
+
return found
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
def to_dict(self) -> dict:
|
|
101
|
+
return {
|
|
102
|
+
"id": self.id,
|
|
103
|
+
"title": self.title,
|
|
104
|
+
"layer": self.layer,
|
|
105
|
+
"summary": self.summary,
|
|
106
|
+
"memory_ids": self.memory_ids,
|
|
107
|
+
"children": [c.to_dict() for c in self.children],
|
|
108
|
+
"confidence": self.confidence,
|
|
109
|
+
"last_updated": self.last_updated.isoformat(),
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def from_dict(cls, data: dict) -> "TreeNode":
|
|
114
|
+
return cls(
|
|
115
|
+
id=data["id"],
|
|
116
|
+
title=data["title"],
|
|
117
|
+
layer=data["layer"],
|
|
118
|
+
summary=data.get("summary", ""),
|
|
119
|
+
memory_ids=data.get("memory_ids", []),
|
|
120
|
+
children=[cls.from_dict(c) for c in data.get("children", [])],
|
|
121
|
+
confidence=data.get("confidence", 1.0),
|
|
122
|
+
last_updated=_parse_dt(data.get("last_updated")),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass
|
|
127
|
+
class TriggerLog:
|
|
128
|
+
"""Audit trail entry for trigger executions."""
|
|
129
|
+
|
|
130
|
+
id: str
|
|
131
|
+
event: str
|
|
132
|
+
action: str
|
|
133
|
+
memories_affected: List[str]
|
|
134
|
+
timestamp: datetime
|
|
135
|
+
|
|
136
|
+
def to_dict(self) -> dict:
|
|
137
|
+
return {
|
|
138
|
+
"id": self.id,
|
|
139
|
+
"event": self.event,
|
|
140
|
+
"action": self.action,
|
|
141
|
+
"memories_affected": self.memories_affected,
|
|
142
|
+
"timestamp": self.timestamp.isoformat() if self.timestamp else None,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@classmethod
|
|
146
|
+
def from_row(cls, row: sqlite3.Row) -> "TriggerLog":
|
|
147
|
+
return cls(
|
|
148
|
+
id=row["id"],
|
|
149
|
+
event=row["event"],
|
|
150
|
+
action=row["action"],
|
|
151
|
+
memories_affected=json.loads(row["memories_affected"]) if row["memories_affected"] else [],
|
|
152
|
+
timestamp=_parse_dt(row["timestamp"]),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
# Helpers
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
def _default_db_path() -> str:
|
|
161
|
+
"""Default SQLite DB path: ~/.memctrl/memories.db"""
|
|
162
|
+
p = Path.home() / ".memctrl" / "memories.db"
|
|
163
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
164
|
+
return str(p)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _parse_dt(value) -> datetime:
|
|
168
|
+
"""Parse datetime from ISO string or return now."""
|
|
169
|
+
if value is None:
|
|
170
|
+
return datetime.now()
|
|
171
|
+
if isinstance(value, datetime):
|
|
172
|
+
return value
|
|
173
|
+
if isinstance(value, str):
|
|
174
|
+
# Try various ISO formats
|
|
175
|
+
for fmt in ("%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"):
|
|
176
|
+
try:
|
|
177
|
+
return datetime.strptime(value.split("+")[0].split("Z")[0], fmt)
|
|
178
|
+
except ValueError:
|
|
179
|
+
continue
|
|
180
|
+
return datetime.now()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _now_iso() -> str:
|
|
184
|
+
return datetime.now().isoformat()
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# ---------------------------------------------------------------------------
|
|
188
|
+
# Store
|
|
189
|
+
# ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
class MemoryStore:
|
|
192
|
+
"""SQLite-backed store for memories, tree nodes, and trigger logs."""
|
|
193
|
+
|
|
194
|
+
def __init__(self, db_path: Optional[str] = None):
|
|
195
|
+
self.db_path = db_path or _default_db_path()
|
|
196
|
+
self._init_db()
|
|
197
|
+
|
|
198
|
+
# --- Connection management ---
|
|
199
|
+
|
|
200
|
+
@contextmanager
|
|
201
|
+
def _connect(self):
|
|
202
|
+
conn = sqlite3.connect(self.db_path)
|
|
203
|
+
conn.row_factory = sqlite3.Row
|
|
204
|
+
try:
|
|
205
|
+
yield conn
|
|
206
|
+
finally:
|
|
207
|
+
conn.close()
|
|
208
|
+
|
|
209
|
+
# --- Schema ---
|
|
210
|
+
|
|
211
|
+
def _init_db(self) -> None:
|
|
212
|
+
with self._connect() as conn:
|
|
213
|
+
conn.executescript(
|
|
214
|
+
"""
|
|
215
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
216
|
+
id TEXT PRIMARY KEY,
|
|
217
|
+
layer TEXT NOT NULL,
|
|
218
|
+
content TEXT NOT NULL,
|
|
219
|
+
source TEXT,
|
|
220
|
+
confidence REAL DEFAULT 1.0,
|
|
221
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
222
|
+
expires_at TIMESTAMP,
|
|
223
|
+
tags TEXT
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
CREATE TABLE IF NOT EXISTS tree_nodes (
|
|
227
|
+
id TEXT PRIMARY KEY,
|
|
228
|
+
parent_id TEXT REFERENCES tree_nodes(id),
|
|
229
|
+
layer TEXT NOT NULL,
|
|
230
|
+
title TEXT NOT NULL,
|
|
231
|
+
summary TEXT,
|
|
232
|
+
memory_ids TEXT,
|
|
233
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
CREATE TABLE IF NOT EXISTS triggers_log (
|
|
237
|
+
id TEXT PRIMARY KEY,
|
|
238
|
+
event TEXT NOT NULL,
|
|
239
|
+
action TEXT NOT NULL,
|
|
240
|
+
memories_affected TEXT,
|
|
241
|
+
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
CREATE INDEX IF NOT EXISTS idx_memories_layer ON memories(layer);
|
|
245
|
+
CREATE INDEX IF NOT EXISTS idx_memories_expires ON memories(expires_at);
|
|
246
|
+
CREATE INDEX IF NOT EXISTS idx_tree_parent ON tree_nodes(parent_id);
|
|
247
|
+
CREATE INDEX IF NOT EXISTS idx_tree_layer ON tree_nodes(layer);
|
|
248
|
+
CREATE INDEX IF NOT EXISTS idx_triggers_ts ON triggers_log(timestamp);
|
|
249
|
+
"""
|
|
250
|
+
)
|
|
251
|
+
conn.commit()
|
|
252
|
+
|
|
253
|
+
# --- Memory CRUD ---
|
|
254
|
+
|
|
255
|
+
def insert_memory(
|
|
256
|
+
self,
|
|
257
|
+
layer: str,
|
|
258
|
+
content: str,
|
|
259
|
+
source: str = "manual",
|
|
260
|
+
confidence: float = 1.0,
|
|
261
|
+
tags: Optional[List[str]] = None,
|
|
262
|
+
expires_at: Optional[datetime] = None,
|
|
263
|
+
) -> str:
|
|
264
|
+
mid = str(uuid.uuid4())
|
|
265
|
+
with self._connect() as conn:
|
|
266
|
+
conn.execute(
|
|
267
|
+
"""INSERT INTO memories (id, layer, content, source, confidence,
|
|
268
|
+
created_at, expires_at, tags)
|
|
269
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
270
|
+
(mid, layer, content, source, confidence,
|
|
271
|
+
_now_iso(),
|
|
272
|
+
expires_at.isoformat() if expires_at else None,
|
|
273
|
+
json.dumps(tags or [])),
|
|
274
|
+
)
|
|
275
|
+
conn.commit()
|
|
276
|
+
return mid
|
|
277
|
+
|
|
278
|
+
def get_memory(self, id: str) -> Optional[Memory]:
|
|
279
|
+
with self._connect() as conn:
|
|
280
|
+
row = conn.execute(
|
|
281
|
+
"SELECT * FROM memories WHERE id = ?", (id,)
|
|
282
|
+
).fetchone()
|
|
283
|
+
return Memory.from_row(row) if row else None
|
|
284
|
+
|
|
285
|
+
def list_memories(self, layer: Optional[str] = None) -> List[Memory]:
|
|
286
|
+
with self._connect() as conn:
|
|
287
|
+
if layer:
|
|
288
|
+
rows = conn.execute(
|
|
289
|
+
"SELECT * FROM memories WHERE layer = ? ORDER BY created_at DESC",
|
|
290
|
+
(layer,),
|
|
291
|
+
).fetchall()
|
|
292
|
+
else:
|
|
293
|
+
rows = conn.execute(
|
|
294
|
+
"SELECT * FROM memories ORDER BY created_at DESC"
|
|
295
|
+
).fetchall()
|
|
296
|
+
return [Memory.from_row(r) for r in rows]
|
|
297
|
+
|
|
298
|
+
def delete_memory(self, id: str) -> bool:
|
|
299
|
+
with self._connect() as conn:
|
|
300
|
+
cur = conn.execute("DELETE FROM memories WHERE id = ?", (id,))
|
|
301
|
+
conn.commit()
|
|
302
|
+
return cur.rowcount > 0
|
|
303
|
+
|
|
304
|
+
def update_memory_layer(self, id: str, new_layer: str) -> bool:
|
|
305
|
+
with self._connect() as conn:
|
|
306
|
+
cur = conn.execute(
|
|
307
|
+
"UPDATE memories SET layer = ? WHERE id = ?",
|
|
308
|
+
(new_layer, id),
|
|
309
|
+
)
|
|
310
|
+
conn.commit()
|
|
311
|
+
return cur.rowcount > 0
|
|
312
|
+
|
|
313
|
+
# --- Expiration ---
|
|
314
|
+
|
|
315
|
+
def expire_old_memories(self) -> int:
|
|
316
|
+
"""Delete memories where expires_at < now(). Returns count."""
|
|
317
|
+
with self._connect() as conn:
|
|
318
|
+
cur = conn.execute(
|
|
319
|
+
"DELETE FROM memories WHERE expires_at IS NOT NULL AND expires_at < ?",
|
|
320
|
+
(_now_iso(),),
|
|
321
|
+
)
|
|
322
|
+
conn.commit()
|
|
323
|
+
return cur.rowcount
|
|
324
|
+
|
|
325
|
+
# --- Consolidation ---
|
|
326
|
+
|
|
327
|
+
def consolidate(self, from_layer: str, to_layer: str) -> List[str]:
|
|
328
|
+
"""Move all memories from from_layer to to_layer. Returns moved IDs."""
|
|
329
|
+
with self._connect() as conn:
|
|
330
|
+
rows = conn.execute(
|
|
331
|
+
"SELECT id FROM memories WHERE layer = ?", (from_layer,)
|
|
332
|
+
).fetchall()
|
|
333
|
+
ids = [r["id"] for r in rows]
|
|
334
|
+
if ids:
|
|
335
|
+
placeholders = ",".join("?" * len(ids))
|
|
336
|
+
conn.execute(
|
|
337
|
+
f"UPDATE memories SET layer = ? WHERE id IN ({placeholders})",
|
|
338
|
+
(to_layer, *ids),
|
|
339
|
+
)
|
|
340
|
+
conn.commit()
|
|
341
|
+
return ids
|
|
342
|
+
|
|
343
|
+
# --- Tree nodes ---
|
|
344
|
+
|
|
345
|
+
def clear_tree_nodes(self) -> None:
|
|
346
|
+
with self._connect() as conn:
|
|
347
|
+
conn.execute("DELETE FROM tree_nodes")
|
|
348
|
+
conn.commit()
|
|
349
|
+
|
|
350
|
+
def insert_tree_node(
|
|
351
|
+
self, node: TreeNode, parent_id: Optional[str] = None
|
|
352
|
+
) -> str:
|
|
353
|
+
with self._connect() as conn:
|
|
354
|
+
conn.execute(
|
|
355
|
+
"""INSERT INTO tree_nodes (id, parent_id, layer, title, summary,
|
|
356
|
+
memory_ids, updated_at)
|
|
357
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
|
358
|
+
(node.id, parent_id, node.layer, node.title, node.summary,
|
|
359
|
+
json.dumps(node.memory_ids), _now_iso()),
|
|
360
|
+
)
|
|
361
|
+
conn.commit()
|
|
362
|
+
return node.id
|
|
363
|
+
|
|
364
|
+
def get_tree_nodes(self, layer: Optional[str] = None) -> List[dict]:
|
|
365
|
+
with self._connect() as conn:
|
|
366
|
+
if layer:
|
|
367
|
+
rows = conn.execute(
|
|
368
|
+
"SELECT * FROM tree_nodes WHERE layer = ?", (layer,)
|
|
369
|
+
).fetchall()
|
|
370
|
+
else:
|
|
371
|
+
rows = conn.execute("SELECT * FROM tree_nodes").fetchall()
|
|
372
|
+
return [
|
|
373
|
+
{
|
|
374
|
+
"id": r["id"],
|
|
375
|
+
"parent_id": r["parent_id"],
|
|
376
|
+
"layer": r["layer"],
|
|
377
|
+
"title": r["title"],
|
|
378
|
+
"summary": r["summary"],
|
|
379
|
+
"memory_ids": json.loads(r["memory_ids"]) if r["memory_ids"] else [],
|
|
380
|
+
}
|
|
381
|
+
for r in rows
|
|
382
|
+
]
|
|
383
|
+
|
|
384
|
+
def build_tree_from_nodes(self) -> Optional[TreeNode]:
|
|
385
|
+
"""Rebuild TreeNode hierarchy from flat DB rows."""
|
|
386
|
+
nodes = self.get_tree_nodes()
|
|
387
|
+
if not nodes:
|
|
388
|
+
return None
|
|
389
|
+
by_id = {n["id"]: n for n in nodes}
|
|
390
|
+
children = {}
|
|
391
|
+
root_candidates = []
|
|
392
|
+
for n in nodes:
|
|
393
|
+
pid = n.get("parent_id")
|
|
394
|
+
if pid:
|
|
395
|
+
children.setdefault(pid, []).append(n)
|
|
396
|
+
else:
|
|
397
|
+
root_candidates.append(n)
|
|
398
|
+
|
|
399
|
+
def build(n: dict) -> TreeNode:
|
|
400
|
+
node = TreeNode(
|
|
401
|
+
id=n["id"],
|
|
402
|
+
title=n["title"],
|
|
403
|
+
layer=n["layer"],
|
|
404
|
+
summary=n.get("summary", ""),
|
|
405
|
+
memory_ids=n.get("memory_ids", []),
|
|
406
|
+
children=[build(c) for c in children.get(n["id"], [])],
|
|
407
|
+
)
|
|
408
|
+
return node
|
|
409
|
+
|
|
410
|
+
if not root_candidates:
|
|
411
|
+
return None
|
|
412
|
+
# Use first root as main root, wrap others under it
|
|
413
|
+
if len(root_candidates) == 1:
|
|
414
|
+
return build(root_candidates[0])
|
|
415
|
+
root = TreeNode(
|
|
416
|
+
id="root", title="Memory Tree", layer="root",
|
|
417
|
+
summary="Root of all memory layers",
|
|
418
|
+
children=[build(r) for r in root_candidates],
|
|
419
|
+
)
|
|
420
|
+
return root
|
|
421
|
+
|
|
422
|
+
# --- Trigger log ---
|
|
423
|
+
|
|
424
|
+
def log_trigger(self, event: str, action: str, memory_ids: List[str]) -> str:
|
|
425
|
+
tid = str(uuid.uuid4())
|
|
426
|
+
with self._connect() as conn:
|
|
427
|
+
conn.execute(
|
|
428
|
+
"""INSERT INTO triggers_log (id, event, action,
|
|
429
|
+
memories_affected, timestamp)
|
|
430
|
+
VALUES (?, ?, ?, ?, ?)""",
|
|
431
|
+
(tid, event, action, json.dumps(memory_ids), _now_iso()),
|
|
432
|
+
)
|
|
433
|
+
conn.commit()
|
|
434
|
+
return tid
|
|
435
|
+
|
|
436
|
+
def get_trigger_log(self, limit: int = 50) -> List[TriggerLog]:
|
|
437
|
+
with self._connect() as conn:
|
|
438
|
+
rows = conn.execute(
|
|
439
|
+
"SELECT * FROM triggers_log ORDER BY timestamp DESC LIMIT ?",
|
|
440
|
+
(limit,),
|
|
441
|
+
).fetchall()
|
|
442
|
+
return [TriggerLog.from_row(r) for r in rows]
|
|
443
|
+
|
|
444
|
+
# --- Stats ---
|
|
445
|
+
|
|
446
|
+
def stats(self) -> dict:
|
|
447
|
+
with self._connect() as conn:
|
|
448
|
+
mem_count = conn.execute(
|
|
449
|
+
"SELECT COUNT(*) FROM memories"
|
|
450
|
+
).fetchone()[0]
|
|
451
|
+
node_count = conn.execute(
|
|
452
|
+
"SELECT COUNT(*) FROM tree_nodes"
|
|
453
|
+
).fetchone()[0]
|
|
454
|
+
trigger_count = conn.execute(
|
|
455
|
+
"SELECT COUNT(*) FROM triggers_log"
|
|
456
|
+
).fetchone()[0]
|
|
457
|
+
return {
|
|
458
|
+
"memories": mem_count,
|
|
459
|
+
"tree_nodes": node_count,
|
|
460
|
+
"triggers": trigger_count,
|
|
461
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: memctrl
|
|
3
|
+
description: >
|
|
4
|
+
Rule-governed memory layer for this project. Call memctrl to store,
|
|
5
|
+
retrieve, or update memory about this project, session, or user preferences.
|
|
6
|
+
Use this when:
|
|
7
|
+
- Starting a new session (load context)
|
|
8
|
+
- Making an architecture decision (store to project layer)
|
|
9
|
+
- Finishing work (trigger on_session_end)
|
|
10
|
+
- Asking "what did we decide about X" (query memory tree)
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# MemCtrl Memory System
|
|
14
|
+
|
|
15
|
+
## When to Use
|
|
16
|
+
|
|
17
|
+
- **Session start**: Run `memctrl query "current project context"` for instant context
|
|
18
|
+
- **Architecture decisions**: Run `memctrl add --layer project "decided to use X because Y"`
|
|
19
|
+
- **Session end**: Run `memctrl trigger on_session_end` to consolidate memories
|
|
20
|
+
- **Any context question**: Run `memctrl query "<your question>"`
|
|
21
|
+
|
|
22
|
+
## Memory Layers
|
|
23
|
+
|
|
24
|
+
| Layer | Purpose | Default Expiry |
|
|
25
|
+
|-------|---------|---------------|
|
|
26
|
+
| project | Architecture decisions, tech stack, ADRs | Never |
|
|
27
|
+
| session | Current task, WIP, what was done this session | 7 days |
|
|
28
|
+
| user | Personal preferences, working style, patterns | 90 days |
|
|
29
|
+
|
|
30
|
+
## Key Commands
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Query what you need
|
|
34
|
+
memctrl query "what is our tech stack?"
|
|
35
|
+
memctrl query "why did we choose PostgreSQL?"
|
|
36
|
+
|
|
37
|
+
# Store decisions
|
|
38
|
+
memctrl add "decided to use Firecracker for sandbox isolation" --layer project
|
|
39
|
+
memctrl add "currently implementing auth flow" --layer session
|
|
40
|
+
memctrl add "prefers async Python, minimal abstractions" --layer user
|
|
41
|
+
|
|
42
|
+
# Manage
|
|
43
|
+
memctrl tree # view full memory tree
|
|
44
|
+
memctrl trigger on_session_end # consolidate session memories
|
|
45
|
+
memctrl audit # review what was remembered/forgotten
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## How Retrieval Works
|
|
49
|
+
|
|
50
|
+
MemCtrl uses tree-based reasoning (like PageIndex) instead of vector similarity:
|
|
51
|
+
- Memories are organized in a semantic tree: project/tech_stack/database
|
|
52
|
+
- When you query, the system reasons about which branches to explore
|
|
53
|
+
- Results include a trace: root → project → tech_stack → database
|
|
54
|
+
- No embeddings needed — pure structured reasoning
|
|
55
|
+
|
|
56
|
+
## MCP Server
|
|
57
|
+
|
|
58
|
+
If the MCP server is running (`memctrl serve`), these tools are available:
|
|
59
|
+
- `memctrl_query(query, layer?)` → Retrieve memories with trace
|
|
60
|
+
- `memctrl_add(content, layer, source?)` → Store a memory
|
|
61
|
+
- `memctrl_trigger(event, context?)` → Fire a trigger
|
|
62
|
+
- `memctrl_tree()` → Get full memory tree
|
|
63
|
+
- `memctrl_audit(limit?)` → View audit log
|
|
File without changes
|