@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 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.2...HEAD
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/search MCP server (vector mode only) |
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.2
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
- MEM_ROOT = Path(".cursor/memory")
1825
- DB_PATH = MEM_ROOT / "mnemo_vector.sqlite"
1826
- PROVIDER = os.getenv("MNEMO_PROVIDER", "openai").lower()
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dinasor/mnemo-cli",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Mnemo installer CLI for the current project folder.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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 cwd / ".mnemo" / "memory"
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()