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/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""MemCtrl — Rule-governed memory layer for AI coding assistants.
|
|
2
|
+
|
|
3
|
+
Inspired by PageIndex's tree-based retrieval and Graphify's install pattern.
|
|
4
|
+
Uses hierarchical reasoning (not vectors) for explainable memory retrieval.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "1.0.0"
|
|
8
|
+
|
|
9
|
+
from memctrl.store import Memory, MemoryStore, TriggerLog, TreeNode
|
|
10
|
+
from memctrl.retriever import RetrievalResult
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"__version__",
|
|
14
|
+
"Memory",
|
|
15
|
+
"MemoryStore",
|
|
16
|
+
"TreeNode",
|
|
17
|
+
"TriggerLog",
|
|
18
|
+
"RetrievalResult",
|
|
19
|
+
]
|
memctrl/cli.py
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
"""MemCtrl — Typer CLI with rich formatting.
|
|
2
|
+
|
|
3
|
+
Commands:
|
|
4
|
+
install, init, add, query, list, tree, forget, clear,
|
|
5
|
+
trigger, audit, serve, --version
|
|
6
|
+
|
|
7
|
+
Uses typer + rich for beautiful terminal output.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
import typer
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
from rich.panel import Panel
|
|
21
|
+
from rich.table import Table
|
|
22
|
+
from rich.tree import Tree as RichTree
|
|
23
|
+
|
|
24
|
+
from memctrl import __version__
|
|
25
|
+
|
|
26
|
+
app = typer.Typer(
|
|
27
|
+
name="memctrl",
|
|
28
|
+
help="Rule-governed memory layer for AI coding assistants",
|
|
29
|
+
add_completion=False,
|
|
30
|
+
)
|
|
31
|
+
console = Console()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# Helpers
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
def _get_store():
|
|
39
|
+
"""Get MemoryStore instance with default DB path."""
|
|
40
|
+
from memctrl.store import MemoryStore
|
|
41
|
+
db_path = os.environ.get("MEMCTRL_DB_PATH")
|
|
42
|
+
return MemoryStore(db_path)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _get_engine():
|
|
46
|
+
"""Get RuleEngine instance."""
|
|
47
|
+
from memctrl.rules import RuleEngine
|
|
48
|
+
return RuleEngine()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
# Callback (version)
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
@app.callback(invoke_without_command=True)
|
|
56
|
+
def main(
|
|
57
|
+
ctx: typer.Context,
|
|
58
|
+
version: bool = typer.Option(False, "--version", help="Show version"),
|
|
59
|
+
):
|
|
60
|
+
if version:
|
|
61
|
+
console.print(f"MemCtrl v{__version__}")
|
|
62
|
+
raise typer.Exit()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
# Commands
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
@app.command()
|
|
70
|
+
def install(
|
|
71
|
+
tool: Optional[str] = typer.Option(None, help="Specific tool to install for"),
|
|
72
|
+
project: bool = typer.Option(False, help="Install at project level (.claude/ etc.)"),
|
|
73
|
+
):
|
|
74
|
+
"""Register SKILL.md with AI coding tools (Claude Code, Cursor, etc.)"""
|
|
75
|
+
from memctrl.installer import install_skill
|
|
76
|
+
paths = install_skill(tool=tool, project=project, verbose=True)
|
|
77
|
+
if paths:
|
|
78
|
+
console.print(f"\n[green]Installed to {len(paths)} location(s)[/green]")
|
|
79
|
+
else:
|
|
80
|
+
console.print("\n[yellow]No tools installed. See paths above.[/yellow]")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@app.command()
|
|
84
|
+
def init(
|
|
85
|
+
force: bool = typer.Option(False, help="Overwrite existing .memoryrc"),
|
|
86
|
+
):
|
|
87
|
+
"""Create .memoryrc in current directory"""
|
|
88
|
+
dest = Path(".memoryrc")
|
|
89
|
+
if dest.exists() and not force:
|
|
90
|
+
console.print(f"[yellow]{dest} already exists. Use --force to overwrite.[/yellow]")
|
|
91
|
+
raise typer.Exit(1)
|
|
92
|
+
|
|
93
|
+
example = Path(__file__).parent / ".memoryrc.example"
|
|
94
|
+
if example.exists():
|
|
95
|
+
content = example.read_text()
|
|
96
|
+
else:
|
|
97
|
+
content = _default_memoryrc()
|
|
98
|
+
|
|
99
|
+
dest.write_text(content)
|
|
100
|
+
console.print(f"[green]Created {dest}[/green]")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@app.command()
|
|
104
|
+
def add(
|
|
105
|
+
content: str = typer.Argument(..., help="Memory content to store"),
|
|
106
|
+
layer: str = typer.Option("session", help="Layer: project/session/user"),
|
|
107
|
+
source: str = typer.Option("manual", help="Source of this memory"),
|
|
108
|
+
):
|
|
109
|
+
"""Manually add a memory"""
|
|
110
|
+
store = _get_store()
|
|
111
|
+
mid = store.insert_memory(layer=layer, content=content, source=source)
|
|
112
|
+
console.print(f"[green]Added memory[/green] [dim]{mid}[/dim] to [bold]{layer}[/bold]")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@app.command()
|
|
116
|
+
def query(
|
|
117
|
+
query_text: str = typer.Argument(..., help="Query to search memory"),
|
|
118
|
+
layer: Optional[str] = typer.Option(None, help="Filter by layer"),
|
|
119
|
+
):
|
|
120
|
+
"""Retrieve relevant memories with reasoning trace"""
|
|
121
|
+
store = _get_store()
|
|
122
|
+
engine = _get_engine()
|
|
123
|
+
rules = engine.load()
|
|
124
|
+
|
|
125
|
+
memories = store.list_memories(layer=layer)
|
|
126
|
+
if not memories:
|
|
127
|
+
console.print("[yellow]No memories found.[/yellow]")
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
mem_dicts = [m.to_dict() for m in memories]
|
|
131
|
+
memory_lookup = {m.id: m.to_dict() for m in memories}
|
|
132
|
+
|
|
133
|
+
# Build tree
|
|
134
|
+
from memctrl.tree import MemoryTreeBuilder
|
|
135
|
+
builder = MemoryTreeBuilder()
|
|
136
|
+
|
|
137
|
+
async def _do_query():
|
|
138
|
+
tree = await builder.build_tree(mem_dicts)
|
|
139
|
+
tree_dict = tree.to_dict()
|
|
140
|
+
|
|
141
|
+
# Retrieve
|
|
142
|
+
from memctrl.retriever import MemoryRetriever
|
|
143
|
+
retriever = MemoryRetriever()
|
|
144
|
+
result = await retriever.retrieve(query_text, tree_dict, memory_lookup=memory_lookup)
|
|
145
|
+
return result
|
|
146
|
+
|
|
147
|
+
result = asyncio.run(_do_query())
|
|
148
|
+
|
|
149
|
+
if result.facts:
|
|
150
|
+
console.print(Panel(f"[bold]Query:[/bold] {query_text}", title="memctrl"))
|
|
151
|
+
console.print(f"\n[bold green]Facts:[/bold green]")
|
|
152
|
+
for i, fact in enumerate(result.facts, 1):
|
|
153
|
+
console.print(f" {i}. {fact}")
|
|
154
|
+
console.print(f"\n[bold blue]Trace:[/bold blue] {' -> '.join(result.trace)}")
|
|
155
|
+
console.print(f"[bold]Confidence:[/bold] {result.confidence:.2f}")
|
|
156
|
+
else:
|
|
157
|
+
console.print(f"[yellow]No relevant memories found for:[/yellow] {query_text}")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@app.command("list")
|
|
161
|
+
def list_memories(
|
|
162
|
+
layer: Optional[str] = typer.Option(None, help="Filter by layer"),
|
|
163
|
+
limit: int = typer.Option(50, help="Max results"),
|
|
164
|
+
):
|
|
165
|
+
"""List all memories"""
|
|
166
|
+
store = _get_store()
|
|
167
|
+
memories = store.list_memories(layer=layer)[:limit]
|
|
168
|
+
|
|
169
|
+
if not memories:
|
|
170
|
+
console.print("[yellow]No memories found.[/yellow]")
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
table = Table(title="Memories", show_lines=True)
|
|
174
|
+
table.add_column("ID", style="dim", max_width=8)
|
|
175
|
+
table.add_column("Layer", style="cyan")
|
|
176
|
+
table.add_column("Content", max_width=60)
|
|
177
|
+
table.add_column("Source", style="green")
|
|
178
|
+
table.add_column("Conf", justify="right")
|
|
179
|
+
|
|
180
|
+
for mem in memories:
|
|
181
|
+
table.add_row(
|
|
182
|
+
mem.id[:8],
|
|
183
|
+
mem.layer,
|
|
184
|
+
mem.content[:80],
|
|
185
|
+
mem.source,
|
|
186
|
+
f"{mem.confidence:.1f}",
|
|
187
|
+
)
|
|
188
|
+
console.print(table)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@app.command()
|
|
192
|
+
def tree():
|
|
193
|
+
"""Display memory tree (rich formatted)"""
|
|
194
|
+
store = _get_store()
|
|
195
|
+
memories = store.list_memories()
|
|
196
|
+
|
|
197
|
+
if not memories:
|
|
198
|
+
console.print("[yellow]No memories to display.[/yellow]")
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
from memctrl.tree import MemoryTreeBuilder
|
|
202
|
+
builder = MemoryTreeBuilder()
|
|
203
|
+
|
|
204
|
+
async def _do_tree():
|
|
205
|
+
mem_dicts = [m.to_dict() for m in memories]
|
|
206
|
+
root = await builder.build_tree(mem_dicts)
|
|
207
|
+
return root
|
|
208
|
+
|
|
209
|
+
root = asyncio.run(_do_tree())
|
|
210
|
+
|
|
211
|
+
def _build_rich(node, rich_node):
|
|
212
|
+
for child in node.children:
|
|
213
|
+
if child.is_leaf() or not child.children:
|
|
214
|
+
label = f"[dim][mem][/dim] {child.title[:50]}"
|
|
215
|
+
else:
|
|
216
|
+
label = f"[bold][dir] {child.title}[/bold]"
|
|
217
|
+
if child.confidence < 1.0:
|
|
218
|
+
label += f" [dim](conf: {child.confidence})[/dim]"
|
|
219
|
+
branch = rich_node.add(label)
|
|
220
|
+
_build_rich(child, branch)
|
|
221
|
+
|
|
222
|
+
rich_root = RichTree("[bold]Memory Tree[/bold]")
|
|
223
|
+
_build_rich(root, rich_root)
|
|
224
|
+
console.print(rich_root)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@app.command()
|
|
228
|
+
def forget(
|
|
229
|
+
memory_id: str = typer.Argument(..., help="Memory ID to forget"),
|
|
230
|
+
):
|
|
231
|
+
"""Remove a memory by ID"""
|
|
232
|
+
store = _get_store()
|
|
233
|
+
if store.delete_memory(memory_id):
|
|
234
|
+
console.print(f"[green]Forgot memory[/green] [dim]{memory_id}[/dim]")
|
|
235
|
+
else:
|
|
236
|
+
console.print(f"[red]Memory not found:[/red] {memory_id}")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@app.command()
|
|
240
|
+
def clear(
|
|
241
|
+
layer: Optional[str] = typer.Option(None, help="Clear specific layer"),
|
|
242
|
+
yes: bool = typer.Option(False, "--yes", help="Skip confirmation"),
|
|
243
|
+
):
|
|
244
|
+
"""Clear memories (all or by layer)"""
|
|
245
|
+
store = _get_store()
|
|
246
|
+
memories = store.list_memories(layer=layer)
|
|
247
|
+
count = len(memories)
|
|
248
|
+
|
|
249
|
+
if count == 0:
|
|
250
|
+
console.print("[yellow]No memories to clear.[/yellow]")
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
target = f"'{layer}' layer" if layer else "ALL memories"
|
|
254
|
+
if not yes:
|
|
255
|
+
confirm = typer.confirm(f"Clear {target}? ({count} memories)")
|
|
256
|
+
if not confirm:
|
|
257
|
+
console.print("Cancelled.")
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
if layer:
|
|
261
|
+
for mem in memories:
|
|
262
|
+
store.delete_memory(mem.id)
|
|
263
|
+
else:
|
|
264
|
+
for mem in memories:
|
|
265
|
+
store.delete_memory(mem.id)
|
|
266
|
+
|
|
267
|
+
console.print(f"[green]Cleared {count} memories.[/green]")
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@app.command()
|
|
271
|
+
def trigger_cmd(
|
|
272
|
+
event: str = typer.Argument(..., help="Event name (e.g., on_session_end)"),
|
|
273
|
+
context: Optional[str] = typer.Option(None, help="JSON context string"),
|
|
274
|
+
):
|
|
275
|
+
"""Manually fire a trigger"""
|
|
276
|
+
store = _get_store()
|
|
277
|
+
engine = _get_engine()
|
|
278
|
+
rules = engine.load()
|
|
279
|
+
|
|
280
|
+
ctx = {}
|
|
281
|
+
if context:
|
|
282
|
+
try:
|
|
283
|
+
ctx = json.loads(context)
|
|
284
|
+
except json.JSONDecodeError:
|
|
285
|
+
console.print("[red]Invalid JSON context[/red]")
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
ids = engine.fire_trigger(event, ctx, store)
|
|
289
|
+
console.print(f"[green]Trigger '{event}' fired[/green] - {len(ids)} memories affected")
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@app.command()
|
|
293
|
+
def audit(
|
|
294
|
+
limit: int = typer.Option(50, help="Number of log entries to show"),
|
|
295
|
+
):
|
|
296
|
+
"""Show audit log of triggers"""
|
|
297
|
+
store = _get_store()
|
|
298
|
+
logs = store.get_trigger_log(limit=limit)
|
|
299
|
+
|
|
300
|
+
if not logs:
|
|
301
|
+
console.print("[yellow]No audit entries.[/yellow]")
|
|
302
|
+
return
|
|
303
|
+
|
|
304
|
+
table = Table(title="Trigger Audit Log")
|
|
305
|
+
table.add_column("Timestamp", style="dim")
|
|
306
|
+
table.add_column("Event", style="cyan")
|
|
307
|
+
table.add_column("Action")
|
|
308
|
+
table.add_column("Memories", justify="right")
|
|
309
|
+
|
|
310
|
+
for log in logs:
|
|
311
|
+
table.add_row(
|
|
312
|
+
log.timestamp.strftime("%Y-%m-%d %H:%M"),
|
|
313
|
+
log.event,
|
|
314
|
+
log.action,
|
|
315
|
+
str(len(log.memories_affected)),
|
|
316
|
+
)
|
|
317
|
+
console.print(table)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
@app.command()
|
|
321
|
+
def heatmap():
|
|
322
|
+
"""Show memory distribution heatmap by layer and tags"""
|
|
323
|
+
store = _get_store()
|
|
324
|
+
memories = store.list_memories()
|
|
325
|
+
|
|
326
|
+
if not memories:
|
|
327
|
+
console.print("[yellow]No memories found.[/yellow]")
|
|
328
|
+
return
|
|
329
|
+
|
|
330
|
+
console.print(Panel("[bold]Memory Heatmap[/bold]", border_style="cyan"))
|
|
331
|
+
|
|
332
|
+
# Layer distribution
|
|
333
|
+
console.print("\n[bold]By Layer:[/bold]")
|
|
334
|
+
by_layer: dict = {}
|
|
335
|
+
for mem in memories:
|
|
336
|
+
by_layer[mem.layer] = by_layer.get(mem.layer, 0) + 1
|
|
337
|
+
total = len(memories)
|
|
338
|
+
for layer, count in sorted(by_layer.items(), key=lambda x: -x[1]):
|
|
339
|
+
pct = count / total * 100
|
|
340
|
+
bar_len = int(pct / 5)
|
|
341
|
+
bar = "█" * bar_len + "░" * (20 - bar_len)
|
|
342
|
+
color = "green" if layer == "project" else "yellow" if layer == "session" else "blue"
|
|
343
|
+
console.print(f" [{color}]{layer:10}[/{color}] {bar} {count:3} ({pct:.0f}%)")
|
|
344
|
+
|
|
345
|
+
# Tag distribution
|
|
346
|
+
tag_counts: dict = {}
|
|
347
|
+
for mem in memories:
|
|
348
|
+
for tag in mem.tags:
|
|
349
|
+
tag_counts[tag] = tag_counts.get(tag, 0) + 1
|
|
350
|
+
|
|
351
|
+
if tag_counts:
|
|
352
|
+
console.print("\n[bold]By Tag:[/bold]")
|
|
353
|
+
for tag, count in sorted(tag_counts.items(), key=lambda x: -x[1])[:10]:
|
|
354
|
+
bar_len = min(count, 20)
|
|
355
|
+
bar = "█" * bar_len + "░" * (20 - bar_len)
|
|
356
|
+
console.print(f" {tag:15} {bar} {count}")
|
|
357
|
+
|
|
358
|
+
console.print(f"\n[dim]Total: {total} memories[/dim]")
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
@app.command()
|
|
362
|
+
def timeline(
|
|
363
|
+
limit: int = typer.Option(20, help="Max events to show"),
|
|
364
|
+
):
|
|
365
|
+
"""Show chronological memory timeline"""
|
|
366
|
+
store = _get_store()
|
|
367
|
+
memories = store.list_memories()[:limit]
|
|
368
|
+
logs = store.get_trigger_log(limit=limit)
|
|
369
|
+
|
|
370
|
+
if not memories and not logs:
|
|
371
|
+
console.print("[yellow]No timeline events.[/yellow]")
|
|
372
|
+
return
|
|
373
|
+
|
|
374
|
+
console.print(Panel("[bold]Memory Timeline[/bold]", border_style="cyan"))
|
|
375
|
+
|
|
376
|
+
# Merge and sort events
|
|
377
|
+
events = []
|
|
378
|
+
for mem in memories:
|
|
379
|
+
events.append({
|
|
380
|
+
"ts": mem.created_at,
|
|
381
|
+
"type": "memory",
|
|
382
|
+
"layer": mem.layer,
|
|
383
|
+
"content": mem.content[:60],
|
|
384
|
+
"icon": "[mem]",
|
|
385
|
+
})
|
|
386
|
+
for log in logs:
|
|
387
|
+
events.append({
|
|
388
|
+
"ts": log.timestamp,
|
|
389
|
+
"type": "trigger",
|
|
390
|
+
"layer": "",
|
|
391
|
+
"content": f"{log.event}: {log.action}",
|
|
392
|
+
"icon": "[refl]",
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
events.sort(key=lambda x: x["ts"], reverse=True)
|
|
396
|
+
|
|
397
|
+
for ev in events[:limit]:
|
|
398
|
+
ts = ev["ts"].strftime("%Y-%m-%d %H:%M") if ev["ts"] else "?"
|
|
399
|
+
if ev["type"] == "memory":
|
|
400
|
+
color = "green" if ev["layer"] == "project" else "yellow" if ev["layer"] == "session" else "blue"
|
|
401
|
+
console.print(f" [dim]{ts}[/dim] [{color}]{ev['icon']}[/{color}] ({ev['layer']}) {ev['content']}")
|
|
402
|
+
else:
|
|
403
|
+
console.print(f" [dim]{ts}[/dim] [magenta]{ev['icon']}[/magenta] {ev['content']}")
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
@app.command()
|
|
407
|
+
def serve(
|
|
408
|
+
port: int = typer.Option(8080, help="Port to run MCP server on"),
|
|
409
|
+
host: str = typer.Option("127.0.0.1", help="Host to bind to"),
|
|
410
|
+
):
|
|
411
|
+
"""Start MCP server"""
|
|
412
|
+
console.print(f"[green]Starting MCP server on {host}:{port}[/green]")
|
|
413
|
+
console.print("[dim]Use Ctrl+C to stop[/dim]")
|
|
414
|
+
|
|
415
|
+
from memctrl.mcp_server import serve_mcp
|
|
416
|
+
asyncio.run(serve_mcp(host=host, port=port))
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
# ---------------------------------------------------------------------------
|
|
420
|
+
# Default .memoryrc content
|
|
421
|
+
# ---------------------------------------------------------------------------
|
|
422
|
+
|
|
423
|
+
def _default_memoryrc() -> str:
|
|
424
|
+
return '''# MemCtrl configuration
|
|
425
|
+
|
|
426
|
+
[layers]
|
|
427
|
+
project = "architecture decisions, tech stack, ADRs, why we chose X"
|
|
428
|
+
session = "current task, WIP, what was done this session"
|
|
429
|
+
user = "preferences, working style, patterns, personal rules"
|
|
430
|
+
|
|
431
|
+
[triggers]
|
|
432
|
+
on_commit = "consolidate session -> project"
|
|
433
|
+
on_session_end = "summarize session -> user"
|
|
434
|
+
'on_file "docs/ADR-*.md"' = "extract -> project"
|
|
435
|
+
'on_file "*.md"' = "extract -> project if contains decision"
|
|
436
|
+
|
|
437
|
+
[forget]
|
|
438
|
+
never = ["passwords", "keys", "PII", "secrets"]
|
|
439
|
+
after_days = { session = 7, user = 90 }
|
|
440
|
+
|
|
441
|
+
[extract]
|
|
442
|
+
confidence = { explicit = 1.0, inferred = 0.7, mentioned = 0.5 }
|
|
443
|
+
'''
|