@dinasor/mnemo-cli 0.0.2 → 0.0.3
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.
- package/CHANGELOG.md +13 -1
- package/README.md +6 -1
- package/VERSION +1 -1
- package/memory_mac.sh +64 -3
- package/package.json +1 -1
- package/scripts/memory/installer/templates/mnemo_vector.py +100 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,17 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Mnemo u
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.0.3] - 2026-02-21
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- Vector engine now loads project `.env` values (when `GEMINI_API_KEY` is not already set in the shell) before provider resolution, so local API key setup works more reliably in CLI and MCP contexts.
|
|
13
|
+
- Default vector provider auto-resolves to `gemini` when `MNEMO_PROVIDER` is unset and `GEMINI_API_KEY` is available; otherwise it falls back to `openai`.
|
|
14
|
+
- Vector memory root discovery now prioritizes the script location (repo-local `scripts/memory/mnemo_vector.py`) before current working directory scanning, preventing cross-project context leaks when invoked from another directory.
|
|
15
|
+
- Embedded POSIX fallback vector engine now mirrors the same `.env` and provider-resolution behavior as the primary template.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- `mnemo_vector.py` now supports direct CLI operations: `sync`, `search`, `forget`, `health`, and `status`, enabling manual vector workflows outside MCP tool calls.
|
|
19
|
+
|
|
9
20
|
## [0.0.2] - 2026-02-21
|
|
10
21
|
|
|
11
22
|
### Changed
|
|
@@ -55,6 +66,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Mnemo u
|
|
|
55
66
|
- Version string drift between installer metadata and generated output by using `VERSION` as single source of truth.
|
|
56
67
|
- Python fallback handling in memory query flows that previously depended on a single interpreter name.
|
|
57
68
|
|
|
58
|
-
[Unreleased]: https://github.com/DiNaSoR/Mnemo/compare/v0.0.
|
|
69
|
+
[Unreleased]: https://github.com/DiNaSoR/Mnemo/compare/v0.0.3...HEAD
|
|
70
|
+
[0.0.3]: https://github.com/DiNaSoR/Mnemo/releases/tag/v0.0.3
|
|
59
71
|
[0.0.2]: https://github.com/DiNaSoR/Mnemo/releases/tag/v0.0.2
|
|
60
72
|
[0.0.1]: https://github.com/DiNaSoR/Mnemo/releases/tag/v0.0.1
|
package/README.md
CHANGED
|
@@ -218,7 +218,7 @@ scripts/
|
|
|
218
218
|
| `add-lesson.ps1` | Creates next `L-XXX` lesson with normalized tags |
|
|
219
219
|
| `add-journal-entry.ps1` | Adds entry under current date in monthly journal |
|
|
220
220
|
| `clear-active.ps1` | Resets `active-context.md` |
|
|
221
|
-
| `mnemo_vector.py` | Vector sync
|
|
221
|
+
| `mnemo_vector.py` | Vector MCP server + CLI (`sync`, `search`, `forget`, `health`, `status`) in vector mode |
|
|
222
222
|
|
|
223
223
|
## 🔐 Git hooks and API keys
|
|
224
224
|
|
|
@@ -230,12 +230,17 @@ Mnemo auto-configures `core.hooksPath` to `.githooks` and installs:
|
|
|
230
230
|
Important:
|
|
231
231
|
- Cursor MCP tools read API keys from `.mnemo/mcp/cursor.mcp.json` env placeholders (`.cursor/mcp.json` stays bridged).
|
|
232
232
|
- Git hooks read API keys from your shell environment.
|
|
233
|
+
- If `GEMINI_API_KEY` is not already in the environment, `scripts/memory/mnemo_vector.py` also tries loading keys from project-root `.env`.
|
|
233
234
|
|
|
234
235
|
```sh
|
|
235
236
|
# bash/zsh example
|
|
236
237
|
export OPENAI_API_KEY="sk-..."
|
|
237
238
|
# or
|
|
238
239
|
export GEMINI_API_KEY="..."
|
|
240
|
+
|
|
241
|
+
# optional direct CLI usage (outside MCP tool calls)
|
|
242
|
+
python3 scripts/memory/mnemo_vector.py sync
|
|
243
|
+
python3 scripts/memory/mnemo_vector.py health
|
|
239
244
|
```
|
|
240
245
|
|
|
241
246
|
## ✅ Recommended daily workflow
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.0.
|
|
1
|
+
0.0.3
|
package/memory_mac.sh
CHANGED
|
@@ -1821,9 +1821,70 @@ from mcp.server.fastmcp import FastMCP
|
|
|
1821
1821
|
|
|
1822
1822
|
SCHEMA_VERSION = 2
|
|
1823
1823
|
EMBED_DIM = 1536
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1824
|
+
|
|
1825
|
+
|
|
1826
|
+
def _resolve_memory_root() -> Path:
|
|
1827
|
+
override = os.getenv("MNEMO_MEMORY_ROOT", "").strip()
|
|
1828
|
+
if override:
|
|
1829
|
+
return Path(override).expanduser().resolve()
|
|
1830
|
+
|
|
1831
|
+
script_repo = Path(__file__).resolve().parents[2]
|
|
1832
|
+
for rel in ((".mnemo", "memory"), (".cursor", "memory")):
|
|
1833
|
+
candidate = script_repo.joinpath(*rel)
|
|
1834
|
+
if candidate.exists():
|
|
1835
|
+
return candidate
|
|
1836
|
+
|
|
1837
|
+
cwd = Path.cwd().resolve()
|
|
1838
|
+
for root in (cwd, *cwd.parents):
|
|
1839
|
+
for rel in ((".mnemo", "memory"), (".cursor", "memory")):
|
|
1840
|
+
candidate = root.joinpath(*rel)
|
|
1841
|
+
if candidate.exists():
|
|
1842
|
+
return candidate
|
|
1843
|
+
return script_repo / ".mnemo" / "memory"
|
|
1844
|
+
|
|
1845
|
+
|
|
1846
|
+
def _parse_env_line(raw_line: str):
|
|
1847
|
+
line = raw_line.strip()
|
|
1848
|
+
if not line or line.startswith("#"):
|
|
1849
|
+
return None
|
|
1850
|
+
if line.startswith("export "):
|
|
1851
|
+
line = line[7:].strip()
|
|
1852
|
+
if "=" not in line:
|
|
1853
|
+
return None
|
|
1854
|
+
key, value = line.split("=", 1)
|
|
1855
|
+
key = key.strip()
|
|
1856
|
+
if not key or any(ch.isspace() for ch in key):
|
|
1857
|
+
return None
|
|
1858
|
+
value = value.strip()
|
|
1859
|
+
if value and len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
|
|
1860
|
+
value = value[1:-1]
|
|
1861
|
+
elif " #" in value:
|
|
1862
|
+
value = value.split(" #", 1)[0].rstrip()
|
|
1863
|
+
return key, value
|
|
1864
|
+
|
|
1865
|
+
|
|
1866
|
+
def _load_project_env() -> None:
|
|
1867
|
+
if os.getenv("GEMINI_API_KEY"):
|
|
1868
|
+
return
|
|
1869
|
+
env_path = Path(".env")
|
|
1870
|
+
if not env_path.exists():
|
|
1871
|
+
return
|
|
1872
|
+
try:
|
|
1873
|
+
for raw_line in env_path.read_text(encoding="utf-8").splitlines():
|
|
1874
|
+
parsed = _parse_env_line(raw_line)
|
|
1875
|
+
if not parsed:
|
|
1876
|
+
continue
|
|
1877
|
+
key, value = parsed
|
|
1878
|
+
os.environ.setdefault(key, value)
|
|
1879
|
+
except OSError:
|
|
1880
|
+
pass
|
|
1881
|
+
|
|
1882
|
+
|
|
1883
|
+
MEM_ROOT = _resolve_memory_root()
|
|
1884
|
+
_DB_OVERRIDE = os.getenv("MNEMO_DB_PATH", "").strip()
|
|
1885
|
+
DB_PATH = Path(_DB_OVERRIDE).expanduser().resolve() if _DB_OVERRIDE else (MEM_ROOT / "mnemo_vector.sqlite")
|
|
1886
|
+
_load_project_env()
|
|
1887
|
+
PROVIDER = os.getenv("MNEMO_PROVIDER", "gemini" if os.getenv("GEMINI_API_KEY") else "openai").lower()
|
|
1827
1888
|
|
|
1828
1889
|
SKIP_NAMES = {
|
|
1829
1890
|
"README.md",
|
package/package.json
CHANGED
|
@@ -9,6 +9,8 @@ import re
|
|
|
9
9
|
import json
|
|
10
10
|
import sqlite3
|
|
11
11
|
import hashlib
|
|
12
|
+
import argparse
|
|
13
|
+
import sys
|
|
12
14
|
from pathlib import Path
|
|
13
15
|
|
|
14
16
|
import sqlite_vec
|
|
@@ -27,19 +29,78 @@ def _resolve_memory_root() -> Path:
|
|
|
27
29
|
if override:
|
|
28
30
|
return Path(override).expanduser().resolve()
|
|
29
31
|
|
|
32
|
+
script_repo = Path(__file__).resolve().parents[2]
|
|
33
|
+
for rel in ((".mnemo", "memory"), (".cursor", "memory")):
|
|
34
|
+
candidate = script_repo.joinpath(*rel)
|
|
35
|
+
if candidate.exists():
|
|
36
|
+
return candidate
|
|
37
|
+
|
|
30
38
|
cwd = Path.cwd().resolve()
|
|
31
39
|
for root in (cwd, *cwd.parents):
|
|
32
40
|
for rel in ((".mnemo", "memory"), (".cursor", "memory")):
|
|
33
41
|
candidate = root.joinpath(*rel)
|
|
34
42
|
if candidate.exists():
|
|
35
43
|
return candidate
|
|
36
|
-
return
|
|
44
|
+
return script_repo / ".mnemo" / "memory"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _resolve_repo_root(memory_root: Path) -> Path:
|
|
48
|
+
root = memory_root.resolve()
|
|
49
|
+
if root.name == "memory" and root.parent.name in {".mnemo", ".cursor"}:
|
|
50
|
+
return root.parent.parent
|
|
51
|
+
cwd = Path.cwd().resolve()
|
|
52
|
+
for candidate in (cwd, *cwd.parents):
|
|
53
|
+
if candidate.joinpath(".mnemo", "memory").exists() or candidate.joinpath(".cursor", "memory").exists():
|
|
54
|
+
return candidate
|
|
55
|
+
return cwd
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _parse_env_line(raw_line: str) -> tuple[str, str] | None:
|
|
59
|
+
line = raw_line.strip()
|
|
60
|
+
if not line or line.startswith("#"):
|
|
61
|
+
return None
|
|
62
|
+
if line.startswith("export "):
|
|
63
|
+
line = line[7:].strip()
|
|
64
|
+
if "=" not in line:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
key, value = line.split("=", 1)
|
|
68
|
+
key = key.strip()
|
|
69
|
+
if not key or any(ch.isspace() for ch in key):
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
value = value.strip()
|
|
73
|
+
if value and len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
|
|
74
|
+
value = value[1:-1]
|
|
75
|
+
elif " #" in value:
|
|
76
|
+
value = value.split(" #", 1)[0].rstrip()
|
|
77
|
+
return key, value
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _load_project_env(repo_root: Path) -> None:
|
|
81
|
+
# Keep shell-provided values authoritative; only fill missing vars from .env.
|
|
82
|
+
if os.getenv("GEMINI_API_KEY"):
|
|
83
|
+
return
|
|
84
|
+
env_path = repo_root / ".env"
|
|
85
|
+
if not env_path.exists():
|
|
86
|
+
return
|
|
87
|
+
try:
|
|
88
|
+
for raw_line in env_path.read_text(encoding="utf-8").splitlines():
|
|
89
|
+
parsed = _parse_env_line(raw_line)
|
|
90
|
+
if not parsed:
|
|
91
|
+
continue
|
|
92
|
+
key, value = parsed
|
|
93
|
+
os.environ.setdefault(key, value)
|
|
94
|
+
except OSError:
|
|
95
|
+
pass
|
|
37
96
|
|
|
38
97
|
|
|
39
98
|
MEM_ROOT = _resolve_memory_root()
|
|
99
|
+
REPO_ROOT = _resolve_repo_root(MEM_ROOT)
|
|
100
|
+
_load_project_env(REPO_ROOT)
|
|
40
101
|
_DB_OVERRIDE = os.getenv("MNEMO_DB_PATH", "").strip()
|
|
41
102
|
DB_PATH = Path(_DB_OVERRIDE).expanduser().resolve() if _DB_OVERRIDE else (MEM_ROOT / "mnemo_vector.sqlite")
|
|
42
|
-
PROVIDER = os.getenv("MNEMO_PROVIDER", "openai").lower()
|
|
103
|
+
PROVIDER = os.getenv("MNEMO_PROVIDER", "gemini" if os.getenv("GEMINI_API_KEY") else "openai").lower()
|
|
43
104
|
|
|
44
105
|
SKIP_NAMES = {
|
|
45
106
|
"README.md", "index.md", "lessons-index.json",
|
|
@@ -552,5 +613,42 @@ def memory_status() -> str:
|
|
|
552
613
|
return json.dumps({"error": str(e)})
|
|
553
614
|
|
|
554
615
|
|
|
616
|
+
def _run_cli(argv: list[str]) -> int:
|
|
617
|
+
parser = argparse.ArgumentParser(description="Mnemo vector CLI")
|
|
618
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
619
|
+
|
|
620
|
+
sub.add_parser("sync", help="Rebuild vector index from memory markdown files")
|
|
621
|
+
|
|
622
|
+
p_search = sub.add_parser("search", help="Semantic search memory")
|
|
623
|
+
p_search.add_argument("query", help="Search query text")
|
|
624
|
+
p_search.add_argument("--top-k", type=int, default=8, help="Number of results to return")
|
|
625
|
+
|
|
626
|
+
p_forget = sub.add_parser("forget", help="Remove vectors by source path")
|
|
627
|
+
p_forget.add_argument("ref_path", help="Reference path to remove (exact match)")
|
|
628
|
+
|
|
629
|
+
sub.add_parser("health", help="Check DB and embedding provider health")
|
|
630
|
+
sub.add_parser("status", help="Return JSON memory status summary")
|
|
631
|
+
|
|
632
|
+
args = parser.parse_args(argv)
|
|
633
|
+
try:
|
|
634
|
+
if args.command == "sync":
|
|
635
|
+
print(vector_sync())
|
|
636
|
+
elif args.command == "search":
|
|
637
|
+
print(vector_search(args.query, top_k=args.top_k))
|
|
638
|
+
elif args.command == "forget":
|
|
639
|
+
print(vector_forget(args.ref_path))
|
|
640
|
+
elif args.command == "health":
|
|
641
|
+
print(vector_health())
|
|
642
|
+
elif args.command == "status":
|
|
643
|
+
print(memory_status())
|
|
644
|
+
except Exception as e:
|
|
645
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
646
|
+
return 1
|
|
647
|
+
return 0
|
|
648
|
+
|
|
649
|
+
|
|
555
650
|
if __name__ == "__main__":
|
|
651
|
+
cli_commands = {"sync", "search", "forget", "health", "status"}
|
|
652
|
+
if len(sys.argv) > 1 and sys.argv[1] in cli_commands:
|
|
653
|
+
raise SystemExit(_run_cli(sys.argv[1:]))
|
|
556
654
|
mcp.run()
|