mnemos-dev 0.1.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.
- mnemos/__init__.py +2 -0
- mnemos/__main__.py +23 -0
- mnemos/cli.py +329 -0
- mnemos/config.py +190 -0
- mnemos/graph.py +228 -0
- mnemos/miner.py +283 -0
- mnemos/obsidian.py +138 -0
- mnemos/palace.py +304 -0
- mnemos/patterns/__init__.py +0 -0
- mnemos/patterns/base.yaml +6 -0
- mnemos/patterns/en.yaml +29 -0
- mnemos/patterns/tr.yaml +31 -0
- mnemos/search.py +164 -0
- mnemos/server.py +497 -0
- mnemos/stack.py +125 -0
- mnemos/watcher.py +174 -0
- mnemos_dev-0.1.0.dist-info/METADATA +215 -0
- mnemos_dev-0.1.0.dist-info/RECORD +21 -0
- mnemos_dev-0.1.0.dist-info/WHEEL +4 -0
- mnemos_dev-0.1.0.dist-info/entry_points.txt +2 -0
- mnemos_dev-0.1.0.dist-info/licenses/LICENSE +21 -0
mnemos/__init__.py
ADDED
mnemos/__main__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Entry point for running Mnemos as a module: python -m mnemos.server"""
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def main() -> None:
|
|
6
|
+
# Parse --vault before heavy imports
|
|
7
|
+
vault_path = None
|
|
8
|
+
args = sys.argv[1:]
|
|
9
|
+
if "--vault" in args:
|
|
10
|
+
idx = args.index("--vault")
|
|
11
|
+
if idx + 1 < len(args):
|
|
12
|
+
vault_path = args[idx + 1]
|
|
13
|
+
|
|
14
|
+
from mnemos.config import load_config
|
|
15
|
+
config = load_config(vault_path)
|
|
16
|
+
|
|
17
|
+
from mnemos.server import create_mcp_server
|
|
18
|
+
mcp = create_mcp_server(config)
|
|
19
|
+
mcp.run(transport="stdio")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if __name__ == "__main__":
|
|
23
|
+
main()
|
mnemos/cli.py
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""Mnemos CLI — init, mine, search, status commands."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
from mnemos.config import load_config, HALLS_DEFAULT, WATCHER_IGNORE_DEFAULT
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# Helpers
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _resolve_vault(args_vault: Optional[str]) -> str:
|
|
21
|
+
"""Return vault path: CLI flag > env var > empty string."""
|
|
22
|
+
if args_vault:
|
|
23
|
+
return str(Path(args_vault).expanduser().resolve())
|
|
24
|
+
import os
|
|
25
|
+
return os.environ.get("MNEMOS_VAULT", "")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _require_vault(vault_path: str, cmd: str) -> None:
|
|
29
|
+
"""Exit with a helpful message if vault_path is empty."""
|
|
30
|
+
if not vault_path:
|
|
31
|
+
sys.exit(
|
|
32
|
+
f"[mnemos {cmd}] No vault path found.\n"
|
|
33
|
+
"Pass --vault <path>, set MNEMOS_VAULT env var, "
|
|
34
|
+
"or run `mnemos init` first."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
# cmd_init
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def cmd_init(args: argparse.Namespace) -> None:
|
|
44
|
+
"""Interactive wizard: scaffold a Mnemos vault."""
|
|
45
|
+
print("=== Mnemos Init Wizard ===\n")
|
|
46
|
+
|
|
47
|
+
# --- Vault path ---
|
|
48
|
+
if args.vault:
|
|
49
|
+
vault_path = str(Path(args.vault).expanduser().resolve())
|
|
50
|
+
print(f"Using vault path: {vault_path}")
|
|
51
|
+
else:
|
|
52
|
+
raw = input("Vault path (Obsidian vault root): ").strip()
|
|
53
|
+
if not raw:
|
|
54
|
+
sys.exit("Vault path cannot be empty.")
|
|
55
|
+
vault_path = str(Path(raw).expanduser().resolve())
|
|
56
|
+
|
|
57
|
+
vault_dir = Path(vault_path)
|
|
58
|
+
if not vault_dir.exists():
|
|
59
|
+
create = input(f" Directory does not exist. Create it? [y/N] ").strip().lower()
|
|
60
|
+
if create == "y":
|
|
61
|
+
vault_dir.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
print(f" Created: {vault_dir}")
|
|
63
|
+
else:
|
|
64
|
+
sys.exit("Aborted.")
|
|
65
|
+
|
|
66
|
+
# --- Languages ---
|
|
67
|
+
raw_langs = input("Languages (comma-separated, e.g. en,tr) [en]: ").strip()
|
|
68
|
+
languages = [l.strip() for l in raw_langs.split(",") if l.strip()] if raw_langs else ["en"]
|
|
69
|
+
|
|
70
|
+
# --- LLM ---
|
|
71
|
+
use_llm_raw = input("Enable LLM-assisted mining? [y/N]: ").strip().lower()
|
|
72
|
+
use_llm = use_llm_raw == "y"
|
|
73
|
+
|
|
74
|
+
# --- Build config ---
|
|
75
|
+
config_data: dict = {
|
|
76
|
+
"vault_path": vault_path,
|
|
77
|
+
"languages": languages,
|
|
78
|
+
"use_llm": use_llm,
|
|
79
|
+
"halls": list(HALLS_DEFAULT),
|
|
80
|
+
"watcher_ignore": list(WATCHER_IGNORE_DEFAULT),
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# --- Write mnemos.yaml ---
|
|
84
|
+
yaml_path = vault_dir / "mnemos.yaml"
|
|
85
|
+
if yaml_path.exists():
|
|
86
|
+
overwrite = input(f"\n mnemos.yaml already exists. Overwrite? [y/N]: ").strip().lower()
|
|
87
|
+
if overwrite != "y":
|
|
88
|
+
print(" Keeping existing mnemos.yaml.")
|
|
89
|
+
else:
|
|
90
|
+
yaml_path.write_text(yaml.dump(config_data, allow_unicode=True), encoding="utf-8")
|
|
91
|
+
print(f" Wrote: {yaml_path}")
|
|
92
|
+
else:
|
|
93
|
+
yaml_path.write_text(yaml.dump(config_data, allow_unicode=True), encoding="utf-8")
|
|
94
|
+
print(f"\n Wrote: {yaml_path}")
|
|
95
|
+
|
|
96
|
+
# --- Create palace structure ---
|
|
97
|
+
from mnemos.config import MnemosConfig
|
|
98
|
+
from mnemos.palace import Palace
|
|
99
|
+
|
|
100
|
+
cfg = MnemosConfig(
|
|
101
|
+
vault_path=vault_path,
|
|
102
|
+
languages=languages,
|
|
103
|
+
use_llm=use_llm,
|
|
104
|
+
)
|
|
105
|
+
palace = Palace(cfg)
|
|
106
|
+
palace.ensure_structure()
|
|
107
|
+
print(f" Created palace structure at: {cfg.palace_dir}")
|
|
108
|
+
|
|
109
|
+
# --- Identity placeholder ---
|
|
110
|
+
identity_file = cfg.identity_full_path / "L0-identity.md"
|
|
111
|
+
if not identity_file.exists():
|
|
112
|
+
identity_file.write_text(
|
|
113
|
+
"---\ntype: identity\nlevel: L0\n---\n\n"
|
|
114
|
+
"# Identity\n\n"
|
|
115
|
+
"This is your Mnemos identity file. "
|
|
116
|
+
"Describe yourself, your goals, and your preferences here.\n",
|
|
117
|
+
encoding="utf-8",
|
|
118
|
+
)
|
|
119
|
+
print(f" Created identity placeholder: {identity_file}")
|
|
120
|
+
|
|
121
|
+
# --- Offer to mine existing files ---
|
|
122
|
+
print()
|
|
123
|
+
mine_now = input("Mine existing markdown files in the vault now? [y/N]: ").strip().lower()
|
|
124
|
+
if mine_now == "y":
|
|
125
|
+
print(" Mining vault (this may take a while)...")
|
|
126
|
+
from mnemos.server import MnemosApp
|
|
127
|
+
|
|
128
|
+
app = MnemosApp(cfg)
|
|
129
|
+
result = app.handle_mine(path=vault_path, use_llm=use_llm)
|
|
130
|
+
print(
|
|
131
|
+
f" Done — scanned: {result['files_scanned']}, "
|
|
132
|
+
f"drawers: {result['drawers_created']}, "
|
|
133
|
+
f"entities: {result['entities_found']}, "
|
|
134
|
+
f"skipped: {result['skipped']}"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# --- MCP connection instructions ---
|
|
138
|
+
print(
|
|
139
|
+
"\n=== MCP Connection ===\n"
|
|
140
|
+
"Add the following to your Claude Desktop / Cursor MCP config:\n\n"
|
|
141
|
+
' "mnemos": {\n'
|
|
142
|
+
' "command": "mnemos",\n'
|
|
143
|
+
' "args": ["serve"],\n'
|
|
144
|
+
f' "env": {{"MNEMOS_VAULT": "{vault_path}"}}\n'
|
|
145
|
+
" }\n\n"
|
|
146
|
+
"Then restart your MCP client.\n"
|
|
147
|
+
"\nSetup complete!"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
# cmd_mine
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def cmd_mine(args: argparse.Namespace) -> None:
|
|
157
|
+
"""Mine a file or directory and print results as JSON."""
|
|
158
|
+
vault_path = _resolve_vault(args.vault)
|
|
159
|
+
_require_vault(vault_path, "mine")
|
|
160
|
+
|
|
161
|
+
cfg = load_config(vault_path)
|
|
162
|
+
|
|
163
|
+
from mnemos.server import MnemosApp
|
|
164
|
+
|
|
165
|
+
app = MnemosApp(cfg)
|
|
166
|
+
result = app.handle_mine(
|
|
167
|
+
path=args.path,
|
|
168
|
+
use_llm=args.llm,
|
|
169
|
+
)
|
|
170
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ---------------------------------------------------------------------------
|
|
174
|
+
# cmd_search
|
|
175
|
+
# ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def cmd_search(args: argparse.Namespace) -> None:
|
|
179
|
+
"""Search the memory palace and print formatted results."""
|
|
180
|
+
vault_path = _resolve_vault(args.vault)
|
|
181
|
+
_require_vault(vault_path, "search")
|
|
182
|
+
|
|
183
|
+
cfg = load_config(vault_path)
|
|
184
|
+
|
|
185
|
+
from mnemos.server import MnemosApp
|
|
186
|
+
|
|
187
|
+
app = MnemosApp(cfg)
|
|
188
|
+
results = app.handle_search(
|
|
189
|
+
query=args.query,
|
|
190
|
+
wing=args.wing,
|
|
191
|
+
hall=args.hall,
|
|
192
|
+
limit=args.limit,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
if not results:
|
|
196
|
+
print("No results found.")
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
for i, r in enumerate(results, 1):
|
|
200
|
+
score = r.get("score", 0.0)
|
|
201
|
+
wing = r.get("wing", "?")
|
|
202
|
+
hall = r.get("hall", "?")
|
|
203
|
+
text = r.get("text", "").strip()
|
|
204
|
+
# Truncate long texts for readability
|
|
205
|
+
preview = text[:200] + "..." if len(text) > 200 else text
|
|
206
|
+
print(f"[{i}] score={score:.3f} wing={wing} hall={hall}")
|
|
207
|
+
print(f" {preview}")
|
|
208
|
+
print()
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
# cmd_status
|
|
213
|
+
# ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def cmd_status(args: argparse.Namespace) -> None:
|
|
217
|
+
"""Print memory palace status as JSON."""
|
|
218
|
+
vault_path = _resolve_vault(args.vault)
|
|
219
|
+
_require_vault(vault_path, "status")
|
|
220
|
+
|
|
221
|
+
cfg = load_config(vault_path)
|
|
222
|
+
|
|
223
|
+
from mnemos.server import MnemosApp
|
|
224
|
+
|
|
225
|
+
app = MnemosApp(cfg)
|
|
226
|
+
result = app.handle_status()
|
|
227
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# ---------------------------------------------------------------------------
|
|
231
|
+
# main — argparse entrypoint
|
|
232
|
+
# ---------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def main() -> None:
|
|
236
|
+
parser = argparse.ArgumentParser(
|
|
237
|
+
prog="mnemos",
|
|
238
|
+
description="Mnemos — Obsidian-native AI memory palace",
|
|
239
|
+
)
|
|
240
|
+
parser.add_argument(
|
|
241
|
+
"--vault",
|
|
242
|
+
metavar="PATH",
|
|
243
|
+
default=None,
|
|
244
|
+
help="Path to the Obsidian vault root (overrides MNEMOS_VAULT env var)",
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
subparsers = parser.add_subparsers(dest="command", metavar="<command>")
|
|
248
|
+
subparsers.required = False
|
|
249
|
+
|
|
250
|
+
# ------------------------------------------------------------------
|
|
251
|
+
# init
|
|
252
|
+
# ------------------------------------------------------------------
|
|
253
|
+
parser_init = subparsers.add_parser(
|
|
254
|
+
"init",
|
|
255
|
+
help="Interactive wizard: scaffold a Mnemos vault",
|
|
256
|
+
)
|
|
257
|
+
parser_init.set_defaults(func=cmd_init)
|
|
258
|
+
|
|
259
|
+
# ------------------------------------------------------------------
|
|
260
|
+
# mine
|
|
261
|
+
# ------------------------------------------------------------------
|
|
262
|
+
parser_mine = subparsers.add_parser(
|
|
263
|
+
"mine",
|
|
264
|
+
help="Mine a file or directory and extract memory fragments",
|
|
265
|
+
)
|
|
266
|
+
parser_mine.add_argument(
|
|
267
|
+
"path",
|
|
268
|
+
help="File or directory to mine",
|
|
269
|
+
)
|
|
270
|
+
parser_mine.add_argument(
|
|
271
|
+
"--llm",
|
|
272
|
+
action="store_true",
|
|
273
|
+
default=False,
|
|
274
|
+
help="Use LLM-assisted extraction (requires anthropic package)",
|
|
275
|
+
)
|
|
276
|
+
parser_mine.set_defaults(func=cmd_mine)
|
|
277
|
+
|
|
278
|
+
# ------------------------------------------------------------------
|
|
279
|
+
# search
|
|
280
|
+
# ------------------------------------------------------------------
|
|
281
|
+
parser_search = subparsers.add_parser(
|
|
282
|
+
"search",
|
|
283
|
+
help="Semantic search over the memory palace",
|
|
284
|
+
)
|
|
285
|
+
parser_search.add_argument(
|
|
286
|
+
"query",
|
|
287
|
+
help="Search query",
|
|
288
|
+
)
|
|
289
|
+
parser_search.add_argument(
|
|
290
|
+
"--wing",
|
|
291
|
+
default=None,
|
|
292
|
+
help="Filter results to this wing",
|
|
293
|
+
)
|
|
294
|
+
parser_search.add_argument(
|
|
295
|
+
"--hall",
|
|
296
|
+
default=None,
|
|
297
|
+
help="Filter results to this hall (e.g. facts, decisions)",
|
|
298
|
+
)
|
|
299
|
+
parser_search.add_argument(
|
|
300
|
+
"--limit",
|
|
301
|
+
type=int,
|
|
302
|
+
default=5,
|
|
303
|
+
help="Maximum number of results (default: 5)",
|
|
304
|
+
)
|
|
305
|
+
parser_search.set_defaults(func=cmd_search)
|
|
306
|
+
|
|
307
|
+
# ------------------------------------------------------------------
|
|
308
|
+
# status
|
|
309
|
+
# ------------------------------------------------------------------
|
|
310
|
+
parser_status = subparsers.add_parser(
|
|
311
|
+
"status",
|
|
312
|
+
help="Show memory palace status",
|
|
313
|
+
)
|
|
314
|
+
parser_status.set_defaults(func=cmd_status)
|
|
315
|
+
|
|
316
|
+
# ------------------------------------------------------------------
|
|
317
|
+
# Dispatch
|
|
318
|
+
# ------------------------------------------------------------------
|
|
319
|
+
args = parser.parse_args()
|
|
320
|
+
|
|
321
|
+
if not hasattr(args, "func"):
|
|
322
|
+
parser.print_help()
|
|
323
|
+
sys.exit(0)
|
|
324
|
+
|
|
325
|
+
args.func(args)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
if __name__ == "__main__":
|
|
329
|
+
main()
|
mnemos/config.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Mnemos configuration — loads mnemos.yaml from vault root, falls back to defaults."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
# Defaults
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
HALLS_DEFAULT: List[str] = ["decisions", "facts", "events", "preferences", "problems"]
|
|
16
|
+
|
|
17
|
+
WATCHER_IGNORE_DEFAULT: List[str] = [
|
|
18
|
+
".obsidian/",
|
|
19
|
+
"Mnemos/_recycled/",
|
|
20
|
+
"*.canvas",
|
|
21
|
+
"Templates/",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# Sub-dataclasses
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class MiningSource:
|
|
32
|
+
"""A source path to mine for memory fragments."""
|
|
33
|
+
|
|
34
|
+
path: str
|
|
35
|
+
mode: str = "session" # session | topic | chat
|
|
36
|
+
external: bool = False # True = outside vault, read-only, no watcher
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
# Main config dataclass
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class MnemosConfig:
|
|
46
|
+
"""All configuration for a Mnemos instance."""
|
|
47
|
+
|
|
48
|
+
# Core paths
|
|
49
|
+
vault_path: str = ""
|
|
50
|
+
palace_root: str = "Mnemos"
|
|
51
|
+
recycled_dir: str = "_recycled"
|
|
52
|
+
identity_dir: str = "_identity"
|
|
53
|
+
|
|
54
|
+
# Language
|
|
55
|
+
languages: List[str] = field(default_factory=lambda: ["en"])
|
|
56
|
+
|
|
57
|
+
# LLM
|
|
58
|
+
use_llm: bool = False
|
|
59
|
+
llm_model: str = "claude-3-5-haiku-20241022"
|
|
60
|
+
|
|
61
|
+
# Mining
|
|
62
|
+
mining_sources: List[MiningSource] = field(default_factory=list)
|
|
63
|
+
|
|
64
|
+
# Search
|
|
65
|
+
search_limit: int = 10
|
|
66
|
+
rerank: bool = False
|
|
67
|
+
rerank_model: str = ""
|
|
68
|
+
|
|
69
|
+
# Watcher
|
|
70
|
+
watcher_enabled: bool = True
|
|
71
|
+
watcher_ignore: List[str] = field(default_factory=lambda: list(WATCHER_IGNORE_DEFAULT))
|
|
72
|
+
|
|
73
|
+
# Halls (memory categories)
|
|
74
|
+
halls: List[str] = field(default_factory=lambda: list(HALLS_DEFAULT))
|
|
75
|
+
|
|
76
|
+
# Internal paths (relative to vault_path / palace_root)
|
|
77
|
+
chromadb_path: str = ".chroma"
|
|
78
|
+
graph_path: str = "graph.json"
|
|
79
|
+
mine_log_path: str = "mine.log"
|
|
80
|
+
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
# Derived path properties
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def palace_dir(self) -> Path:
|
|
87
|
+
"""Absolute path to the palace root inside the vault."""
|
|
88
|
+
return Path(self.vault_path) / self.palace_root
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def wings_dir(self) -> Path:
|
|
92
|
+
"""Absolute path to the wings directory (where halls live)."""
|
|
93
|
+
return self.palace_dir / "wings"
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def recycled_full_path(self) -> Path:
|
|
97
|
+
"""Absolute path to the recycled fragments directory."""
|
|
98
|
+
return self.palace_dir / self.recycled_dir
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def identity_full_path(self) -> Path:
|
|
102
|
+
"""Absolute path to the identity directory."""
|
|
103
|
+
return self.palace_dir / self.identity_dir
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def chromadb_full_path(self) -> Path:
|
|
107
|
+
"""Absolute path to the ChromaDB data directory."""
|
|
108
|
+
return self.palace_dir / self.chromadb_path
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def graph_full_path(self) -> Path:
|
|
112
|
+
"""Absolute path to the graph JSON file."""
|
|
113
|
+
return self.palace_dir / self.graph_path
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def mine_log_full_path(self) -> Path:
|
|
117
|
+
"""Absolute path to the mine log file."""
|
|
118
|
+
return self.palace_dir / self.mine_log_path
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
# Loader
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def load_config(vault_path: Optional[str] = None) -> MnemosConfig:
|
|
127
|
+
"""Load MnemosConfig from mnemos.yaml in vault root.
|
|
128
|
+
|
|
129
|
+
Falls back to defaults when the file does not exist or a key is absent.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
vault_path: Explicit vault path. If None, reads MNEMOS_VAULT env var,
|
|
133
|
+
then falls back to an empty string (tests supply it later).
|
|
134
|
+
"""
|
|
135
|
+
if vault_path is None:
|
|
136
|
+
vault_path = os.environ.get("MNEMOS_VAULT", "")
|
|
137
|
+
|
|
138
|
+
cfg = MnemosConfig(vault_path=vault_path)
|
|
139
|
+
|
|
140
|
+
if not vault_path:
|
|
141
|
+
return cfg
|
|
142
|
+
|
|
143
|
+
yaml_path = Path(vault_path) / "mnemos.yaml"
|
|
144
|
+
if not yaml_path.exists():
|
|
145
|
+
return cfg
|
|
146
|
+
|
|
147
|
+
with yaml_path.open("r", encoding="utf-8") as fh:
|
|
148
|
+
raw: dict = yaml.safe_load(fh) or {}
|
|
149
|
+
|
|
150
|
+
# Scalar fields
|
|
151
|
+
for scalar in (
|
|
152
|
+
"palace_root",
|
|
153
|
+
"recycled_dir",
|
|
154
|
+
"identity_dir",
|
|
155
|
+
"use_llm",
|
|
156
|
+
"llm_model",
|
|
157
|
+
"search_limit",
|
|
158
|
+
"rerank",
|
|
159
|
+
"rerank_model",
|
|
160
|
+
"watcher_enabled",
|
|
161
|
+
"chromadb_path",
|
|
162
|
+
"graph_path",
|
|
163
|
+
"mine_log_path",
|
|
164
|
+
):
|
|
165
|
+
if scalar in raw:
|
|
166
|
+
setattr(cfg, scalar, raw[scalar])
|
|
167
|
+
|
|
168
|
+
# List fields
|
|
169
|
+
if "languages" in raw:
|
|
170
|
+
cfg.languages = list(raw["languages"])
|
|
171
|
+
|
|
172
|
+
if "halls" in raw:
|
|
173
|
+
cfg.halls = list(raw["halls"])
|
|
174
|
+
|
|
175
|
+
if "watcher_ignore" in raw:
|
|
176
|
+
cfg.watcher_ignore = list(raw["watcher_ignore"])
|
|
177
|
+
|
|
178
|
+
# Mining sources
|
|
179
|
+
if "mining_sources" in raw:
|
|
180
|
+
cfg.mining_sources = [
|
|
181
|
+
MiningSource(
|
|
182
|
+
path=src.get("path", ""),
|
|
183
|
+
mode=src.get("mode", "session"),
|
|
184
|
+
external=src.get("external", False),
|
|
185
|
+
)
|
|
186
|
+
for src in raw["mining_sources"]
|
|
187
|
+
if isinstance(src, dict)
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
return cfg
|