mnemo-mcp-agent 0.3.2__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.
- mnemo/__init__.py +3 -0
- mnemo/cli/__init__.py +1 -0
- mnemo/cli/main.py +512 -0
- mnemo/cli/visualize.py +640 -0
- mnemo/core/__init__.py +1 -0
- mnemo/core/decay.py +96 -0
- mnemo/core/models.py +270 -0
- mnemo/core/rrf.py +78 -0
- mnemo/engine/__init__.py +1 -0
- mnemo/engine/audn.py +185 -0
- mnemo/engine/retriever.py +139 -0
- mnemo/engine/scanner.py +484 -0
- mnemo/engine/tier_manager.py +190 -0
- mnemo/mcp/__init__.py +1 -0
- mnemo/mcp/prompts.py +40 -0
- mnemo/mcp/resources.py +91 -0
- mnemo/mcp/server.py +141 -0
- mnemo/mcp/tools.py +259 -0
- mnemo/serialization/__init__.py +1 -0
- mnemo/serialization/toon.py +422 -0
- mnemo/storage/__init__.py +1 -0
- mnemo/storage/connection.py +193 -0
- mnemo/storage/fts_store.py +100 -0
- mnemo/storage/graph_store.py +178 -0
- mnemo/storage/migrations.py +221 -0
- mnemo/storage/schema.sql +141 -0
- mnemo/storage/vector_store.py +232 -0
- mnemo_mcp_agent-0.3.2.dist-info/METADATA +111 -0
- mnemo_mcp_agent-0.3.2.dist-info/RECORD +32 -0
- mnemo_mcp_agent-0.3.2.dist-info/WHEEL +4 -0
- mnemo_mcp_agent-0.3.2.dist-info/entry_points.txt +2 -0
- mnemo_mcp_agent-0.3.2.dist-info/licenses/LICENSE +21 -0
mnemo/__init__.py
ADDED
mnemo/cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI package."""
|
mnemo/cli/main.py
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
"""Mnemo CLI — Borderless Matrix interface.
|
|
2
|
+
|
|
3
|
+
Commands:
|
|
4
|
+
init Initialise the memory database.
|
|
5
|
+
remember Store a new fact (AUDN pipeline).
|
|
6
|
+
search Hybrid 3-channel search.
|
|
7
|
+
invalidate Soft-delete a fact.
|
|
8
|
+
tier-decay Recompute all salience scores and migrate tiers.
|
|
9
|
+
debt Detect and display knowledge debt.
|
|
10
|
+
serve Start the MCP stdio server.
|
|
11
|
+
stats Show memory statistics.
|
|
12
|
+
visualize Generate interactive knowledge Sankey diagram.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import sys
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
import typer
|
|
21
|
+
from rich import box
|
|
22
|
+
from rich.console import Console
|
|
23
|
+
from rich.table import Table
|
|
24
|
+
|
|
25
|
+
# Ensure UTF-8 output encoding across Windows legacy consoles
|
|
26
|
+
if sys.platform == "win32":
|
|
27
|
+
try:
|
|
28
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
29
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
|
|
30
|
+
if hasattr(sys.stderr, "reconfigure"):
|
|
31
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
|
|
32
|
+
except Exception:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
app = typer.Typer(
|
|
36
|
+
name="mnemo",
|
|
37
|
+
help="Mnemo -- bitemporal long-term memory for AI agents.",
|
|
38
|
+
rich_markup_mode="rich",
|
|
39
|
+
no_args_is_help=True,
|
|
40
|
+
)
|
|
41
|
+
console = Console(highlight=False)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _safe_str(val: Any) -> str:
|
|
45
|
+
"""Format string safely for display."""
|
|
46
|
+
return str(val)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# init
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
@app.command()
|
|
53
|
+
def init(
|
|
54
|
+
db_path: str = typer.Option(
|
|
55
|
+
"",
|
|
56
|
+
"--db",
|
|
57
|
+
help="Path to SQLite database. Leave empty for default (~/.mnemo/memory.db).",
|
|
58
|
+
),
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Initialise (or re-initialise) the Mnemo memory database."""
|
|
61
|
+
from mnemo.storage.connection import Database, default_db_path
|
|
62
|
+
|
|
63
|
+
target = db_path if db_path else str(default_db_path())
|
|
64
|
+
Database(db_path=target)
|
|
65
|
+
console.print("→ [bold]mnemo init[/bold]")
|
|
66
|
+
console.print(f" [dim]database[/dim] {_safe_str(target)}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# remember
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
@app.command()
|
|
73
|
+
def remember(
|
|
74
|
+
text: str = typer.Argument(..., help="Fact text to store."),
|
|
75
|
+
category: str = typer.Option("general", "-c", "--category", help="Fact category."),
|
|
76
|
+
db_path: str = typer.Option("", "--db", help="Database path."),
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Store a new fact through the AUDN pipeline."""
|
|
79
|
+
from mnemo.core.models import AUDNOperation
|
|
80
|
+
from mnemo.engine.audn import AUDNClassifier
|
|
81
|
+
from mnemo.serialization.toon import encode_fact
|
|
82
|
+
from mnemo.storage.connection import Database, default_db_path
|
|
83
|
+
from mnemo.storage.vector_store import VectorStore, create_embedder
|
|
84
|
+
|
|
85
|
+
target = db_path if db_path else str(default_db_path())
|
|
86
|
+
db = Database(db_path=target)
|
|
87
|
+
vs = VectorStore(create_embedder())
|
|
88
|
+
audn = AUDNClassifier(vs)
|
|
89
|
+
|
|
90
|
+
with db.session() as conn:
|
|
91
|
+
op, existing_id = audn.classify(text, category, conn)
|
|
92
|
+
|
|
93
|
+
if op == AUDNOperation.ADD:
|
|
94
|
+
fact = audn.execute_add(text, category, conn)
|
|
95
|
+
console.print("→ [bold]mnemo remember[/bold] [green]add[/green]")
|
|
96
|
+
console.print(
|
|
97
|
+
f" [dim]fact[/dim] {encode_fact(fact.model_dump(exclude={'embedding'}))}"
|
|
98
|
+
)
|
|
99
|
+
elif op == AUDNOperation.UPDATE and existing_id:
|
|
100
|
+
fact = audn.execute_update(existing_id, text, category, conn)
|
|
101
|
+
console.print("→ [bold]mnemo remember[/bold] [yellow]update[/yellow]")
|
|
102
|
+
console.print(f" [dim]old[/dim] {existing_id[:8]}..")
|
|
103
|
+
console.print(
|
|
104
|
+
f" [dim]new[/dim] {encode_fact(fact.model_dump(exclude={'embedding'}))}"
|
|
105
|
+
)
|
|
106
|
+
elif op == AUDNOperation.NOOP and existing_id:
|
|
107
|
+
audn.execute_noop(existing_id, conn)
|
|
108
|
+
console.print("→ [bold]mnemo remember[/bold] [dim]noop[/dim]")
|
|
109
|
+
console.print(f" [dim]reinforced[/dim] {existing_id[:8]}..")
|
|
110
|
+
else:
|
|
111
|
+
console.print(f"→ [bold]mnemo remember[/bold] [dim]{op.value}[/dim]")
|
|
112
|
+
|
|
113
|
+
db.close()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
# search
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
@app.command()
|
|
120
|
+
def search(
|
|
121
|
+
query: str = typer.Argument(..., help="Search query."),
|
|
122
|
+
limit: int = typer.Option(10, "-n", "--limit", help="Max results."),
|
|
123
|
+
min_tier: str = typer.Option(
|
|
124
|
+
"archived", "--min-tier", help="Minimum tier: core, working, peripheral, archived."
|
|
125
|
+
),
|
|
126
|
+
db_path: str = typer.Option("", "--db", help="Database path."),
|
|
127
|
+
) -> None:
|
|
128
|
+
"""Hybrid 3-channel search (Vector + FTS5 + Graph + RRF)."""
|
|
129
|
+
from mnemo.core.models import MemoryTier
|
|
130
|
+
from mnemo.engine.retriever import HybridRetriever
|
|
131
|
+
from mnemo.storage.connection import Database, default_db_path
|
|
132
|
+
from mnemo.storage.fts_store import FTSStore
|
|
133
|
+
from mnemo.storage.graph_store import GraphStore
|
|
134
|
+
from mnemo.storage.vector_store import VectorStore, create_embedder
|
|
135
|
+
|
|
136
|
+
target = db_path if db_path else str(default_db_path())
|
|
137
|
+
db = Database(db_path=target)
|
|
138
|
+
vs = VectorStore(create_embedder())
|
|
139
|
+
retriever = HybridRetriever(vs, FTSStore(), GraphStore())
|
|
140
|
+
tier = MemoryTier(min_tier.lower())
|
|
141
|
+
|
|
142
|
+
with db.session() as conn:
|
|
143
|
+
results = retriever.search(query, conn, limit=limit, min_tier=tier)
|
|
144
|
+
|
|
145
|
+
if not results:
|
|
146
|
+
console.print("[dim]No matching memories found.[/dim]")
|
|
147
|
+
db.close()
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
table = Table(
|
|
151
|
+
title="→ [bold]Search Results[/bold]",
|
|
152
|
+
title_justify="left",
|
|
153
|
+
box=box.SIMPLE_HEAD,
|
|
154
|
+
show_header=True,
|
|
155
|
+
header_style="bold dim",
|
|
156
|
+
padding=(0, 1),
|
|
157
|
+
)
|
|
158
|
+
table.add_column("#", style="dim", width=4)
|
|
159
|
+
table.add_column("Score", justify="right", width=10)
|
|
160
|
+
table.add_column("Tier", width=10)
|
|
161
|
+
table.add_column("Category", width=12)
|
|
162
|
+
table.add_column("Text", ratio=1)
|
|
163
|
+
table.add_column("ID", style="dim", width=10)
|
|
164
|
+
|
|
165
|
+
for i, r in enumerate(results, 1):
|
|
166
|
+
tier_style = {
|
|
167
|
+
"core": "bold green",
|
|
168
|
+
"working": "bold yellow",
|
|
169
|
+
"peripheral": "dim cyan",
|
|
170
|
+
"archived": "dim",
|
|
171
|
+
}.get(r.fact.tier.value, "")
|
|
172
|
+
table.add_row(
|
|
173
|
+
str(i),
|
|
174
|
+
f"{r.score:.6f}",
|
|
175
|
+
f"[{tier_style}]{r.fact.tier.value}[/{tier_style}]",
|
|
176
|
+
r.fact.category,
|
|
177
|
+
r.fact.text[:80],
|
|
178
|
+
r.fact.id[:8] + "..",
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
console.print()
|
|
182
|
+
console.print(table)
|
|
183
|
+
console.print()
|
|
184
|
+
db.close()
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# ---------------------------------------------------------------------------
|
|
188
|
+
# invalidate
|
|
189
|
+
# ---------------------------------------------------------------------------
|
|
190
|
+
@app.command()
|
|
191
|
+
def invalidate(
|
|
192
|
+
fact_id: str = typer.Argument(..., help="UUID of the fact to invalidate."),
|
|
193
|
+
db_path: str = typer.Option("", "--db", help="Database path."),
|
|
194
|
+
) -> None:
|
|
195
|
+
"""Soft-delete a fact (close its valid_end)."""
|
|
196
|
+
from mnemo.engine.audn import AUDNClassifier
|
|
197
|
+
from mnemo.storage.connection import Database, default_db_path
|
|
198
|
+
from mnemo.storage.vector_store import VectorStore, create_embedder
|
|
199
|
+
|
|
200
|
+
target = db_path if db_path else str(default_db_path())
|
|
201
|
+
db = Database(db_path=target)
|
|
202
|
+
vs = VectorStore(create_embedder())
|
|
203
|
+
audn = AUDNClassifier(vs)
|
|
204
|
+
|
|
205
|
+
with db.session() as conn:
|
|
206
|
+
audn.execute_delete(fact_id, conn)
|
|
207
|
+
|
|
208
|
+
console.print("→ [bold]mnemo invalidate[/bold]")
|
|
209
|
+
console.print(f" [dim]invalidated[/dim] [red]{fact_id[:8]}..[/red]")
|
|
210
|
+
db.close()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# ---------------------------------------------------------------------------
|
|
214
|
+
# tier-decay
|
|
215
|
+
# ---------------------------------------------------------------------------
|
|
216
|
+
@app.command("tier-decay")
|
|
217
|
+
def tier_decay(
|
|
218
|
+
db_path: str = typer.Option("", "--db", help="Database path."),
|
|
219
|
+
) -> None:
|
|
220
|
+
"""Recompute all salience scores and migrate tiers."""
|
|
221
|
+
from mnemo.engine.tier_manager import TierManager
|
|
222
|
+
from mnemo.storage.connection import Database, default_db_path
|
|
223
|
+
|
|
224
|
+
target = db_path if db_path else str(default_db_path())
|
|
225
|
+
db = Database(db_path=target)
|
|
226
|
+
mgr = TierManager()
|
|
227
|
+
|
|
228
|
+
with db.session() as conn:
|
|
229
|
+
result = mgr.decay_all(conn)
|
|
230
|
+
|
|
231
|
+
console.print("→ [bold]mnemo tier-decay[/bold]")
|
|
232
|
+
console.print(f" [dim]processed[/dim] [green]{result['processed']}[/green]")
|
|
233
|
+
console.print(f" [dim]migrated[/dim] [yellow]{result['migrated']}[/yellow]")
|
|
234
|
+
db.close()
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# ---------------------------------------------------------------------------
|
|
238
|
+
# debt
|
|
239
|
+
# ---------------------------------------------------------------------------
|
|
240
|
+
@app.command()
|
|
241
|
+
def debt(
|
|
242
|
+
db_path: str = typer.Option("", "--db", help="Database path."),
|
|
243
|
+
) -> None:
|
|
244
|
+
"""Detect and display knowledge / architectural debt."""
|
|
245
|
+
from mnemo.engine.tier_manager import TierManager
|
|
246
|
+
from mnemo.storage.connection import Database, default_db_path
|
|
247
|
+
|
|
248
|
+
target = db_path if db_path else str(default_db_path())
|
|
249
|
+
db = Database(db_path=target)
|
|
250
|
+
mgr = TierManager()
|
|
251
|
+
|
|
252
|
+
with db.session() as conn:
|
|
253
|
+
mgr.decay_all(conn)
|
|
254
|
+
items = mgr.detect_debt(conn)
|
|
255
|
+
if items:
|
|
256
|
+
mgr.persist_debt(items, conn)
|
|
257
|
+
|
|
258
|
+
if not items:
|
|
259
|
+
console.print("→ [bold]mnemo debt[/bold]")
|
|
260
|
+
console.print(" [green]No debt detected.[/green]")
|
|
261
|
+
db.close()
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
table = Table(
|
|
265
|
+
title="→ [bold]Debt Ledger[/bold]",
|
|
266
|
+
title_justify="left",
|
|
267
|
+
box=box.SIMPLE_HEAD,
|
|
268
|
+
show_header=True,
|
|
269
|
+
header_style="bold dim",
|
|
270
|
+
padding=(0, 1),
|
|
271
|
+
)
|
|
272
|
+
table.add_column("Ceiling", width=10)
|
|
273
|
+
table.add_column("Trigger", width=20)
|
|
274
|
+
table.add_column("Context", ratio=1)
|
|
275
|
+
|
|
276
|
+
for item in items:
|
|
277
|
+
ceiling_style = {
|
|
278
|
+
"critical": "bold red",
|
|
279
|
+
"high": "red",
|
|
280
|
+
"medium": "yellow",
|
|
281
|
+
"low": "dim",
|
|
282
|
+
}.get(item.ceiling, "")
|
|
283
|
+
table.add_row(
|
|
284
|
+
f"[{ceiling_style}]{item.ceiling}[/{ceiling_style}]",
|
|
285
|
+
item.trigger,
|
|
286
|
+
item.code_context,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
console.print()
|
|
290
|
+
console.print(table)
|
|
291
|
+
console.print()
|
|
292
|
+
db.close()
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
# ---------------------------------------------------------------------------
|
|
296
|
+
# serve
|
|
297
|
+
# ---------------------------------------------------------------------------
|
|
298
|
+
@app.command()
|
|
299
|
+
def serve() -> None:
|
|
300
|
+
"""Start the Mnemo MCP server (stdio transport)."""
|
|
301
|
+
err_console = Console(stderr=True, highlight=False)
|
|
302
|
+
err_console.print("→ [bold]mnemo serve[/bold]")
|
|
303
|
+
err_console.print(" [dim]transport[/dim] stdio")
|
|
304
|
+
err_console.print(" [dim]status[/dim] running (press Ctrl+C to stop)")
|
|
305
|
+
from mnemo.mcp.server import run_server
|
|
306
|
+
|
|
307
|
+
run_server()
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# ---------------------------------------------------------------------------
|
|
311
|
+
# stats
|
|
312
|
+
# ---------------------------------------------------------------------------
|
|
313
|
+
@app.command()
|
|
314
|
+
def stats(
|
|
315
|
+
db_path: str = typer.Option("", "--db", help="Database path."),
|
|
316
|
+
) -> None:
|
|
317
|
+
"""Show memory store statistics."""
|
|
318
|
+
import time
|
|
319
|
+
|
|
320
|
+
from mnemo.storage.connection import Database, default_db_path
|
|
321
|
+
|
|
322
|
+
target = db_path if db_path else str(default_db_path())
|
|
323
|
+
db = Database(db_path=target)
|
|
324
|
+
now = time.time()
|
|
325
|
+
|
|
326
|
+
with db.session() as conn:
|
|
327
|
+
total = conn.execute("SELECT COUNT(*) AS c FROM facts WHERE ingest_end IS NULL").fetchone()
|
|
328
|
+
active = conn.execute(
|
|
329
|
+
"SELECT COUNT(*) AS c FROM facts WHERE ingest_end IS NULL "
|
|
330
|
+
"AND (valid_end IS NULL OR valid_end > ?)",
|
|
331
|
+
(now,),
|
|
332
|
+
).fetchone()
|
|
333
|
+
tiers = conn.execute(
|
|
334
|
+
"SELECT tier, COUNT(*) AS c FROM facts "
|
|
335
|
+
"WHERE ingest_end IS NULL AND (valid_end IS NULL OR valid_end > ?) "
|
|
336
|
+
"GROUP BY tier ORDER BY tier",
|
|
337
|
+
(now,),
|
|
338
|
+
).fetchall()
|
|
339
|
+
entities_count = conn.execute(
|
|
340
|
+
"SELECT COUNT(*) AS c FROM entities WHERE ingest_end IS NULL "
|
|
341
|
+
"AND (valid_end IS NULL OR valid_end > ?)",
|
|
342
|
+
(now,),
|
|
343
|
+
).fetchone()
|
|
344
|
+
relations_count = conn.execute(
|
|
345
|
+
"SELECT COUNT(*) AS c FROM relations WHERE ingest_end IS NULL "
|
|
346
|
+
"AND (valid_end IS NULL OR valid_end > ?)",
|
|
347
|
+
(now,),
|
|
348
|
+
).fetchone()
|
|
349
|
+
debt_count = conn.execute("SELECT COUNT(*) AS c FROM debt_ledger").fetchone()
|
|
350
|
+
|
|
351
|
+
table = Table(
|
|
352
|
+
title="→ [bold]Mnemo Memory Stats[/bold]",
|
|
353
|
+
title_justify="left",
|
|
354
|
+
box=box.SIMPLE_HEAD,
|
|
355
|
+
show_header=True,
|
|
356
|
+
header_style="bold dim",
|
|
357
|
+
padding=(0, 1),
|
|
358
|
+
)
|
|
359
|
+
table.add_column("Metric", style="bold", ratio=1)
|
|
360
|
+
table.add_column("Value", justify="right", width=14)
|
|
361
|
+
|
|
362
|
+
table.add_row("Total facts (all versions)", str(total["c"]))
|
|
363
|
+
table.add_row("Active facts", str(active["c"]))
|
|
364
|
+
for t in tiers:
|
|
365
|
+
table.add_row(f" · Tier: {t['tier']}", str(t["c"]))
|
|
366
|
+
table.add_row("Active entities", str(entities_count["c"]))
|
|
367
|
+
table.add_row("Active relations", str(relations_count["c"]))
|
|
368
|
+
table.add_row("Debt ledger items", str(debt_count["c"]))
|
|
369
|
+
table.add_row("Database", _safe_str(target))
|
|
370
|
+
|
|
371
|
+
console.print()
|
|
372
|
+
console.print(table)
|
|
373
|
+
console.print()
|
|
374
|
+
db.close()
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
# ---------------------------------------------------------------------------
|
|
378
|
+
# visualize
|
|
379
|
+
# ---------------------------------------------------------------------------
|
|
380
|
+
@app.command()
|
|
381
|
+
def visualize(
|
|
382
|
+
output: str = typer.Option(
|
|
383
|
+
"./mnemo_graph.html",
|
|
384
|
+
"-o",
|
|
385
|
+
"--output",
|
|
386
|
+
help="Path to output HTML file.",
|
|
387
|
+
),
|
|
388
|
+
open_browser: bool = typer.Option(
|
|
389
|
+
True,
|
|
390
|
+
"--open/--no-open",
|
|
391
|
+
help="Automatically open generated HTML visualization in default browser.",
|
|
392
|
+
),
|
|
393
|
+
format_type: str = typer.Option(
|
|
394
|
+
"html",
|
|
395
|
+
"--format",
|
|
396
|
+
"-f",
|
|
397
|
+
help="Visualization format: 'html' (interactive Sankey) or 'mermaid' (terminal output).",
|
|
398
|
+
),
|
|
399
|
+
db_path: str = typer.Option("", "--db", help="Database path."),
|
|
400
|
+
) -> None:
|
|
401
|
+
"""Generate an interactive Dark Minimal Sankey flow diagram of the memory graph."""
|
|
402
|
+
import webbrowser
|
|
403
|
+
from pathlib import Path
|
|
404
|
+
|
|
405
|
+
from mnemo.cli.visualize import (
|
|
406
|
+
export_knowledge_sankey_data,
|
|
407
|
+
generate_html_report,
|
|
408
|
+
generate_mermaid_sankey,
|
|
409
|
+
)
|
|
410
|
+
from mnemo.storage.connection import Database, default_db_path
|
|
411
|
+
|
|
412
|
+
target_db = db_path if db_path else str(default_db_path())
|
|
413
|
+
db = Database(db_path=target_db)
|
|
414
|
+
|
|
415
|
+
with db.session() as conn:
|
|
416
|
+
data = export_knowledge_sankey_data(conn)
|
|
417
|
+
|
|
418
|
+
db.close()
|
|
419
|
+
|
|
420
|
+
if format_type.lower() == "mermaid":
|
|
421
|
+
mermaid_code = generate_mermaid_sankey(data)
|
|
422
|
+
console.print(mermaid_code)
|
|
423
|
+
return
|
|
424
|
+
|
|
425
|
+
html_content = generate_html_report(data)
|
|
426
|
+
out_path = Path(output).resolve()
|
|
427
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
428
|
+
out_path.write_text(html_content, encoding="utf-8")
|
|
429
|
+
|
|
430
|
+
console.print("→ [bold]mnemo visualize[/bold]")
|
|
431
|
+
console.print(f" [dim]nodes[/dim] {len(data['nodes'])}")
|
|
432
|
+
console.print(f" [dim]active flows[/dim] {len(data['values'])}")
|
|
433
|
+
console.print(f" [dim]core memory[/dim] {data['metrics']['core_pct']}%")
|
|
434
|
+
console.print()
|
|
435
|
+
console.print(f'the result is available by path "{_safe_str(out_path)}"')
|
|
436
|
+
|
|
437
|
+
if open_browser:
|
|
438
|
+
webbrowser.open(out_path.as_uri())
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
# ---------------------------------------------------------------------------
|
|
442
|
+
# doctor
|
|
443
|
+
# ---------------------------------------------------------------------------
|
|
444
|
+
@app.command()
|
|
445
|
+
def doctor(
|
|
446
|
+
db_path: str = typer.Option("", "--db", help="Database path."),
|
|
447
|
+
) -> None:
|
|
448
|
+
"""Diagnose database health, FTS5 index integrity, and graph statistics."""
|
|
449
|
+
from mnemo.storage.connection import Database, default_db_path
|
|
450
|
+
|
|
451
|
+
target = db_path if db_path else str(default_db_path())
|
|
452
|
+
db = Database(db_path=target)
|
|
453
|
+
health = db.check_integrity()
|
|
454
|
+
|
|
455
|
+
status_color = "green" if health["integrity_ok"] else "red"
|
|
456
|
+
fts_color = "green" if health["fts_in_sync"] else "yellow"
|
|
457
|
+
|
|
458
|
+
console.print("→ [bold]mnemo doctor[/bold]")
|
|
459
|
+
console.print(f" [dim]database[/dim] {_safe_str(target)}")
|
|
460
|
+
console.print(
|
|
461
|
+
f" [dim]schema version[/dim] v{health['schema_version']} (target v{health['target_version']})"
|
|
462
|
+
)
|
|
463
|
+
console.print(
|
|
464
|
+
f" [dim]sqlite integrity[/dim][{status_color}] {'ok' if health['integrity_ok'] else 'corrupted'}[/{status_color}]"
|
|
465
|
+
)
|
|
466
|
+
console.print(f" [dim]journal mode[/dim] {health['journal_mode']}")
|
|
467
|
+
console.print(
|
|
468
|
+
f" [dim]fts5 sync[/dim] [{fts_color}] {'synchronized' if health['fts_in_sync'] else 'repaired'}[/{fts_color}]"
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
counts = health.get("table_counts", {})
|
|
472
|
+
console.print(f" [dim]active facts[/dim] {counts.get('facts', 0)}")
|
|
473
|
+
console.print(f" [dim]entities[/dim] {counts.get('entities', 0)}")
|
|
474
|
+
console.print(f" [dim]relations[/dim] {counts.get('relations', 0)}")
|
|
475
|
+
console.print(f" [dim]cached files[/dim] {counts.get('file_scan_cache', 0)}")
|
|
476
|
+
db.close()
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
# ---------------------------------------------------------------------------
|
|
480
|
+
# scan
|
|
481
|
+
# ---------------------------------------------------------------------------
|
|
482
|
+
@app.command()
|
|
483
|
+
def scan(
|
|
484
|
+
path: str = typer.Argument(".", help="Project directory path to scan."),
|
|
485
|
+
db_path: str = typer.Option("", "--db", help="Database path."),
|
|
486
|
+
force: bool = typer.Option(False, "--force", "-f", help="Force re-scan all files."),
|
|
487
|
+
) -> None:
|
|
488
|
+
"""Perform an incremental AST scan to extract modules, classes, and dependencies."""
|
|
489
|
+
from mnemo.engine.scanner import ProjectScanner
|
|
490
|
+
from mnemo.storage.connection import Database, default_db_path
|
|
491
|
+
|
|
492
|
+
target = db_path if db_path else str(default_db_path())
|
|
493
|
+
db = Database(db_path=target)
|
|
494
|
+
scanner = ProjectScanner(root_path=path)
|
|
495
|
+
|
|
496
|
+
with db.session() as conn:
|
|
497
|
+
res = scanner.scan(conn, force=force)
|
|
498
|
+
|
|
499
|
+
console.print("→ [bold]mnemo scan[/bold]")
|
|
500
|
+
console.print(f" [dim]scanned files[/dim] {res['scanned_files']}")
|
|
501
|
+
console.print(f" [dim]skipped files[/dim] {res['skipped_files']}")
|
|
502
|
+
console.print(f" [dim]entities added[/dim] {res['entities_added']}")
|
|
503
|
+
console.print(f" [dim]relations added[/dim] {res['relations_added']}")
|
|
504
|
+
db.close()
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
# ---------------------------------------------------------------------------
|
|
508
|
+
# Entry point
|
|
509
|
+
# ---------------------------------------------------------------------------
|
|
510
|
+
|
|
511
|
+
if __name__ == "__main__":
|
|
512
|
+
app()
|