mnemolith 0.2.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.
mnemolith/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Mnemolith: shared memory for AI coding tools that stays true to your code."""
2
+
3
+ __version__ = "0.2.0"
mnemolith/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Lets AI tools launch the server as `python -m mnemolith serve`, which works even when the
2
+ `mnemolith` command isn't on their PATH (common for apps started from the dock or Start menu)."""
3
+ from .cli import main
4
+
5
+ main()
mnemolith/cli.py ADDED
@@ -0,0 +1,462 @@
1
+ """The `mnemolith` command."""
2
+ import argparse
3
+ import sys
4
+ from collections import Counter
5
+
6
+ from . import __version__, config, connect, engine
7
+
8
+ OK, WARN, FAIL = " ok ", " !! ", " FAIL "
9
+
10
+
11
+ def _say(mark: str, text: str) -> None:
12
+ lines = text.splitlines() or [""]
13
+ print(f"[{mark}] {lines[0]}")
14
+ for line in lines[1:]:
15
+ print(f" {line}")
16
+
17
+
18
+ # ---------- commands ----------
19
+
20
+ def _java_label(java: str) -> str:
21
+ return f"private copy in {java_dir()}" if engine.is_private(java) else java
22
+
23
+
24
+ def java_dir():
25
+ from .javart import JAVA_DIR
26
+ return JAVA_DIR
27
+
28
+
29
+ def _java_for_setup():
30
+ """The computer's own Java if it has a recent enough one; otherwise downloads a private copy."""
31
+ try:
32
+ return engine.find_java()
33
+ except engine.EngineError as missing:
34
+ from . import javart
35
+ reason = str(missing).splitlines()[0] # "Java isn't installed." or "Found Java 8, which is too old."
36
+ print(f"[ .... ] {reason} Downloading a private copy of Java {javart.FEATURE_VERSION} (Eclipse Temurin, "
37
+ f"from adoptium.net, about 50 MB, only the first time).\n"
38
+ f" It goes in {javart.JAVA_DIR}, only Mnemolith uses it, and nothing else on your "
39
+ f"computer changes.", flush=True)
40
+ try:
41
+ javart.download()
42
+ except javart.JavaDownloadError as e:
43
+ raise engine.EngineError(str(e))
44
+ return engine.find_java()
45
+
46
+
47
+ def cmd_setup(args) -> int:
48
+ print(f"Setting up Mnemolith {__version__}\n")
49
+ try:
50
+ java, major = _java_for_setup()
51
+ _say(OK, f"Java {major} ({_java_label(java)})")
52
+ _say(OK, engine.start())
53
+ except engine.EngineError as e:
54
+ _say(FAIL, str(e))
55
+ print("\nFix that, then run `mnemolith setup` again."
56
+ + ("" if config.CONTACT_URL in str(e) else f"\nStill stuck? Tell us: {config.CONTACT_URL}"))
57
+ return 1
58
+
59
+ if config.FAKE_EMBEDDER:
60
+ _say(OK, "Embedding model: test mode, nothing to download")
61
+ else:
62
+ if any(config.MODELS_DIR.glob("*")):
63
+ print("[ .... ] Loading the embedding model...", flush=True)
64
+ else:
65
+ print(f"[ .... ] Downloading the embedding model from Hugging Face (huggingface.co, about 90 MB, only the "
66
+ f"first time)\n into {config.MODELS_DIR}. It's what lets Mnemolith search memories by "
67
+ f"meaning...", flush=True)
68
+ try:
69
+ from .server import embedder
70
+ next(iter(embedder().embed(["warm up"])))
71
+ _say(OK, f"Embedding model ready ({config.MODEL})")
72
+ except Exception as e:
73
+ detail = (str(e).strip().splitlines() or [type(e).__name__])[0][:160]
74
+ _say(WARN, f"Couldn't download the embedding model from Hugging Face ({detail}).\n"
75
+ f"If your network blocks huggingface.co (common on work or school networks), try another\n"
76
+ f"network, then run `mnemolith setup` again. Otherwise it downloads the first time your\n"
77
+ f"AI saves or searches a memory. Still stuck? Tell us: {config.CONTACT_URL}")
78
+
79
+ tools = args.tools or connect.detected()
80
+ if not tools:
81
+ _say(WARN, "No supported AI coding tools found (Claude Code, Cursor, Codex).")
82
+ print("\n" + connect.generic_instructions())
83
+ failed = []
84
+ for tool in tools:
85
+ done, message = connect.CONNECTORS[tool]()
86
+ _say(OK if done else FAIL, message)
87
+ if not done:
88
+ failed.append(tool)
89
+ if failed:
90
+ print(f"\nAlmost done: fix the {', '.join(failed)} problem above, then run "
91
+ f"`mnemolith connect {' '.join(failed)}`.")
92
+ return 1
93
+
94
+ print("\nDone. Open your AI tool in any git repo and try:\n"
95
+ ' "Remember that our tests need DOCKER_HOST set. It depends on docker-compose.yml."\n'
96
+ "Then, in a new session:\n"
97
+ ' "How do I run the tests?"\n'
98
+ "\nSee and fix what your AI remembers any time with `mnemolith ui`.")
99
+ return 0
100
+
101
+
102
+ def cmd_start(args) -> int:
103
+ try:
104
+ print(engine.start())
105
+ return 0
106
+ except engine.EngineError as e:
107
+ print(e)
108
+ return 1
109
+
110
+
111
+ def cmd_stop(args) -> int:
112
+ print(engine.stop())
113
+ return 0
114
+
115
+
116
+ def cmd_status(args) -> int:
117
+ state = engine.probe()
118
+ if state == "ours":
119
+ pid = engine.running_pid()
120
+ print(f"Engine: running on {config.HOST}:{config.PORT}" + (f" (pid {pid})" if pid else ""))
121
+ elif state == "other":
122
+ print(f"Engine: not running. {engine.port_taken_message()}")
123
+ return 1
124
+ else:
125
+ print("Engine: not running (it starts automatically when your AI tool needs it, or run `mnemolith start`)")
126
+ print(f"Data: {config.DATA_DIR}")
127
+ if state == "ours":
128
+ from .server import project_name, run
129
+ project = project_name()
130
+ counts = Counter(status for _, status, _, _ in run("MEM.LIST", project, "STATUS", "any"))
131
+ summary = ", ".join(f"{counts[s]} {s}" for s in ("active", "stale", "superseded") if counts[s])
132
+ print(f"This project ({project}): {summary or 'no memories yet'}")
133
+ connected = [t for t in connect.TOOLS if connect.CHECKS[t]()]
134
+ print("Connected tools: " + (", ".join(connected) if connected else "none (run `mnemolith connect`)"))
135
+ return 0
136
+
137
+
138
+ def cmd_connect(args) -> int:
139
+ if args.print:
140
+ print(connect.generic_instructions())
141
+ return 0
142
+ tools = args.tools or connect.detected()
143
+ if not tools:
144
+ print("No supported AI coding tools found (Claude Code, Cursor, Codex).\n")
145
+ print(connect.generic_instructions())
146
+ return 1
147
+ failed = False
148
+ for tool in tools:
149
+ done, message = connect.CONNECTORS[tool]()
150
+ _say(OK if done else FAIL, message)
151
+ failed |= not done
152
+ return 1 if failed else 0
153
+
154
+
155
+ def cmd_doctor(args) -> int:
156
+ problems = 0
157
+ _say(OK, f"Mnemolith {__version__}, Python {sys.version.split()[0]} ({sys.executable})")
158
+ try:
159
+ java, major = engine.find_java()
160
+ _say(OK, f"Java {major} ({_java_label(java)})")
161
+ except engine.EngineError as e:
162
+ _say(FAIL, str(e))
163
+ problems += 1
164
+ if engine.JAR.is_file():
165
+ _say(OK, f"Engine file present ({engine.JAR.name})")
166
+ else:
167
+ _say(FAIL, f"Engine file missing ({engine.JAR}). Reinstall Mnemolith.")
168
+ problems += 1
169
+ state = engine.probe()
170
+ if state == "ours":
171
+ _say(OK, f"Engine answering on {config.HOST}:{config.PORT}")
172
+ elif state == "other":
173
+ _say(FAIL, engine.port_taken_message())
174
+ problems += 1
175
+ else:
176
+ _say(WARN, f"Engine not running. It starts automatically when needed; `mnemolith start` starts it now.\n"
177
+ f"If it won't start, see {config.LOG_FILE}")
178
+ if config.FAKE_EMBEDDER:
179
+ _say(WARN, "Test embedder is on (MNEMOLITH_EMBEDDER=fake): search quality is poor. Unset it for real use.")
180
+ elif any(config.MODELS_DIR.glob("*")):
181
+ _say(OK, f"Embedding model downloaded ({config.MODELS_DIR})")
182
+ else:
183
+ _say(WARN, "Embedding model not downloaded yet; it downloads on first use (about 90 MB).")
184
+ for tool in connect.TOOLS:
185
+ if connect.CHECKS[tool]():
186
+ _say(OK, f"Connected to {tool}")
187
+ elif tool in connect.detected():
188
+ _say(WARN, f"{tool} is installed but not connected. Run `mnemolith connect {tool}`.")
189
+ from .server import git_toplevel, project_name
190
+ top = git_toplevel()
191
+ _say(OK if top else WARN, f"This folder's project: {project_name()}"
192
+ + ("" if top else " (not a git repo, so memories can't be linked to file versions here)"))
193
+ print("\nEverything looks good." if problems == 0 else
194
+ f"\n{problems} problem(s) to fix above. Still stuck? Tell us: {config.CONTACT_URL}")
195
+ return 1 if problems else 0
196
+
197
+
198
+ def cmd_list(args) -> int:
199
+ from .server import EngineDown, project_name, run, store_down
200
+ try:
201
+ rows = run("MEM.LIST", project_name(), "STATUS", args.status)
202
+ except EngineDown as e:
203
+ print(store_down(e))
204
+ return 1
205
+ if not rows:
206
+ print(f"No {'' if args.status == 'any' else args.status + ' '}memories in {project_name()} yet.")
207
+ for mid, status, files, text in rows:
208
+ linked = ", ".join(f.rsplit("@", 1)[0] for f in files.split(",") if f)
209
+ print(f"{mid:>5} {status:<10} {text}" + (f" [{linked}]" if linked else ""))
210
+ return 0
211
+
212
+
213
+ def _tool_output(name: str, *tool_args) -> int:
214
+ from . import server
215
+ print(getattr(server, name)(*tool_args))
216
+ return 0
217
+
218
+
219
+ def cmd_add(args) -> int:
220
+ from . import repos
221
+ from .server import EngineDown, store_down
222
+ try:
223
+ engine.ensure_running()
224
+ added = repos.add(args.path)
225
+ except repos.RepoError as e:
226
+ print(e)
227
+ return 1
228
+ except (engine.EngineError, EngineDown) as e:
229
+ print(store_down(e))
230
+ return 1
231
+ print(f"Added {added['name']} ({added['path']})" + (f", github.com/{added['github']}" if added["github"] else "") + ".")
232
+ return 0
233
+
234
+
235
+ def _ask(question: str) -> bool:
236
+ if not sys.stdin.isatty():
237
+ return False
238
+ try:
239
+ return input(f"{question} [y/N] ").strip().lower() in ("y", "yes")
240
+ except EOFError:
241
+ return False
242
+
243
+
244
+ def cmd_import(args) -> int:
245
+ from pathlib import Path
246
+ from . import portability, repos, server
247
+ folder = Path(args.path).expanduser()
248
+ if not folder.is_dir():
249
+ print(f"{args.path} isn't a folder.")
250
+ return 1
251
+ root = repos.git_top(folder) or folder.resolve()
252
+ project = root.name
253
+ try:
254
+ engine.ensure_running()
255
+ found = portability.scan(project, root)
256
+ except (engine.EngineError, server.EngineDown) as e:
257
+ print(server.store_down(e))
258
+ return 1
259
+ if not found["files"]:
260
+ print(f"No notes files in {root}.\nMnemolith looks for: " + ", ".join(portability.NOTE_FILES)
261
+ + ", and .cursor/rules/.")
262
+ return 0
263
+ notes = found["notes"]
264
+ new = [n for n in notes if n["selected"]]
265
+ left_out = [n for n in notes if not n["known"] and not n["selected"]]
266
+ known = [n for n in notes if n["known"]]
267
+ print(f"Found {len(notes)} notes in " + ", ".join(f["path"] for f in found["files"]) + f" ({project})\n")
268
+ for i, n in enumerate(new, 1):
269
+ print(f" {i:>3}. {n['text']}" + (f"\n linked to {', '.join(n['files'])}" if n["files"] else ""))
270
+ if left_out:
271
+ print(f"\nLeft out ({len(left_out)}):")
272
+ for n in left_out:
273
+ if n["similar"]:
274
+ what = n["similar"]["id"] or "another note here"
275
+ why = f"very similar to {what}: \"{n['similar']['text']}\""
276
+ elif n["missing"]:
277
+ why = f"mentions {', '.join(n['missing'])}, which doesn't exist (probably out of date)"
278
+ else:
279
+ why = "doesn't look like a fact about the project"
280
+ print(f" - {n['text']}\n {why}")
281
+ if known:
282
+ print(f"\nAlready remembered: {len(known)}")
283
+ if not new:
284
+ print("\nNothing new to import.")
285
+ return 0
286
+ if left_out:
287
+ print("\nTo pick notes one by one, including the left-out ones, use Import notes in `mnemolith ui`.")
288
+ if not args.yes and not _ask(f"\nImport {len(new)} {'memory' if len(new) == 1 else 'memories'} into {project}?"):
289
+ print("Nothing imported." + ("" if sys.stdin.isatty() else " Pass --yes to import without asking."))
290
+ return 0
291
+ try:
292
+ result = portability.apply(project, root)
293
+ repos.add(str(root)) # so the dashboard can check the imported memories' files
294
+ except repos.RepoError:
295
+ pass
296
+ except server.EngineDown as e:
297
+ print(server.store_down(e))
298
+ return 1
299
+ saved = len(result["saved"])
300
+ print(f"Imported {saved} {'memory' if saved == 1 else 'memories'} into {project}."
301
+ + (f" {result['duplicates']} matched memories you already had." if result["duplicates"] else "")
302
+ + (f"\n{result['conflicts']} may contradict an existing memory: decide in `mnemolith ui`."
303
+ if result["conflicts"] else ""))
304
+ return 0
305
+
306
+
307
+ def cmd_export(args) -> int:
308
+ import json
309
+ from pathlib import Path
310
+ from . import portability, server
311
+ try:
312
+ engine.ensure_running()
313
+ data = portability.export(args.project or None)
314
+ except (engine.EngineError, server.EngineDown) as e:
315
+ print(server.store_down(e))
316
+ return 1
317
+ text = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
318
+ if args.output == "-":
319
+ sys.stdout.write(text)
320
+ return 0
321
+ out = Path(args.output or portability.export_filename(args.project[0] if len(args.project or []) == 1 else None))
322
+ out = out.expanduser()
323
+ out.write_text(text, encoding="utf-8")
324
+ n = sum(len(p["memories"]) for p in data["projects"])
325
+ print(f"Exported {n} {'memory' if n == 1 else 'memories'} from {len(data['projects'])} "
326
+ f"{'project' if len(data['projects']) == 1 else 'projects'} to {out.resolve()}")
327
+ return 0
328
+
329
+
330
+ def _uninstall_command() -> str:
331
+ exe = sys.executable.replace("\\", "/")
332
+ if "/uv/tools/" in exe or "/uv/tool/" in exe:
333
+ return "uv tool uninstall mnemolith"
334
+ if "/pipx/" in exe:
335
+ return "pipx uninstall mnemolith"
336
+ return f"{sys.executable} -m pip uninstall mnemolith"
337
+
338
+
339
+ def cmd_uninstall(args) -> int:
340
+ import shutil
341
+ print("Uninstalling Mnemolith\n")
342
+ failed = False
343
+ for tool in connect.TOOLS:
344
+ done, message = connect.DISCONNECTORS[tool]()
345
+ _say(OK if done else FAIL, message)
346
+ failed |= not done
347
+ print(" Restart any AI tools that are open, so they stop using Mnemolith.")
348
+ count = None
349
+ if engine.probe() == "ours":
350
+ try:
351
+ from .ui import all_memories
352
+ count = len(all_memories())
353
+ except Exception:
354
+ pass
355
+ stopped = engine.stop()
356
+ still_running = engine.probe() == "ours"
357
+ _say(FAIL if still_running else OK, stopped)
358
+ failed |= still_running
359
+
360
+ has_data = config.HOME.exists()
361
+ if still_running and has_data: # it would rewrite them anyway
362
+ print(f"\nYour memories are kept in {config.HOME}: stop the engine first to delete them.")
363
+ print(f"\nLast step, remove the program itself:\n {_uninstall_command()}")
364
+ return 1
365
+ delete = args.delete_data
366
+ if has_data and not delete:
367
+ what = f"Your {count} {'memory is' if count == 1 else 'memories are'}" if count else "Your memories are"
368
+ print(f"\n{what} in {config.HOME}.")
369
+ delete = _ask("Delete them too? This can't be undone (keep a copy first with `mnemolith export`).")
370
+ if has_data and delete:
371
+ shutil.rmtree(config.HOME, ignore_errors=True)
372
+ _say(OK, f"Deleted {config.HOME}")
373
+ elif has_data:
374
+ from . import javart
375
+ if javart.remove():
376
+ _say(OK, f"Removed Mnemolith's private copy of Java ({javart.JAVA_DIR})")
377
+ _say(OK, f"Kept your memories in {config.HOME}. Reinstall any time and they're back.")
378
+ print(f"\nLast step, remove the program itself:\n {_uninstall_command()}")
379
+ return 1 if failed else 0
380
+
381
+
382
+ def cmd_serve(args) -> int:
383
+ from .server import serve
384
+ serve()
385
+ return 0
386
+
387
+
388
+ def main(argv=None) -> None:
389
+ try:
390
+ sys.stdout.reconfigure(errors="replace") # never crash printing on a limited console
391
+ except (AttributeError, ValueError):
392
+ pass
393
+ from . import legacy
394
+ moved = legacy.migrate_home() # first run after the rename from Aether Cache
395
+ if moved:
396
+ print(moved + "\n", file=sys.stderr) # stderr: when an AI tool runs `serve`, stdout carries the MCP protocol
397
+ parser = argparse.ArgumentParser(
398
+ prog="mnemolith",
399
+ description="Shared memory for AI coding tools that stays true to your code.")
400
+ parser.add_argument("--version", action="version", version=f"mnemolith {__version__}")
401
+ sub = parser.add_subparsers(dest="command", metavar="<command>")
402
+
403
+ p = sub.add_parser("setup", help="one-time setup: start the engine, download the model, connect your AI tools")
404
+ p.add_argument("tools", nargs="*", metavar="tool",
405
+ help="which tools to connect (default: every supported tool found)")
406
+ p.set_defaults(func=cmd_setup)
407
+ p = sub.add_parser("connect", help="connect AI tools: claude-code, cursor, codex")
408
+ p.add_argument("tools", nargs="*", metavar="tool",
409
+ help="which tools to connect (default: every supported tool found)")
410
+ p.add_argument("--print", action="store_true", help="print config for any other MCP tool instead")
411
+ p.set_defaults(func=cmd_connect)
412
+ sub.add_parser("start", help="start the engine in the background").set_defaults(func=cmd_start)
413
+ sub.add_parser("stop", help="stop the engine").set_defaults(func=cmd_stop)
414
+ sub.add_parser("status", help="is it running, and what's in this project's memory").set_defaults(func=cmd_status)
415
+ sub.add_parser("doctor", help="check everything and say how to fix problems").set_defaults(func=cmd_doctor)
416
+ p = sub.add_parser("list", help="list this project's memories")
417
+ p.add_argument("--status", default="active", choices=["active", "stale", "superseded", "any"])
418
+ p.set_defaults(func=cmd_list)
419
+ sub.add_parser("recall", help="search this project's memories").add_argument("query")
420
+ sub.choices["recall"].set_defaults(func=lambda a: _tool_output("recall", a.query))
421
+ sub.add_parser("explain", help="where a memory came from and its history").add_argument("id")
422
+ sub.choices["explain"].set_defaults(func=lambda a: _tool_output("explain_memory", a.id))
423
+ sub.add_parser("sync", help="write current memories into AGENTS.md / CLAUDE.md").set_defaults(
424
+ func=lambda a: _tool_output("sync_to_files"))
425
+ sub.add_parser("check", help="retire memories whose linked files changed").set_defaults(
426
+ func=lambda a: _tool_output("check_stale"))
427
+ p = sub.add_parser("add", help="add a repo so the dashboard can check its files (default: this folder)")
428
+ p.add_argument("path", nargs="?", default=".")
429
+ p.set_defaults(func=cmd_add)
430
+ p = sub.add_parser("import", help="turn notes in CLAUDE.md, AGENTS.md, and Cursor rules into memories")
431
+ p.add_argument("path", nargs="?", default=".", help="the repo to import from (default: this folder)")
432
+ p.add_argument("--yes", "-y", action="store_true", help="import without asking")
433
+ p.set_defaults(func=cmd_import)
434
+ p = sub.add_parser("export", help="save memories, with their history, to a JSON file")
435
+ p.add_argument("--project", "-p", action="append", metavar="NAME", help="only this project (repeatable; default: all)")
436
+ p.add_argument("--output", "-o", metavar="FILE", help="where to write it (default: mnemolith-<date>.json here; - for stdout)")
437
+ p.set_defaults(func=cmd_export)
438
+ p = sub.add_parser("uninstall", help="disconnect your AI tools, stop the engine, and optionally delete memories")
439
+ p.add_argument("--delete-data", action="store_true", help="also delete all memories, without asking")
440
+ p.set_defaults(func=cmd_uninstall)
441
+ p = sub.add_parser("ui", help="open the dashboard: see and fix what your AI tools remember")
442
+ p.add_argument("--port", type=int, help="port for the dashboard (default 7381)")
443
+ p.add_argument("--no-open", action="store_true", help="don't open a browser, just print the link")
444
+ p.set_defaults(func=lambda a: __import__("mnemolith.ui", fromlist=["serve"]).serve(a.port, not a.no_open))
445
+ sub.add_parser("serve", help="run the MCP server (your AI tool runs this for you)").set_defaults(func=cmd_serve)
446
+
447
+ args = parser.parse_args(argv)
448
+ unknown = [t for t in getattr(args, "tools", None) or [] if t not in connect.TOOLS]
449
+ if unknown:
450
+ parser.error(f"unknown tool {unknown[0]!r}; choose from {', '.join(connect.TOOLS)} "
451
+ f"(or `mnemolith connect --print` for any other MCP tool)")
452
+ if not getattr(args, "func", None):
453
+ parser.print_help()
454
+ print("\nNew here? Run `mnemolith setup`.")
455
+ sys.exit(0)
456
+ try:
457
+ sys.exit(args.func(args))
458
+ except KeyboardInterrupt:
459
+ sys.exit(130)
460
+ except BrokenPipeError: # output piped into something like `head` that stopped reading
461
+ sys.stderr.close()
462
+ sys.exit(0)
mnemolith/config.py ADDED
@@ -0,0 +1,51 @@
1
+ """Where Mnemolith keeps things, and how to reach the engine. All settings are optional."""
2
+ import os
3
+ from pathlib import Path
4
+
5
+
6
+ def _env(*names: str, default: str = "") -> str:
7
+ """First set environment variable among names. Each MNEMOLITH_* setting also accepts the names from before
8
+ the rename (AETHER_*, and MEMORY_* from before that), so existing setups keep working."""
9
+ names = [n for name in names for n in (name, name.replace("MNEMOLITH_", "AETHER_", 1))]
10
+ for name in names:
11
+ value = os.environ.get(name)
12
+ if value:
13
+ return value
14
+ return default
15
+
16
+
17
+ # Everything lives under one folder: the engine's data, its log, and the downloaded embedding model.
18
+ HOME = Path(_env("MNEMOLITH_HOME", default=str(Path.home() / ".mnemolith"))).expanduser()
19
+ CUSTOM_HOME = bool(_env("MNEMOLITH_HOME"))
20
+ DATA_DIR = HOME / "data"
21
+ LOG_FILE = HOME / "engine.log"
22
+ PID_FILE = HOME / "engine.pid"
23
+ STAMP_FILE = HOME / "engine.version" # which engine file the running engine was started from
24
+ MODELS_DIR = HOME / "models"
25
+
26
+ # Where people can reach us when something goes wrong (the website's contact form).
27
+ CONTACT_URL = "https://mnemolith.dev/install.html#contact"
28
+
29
+ # The engine listens on this machine only. The port is deliberately not Redis's 6379, so Mnemolith
30
+ # never collides with a real Redis you may already run.
31
+ HOST = _env("MNEMOLITH_HOST", "MEMORY_HOST", default="127.0.0.1")
32
+ PORT = int(_env("MNEMOLITH_PORT", "MEMORY_PORT", default="7380"))
33
+
34
+ # Which AI tool this server is running for (recorded with every memory it saves).
35
+ SOURCE = _env("MNEMOLITH_SOURCE", "MEMORY_SOURCE", default="unknown")
36
+ MODEL = _env("MNEMOLITH_MODEL", "MEMORY_MODEL", default="sentence-transformers/all-MiniLM-L6-v2")
37
+ FAKE_EMBEDDER = _env("MNEMOLITH_EMBEDDER", "MEMORY_EMBEDDER") == "fake" # testing only, needs no download
38
+ # Existing memories at least this similar to a newly saved one are shown to the AI to judge.
39
+ # Low on purpose: in calibration, "we use Postgres" vs "we migrated to MySQL" scored only 0.30.
40
+ RELATED_SIMILARITY = float(_env("MNEMOLITH_RELATED_SIMILARITY", "MEMORY_RELATED_SIMILARITY", default="0.25"))
41
+
42
+
43
+ def project_override() -> str:
44
+ return _env("MNEMOLITH_PROJECT", "MEMORY_PROJECT")
45
+
46
+
47
+ def repo_override() -> str:
48
+ """The project folder, when the AI tool says where it is (Cursor passes ${workspaceFolder}).
49
+ Ignored if the tool didn't fill in the placeholder."""
50
+ value = _env("MNEMOLITH_REPO", "MEMORY_REPO")
51
+ return "" if "${" in value else value