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 ADDED
@@ -0,0 +1,2 @@
1
+ """Mnemos — Obsidian-native AI memory palace."""
2
+ __version__ = "0.1.0"
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