@dinasor/mnemo-cli 0.0.4 → 0.0.6
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 +23 -1
- package/README.md +50 -3
- package/VERSION +1 -1
- package/bin/mnemo.js +513 -101
- package/memory_mac.sh +48 -9
- package/package.json +1 -1
- package/scripts/memory/installer/templates/mnemo_vector.py +34 -7
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,26 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Mnemo u
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.0.6] - 2026-02-22
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `npx @dinasor/mnemo-cli@latest` now launches a smart interactive wizard when run in a TTY, guiding users through vector mode enablement, provider selection, and API key setup in a beautiful ANSI UI.
|
|
13
|
+
- Wizard automatically detects existing API keys in the shell environment or project `.env` and skips the key prompt when a real value is already present.
|
|
14
|
+
- API key entered during the wizard is immediately appended to the project's `.env` file and injected into the current process environment.
|
|
15
|
+
- Dependency preflight check displays Node.js, Git, Python, pip, and all required Python packages (with live "checking…" → installed/will-be-installed status) before running the installer.
|
|
16
|
+
- `--yes` / `-y` flag skips the wizard entirely for non-interactive / CI use while still showing the dependency check.
|
|
17
|
+
- Success box at the end of install shows next steps: `vector_health → vector_sync` hint and `mnemo-codebase-optimizer` skill reminder for seeding codebase memory.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- `package.json` version aligned to `0.0.6` (was stale at `0.0.1`).
|
|
21
|
+
|
|
22
|
+
## [0.0.5] - 2026-02-22
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- Vector `.env` loading now treats blank/unresolved MCP placeholder values (for example `${env:GEMINI_API_KEY}`) as missing and correctly hydrates `GEMINI_API_KEY`/`OPENAI_API_KEY` from project `.env`.
|
|
26
|
+
- Vector provider resolution now ignores unresolved `MNEMO_PROVIDER` placeholders and falls back to valid runtime values (`gemini` when key is present, otherwise `openai`).
|
|
27
|
+
- `.env` key parser now strips BOM characters from key names to prevent silent misses on Windows-generated files.
|
|
28
|
+
|
|
9
29
|
## [0.0.4] - 2026-02-22
|
|
10
30
|
|
|
11
31
|
### Added
|
|
@@ -75,7 +95,9 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Mnemo u
|
|
|
75
95
|
- Version string drift between installer metadata and generated output by using `VERSION` as single source of truth.
|
|
76
96
|
- Python fallback handling in memory query flows that previously depended on a single interpreter name.
|
|
77
97
|
|
|
78
|
-
[Unreleased]: https://github.com/DiNaSoR/Mnemo/compare/v0.0.
|
|
98
|
+
[Unreleased]: https://github.com/DiNaSoR/Mnemo/compare/v0.0.6...HEAD
|
|
99
|
+
[0.0.6]: https://github.com/DiNaSoR/Mnemo/compare/v0.0.5...v0.0.6
|
|
100
|
+
[0.0.5]: https://github.com/DiNaSoR/Mnemo/compare/v0.0.4...v0.0.5
|
|
79
101
|
[0.0.4]: https://github.com/DiNaSoR/Mnemo/releases/tag/v0.0.4
|
|
80
102
|
[0.0.3]: https://github.com/DiNaSoR/Mnemo/releases/tag/v0.0.3
|
|
81
103
|
[0.0.2]: https://github.com/DiNaSoR/Mnemo/releases/tag/v0.0.2
|
package/README.md
CHANGED
|
@@ -28,13 +28,24 @@ Run from the **target project root**.
|
|
|
28
28
|
### Any OS (npx, recommended)
|
|
29
29
|
|
|
30
30
|
```sh
|
|
31
|
-
#
|
|
31
|
+
# Interactive wizard — guides you through every option (recommended)
|
|
32
32
|
npx @dinasor/mnemo-cli@latest
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The wizard will ask you:
|
|
36
|
+
|
|
37
|
+
1. **Vector mode** — enable semantic / embedding-based memory recall?
|
|
38
|
+
2. **Provider** — Gemini (`GEMINI_API_KEY`) or OpenAI (`OPENAI_API_KEY`)?
|
|
39
|
+
3. **API key** — enter now (saved to `.env`), already in `.env`, or skip.
|
|
33
40
|
|
|
34
|
-
|
|
41
|
+
It then runs a live dependency check (Node · Git · Python · pip · packages) and launches the installer.
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
# Non-interactive (CI / scripting) — skip wizard, use flags directly
|
|
45
|
+
npx @dinasor/mnemo-cli@latest --yes
|
|
46
|
+
npx @dinasor/mnemo-cli@latest --enable-vector --vector-provider gemini --yes
|
|
35
47
|
npx @dinasor/mnemo-cli@latest --dry-run
|
|
36
48
|
npx @dinasor/mnemo-cli@latest --force
|
|
37
|
-
npx @dinasor/mnemo-cli@latest --enable-vector --vector-provider gemini
|
|
38
49
|
```
|
|
39
50
|
|
|
40
51
|
### Windows (PowerShell)
|
|
@@ -79,6 +90,38 @@ vector_sync
|
|
|
79
90
|
|
|
80
91
|
---
|
|
81
92
|
|
|
93
|
+
## 🧠 Seeding memory for an existing codebase
|
|
94
|
+
|
|
95
|
+
After installing Mnemo in a project that already has code, use the bundled
|
|
96
|
+
`mnemo-codebase-optimizer` skill to fill memory quickly and accurately.
|
|
97
|
+
|
|
98
|
+
**In Cursor (or any agent that loads `.cursor/skills/`):**
|
|
99
|
+
|
|
100
|
+
1. Open the project.
|
|
101
|
+
2. Start a new conversation and say:
|
|
102
|
+
|
|
103
|
+
> Use the **mnemo-codebase-optimizer** skill to seed memory for this codebase.
|
|
104
|
+
|
|
105
|
+
Or load the skill directly:
|
|
106
|
+
|
|
107
|
+
```text
|
|
108
|
+
@.cursor/skills/mnemo-codebase-optimizer/SKILL.md
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
3. The agent will map architecture, ownership, dev workflows, risks, commands,
|
|
112
|
+
and write optimized `memo.md`, `hot-rules.md`, `active-context.md`, lessons,
|
|
113
|
+
and a journal summary — then validate retrieval quality.
|
|
114
|
+
|
|
115
|
+
The skill is installed automatically by `memory.ps1` / `memory_mac.sh` and lives at:
|
|
116
|
+
|
|
117
|
+
```text
|
|
118
|
+
.cursor/skills/mnemo-codebase-optimizer/
|
|
119
|
+
SKILL.md ← skill prompt + checklist
|
|
120
|
+
reference.md ← memory file templates + retrieval queries
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
82
125
|
## 🧩 IDE setup guide (per project)
|
|
83
126
|
|
|
84
127
|
Use the section that matches your IDE. Each project should run Mnemo install once.
|
|
@@ -257,6 +300,7 @@ python3 scripts/memory/mnemo_vector.py health
|
|
|
257
300
|
|
|
258
301
|
## 📋 Requirements
|
|
259
302
|
|
|
303
|
+
- **Node.js 18+** (for `npx @dinasor/mnemo-cli@latest`)
|
|
260
304
|
- **PowerShell**: Windows PowerShell 5.1+ or PowerShell 7 (`pwsh`)
|
|
261
305
|
- **Git**
|
|
262
306
|
- **Optional**: Python 3 for SQLite FTS index
|
|
@@ -265,6 +309,9 @@ python3 scripts/memory/mnemo_vector.py health
|
|
|
265
309
|
- API key: `OPENAI_API_KEY` or `GEMINI_API_KEY`
|
|
266
310
|
- Auto-installed deps: `openai`, `sqlite-vec`, `mcp[cli]>=1.2.0,<2.0` (+ `google-genai` for Gemini)
|
|
267
311
|
|
|
312
|
+
> The interactive wizard (`npx @dinasor/mnemo-cli@latest`) checks all of these
|
|
313
|
+
> before running the installer and reports which packages are already installed.
|
|
314
|
+
|
|
268
315
|
## 🤝 Contributing
|
|
269
316
|
|
|
270
317
|
See [CONTRIBUTING.md](CONTRIBUTING.md). Bug reports and feature requests use the issue templates in [.github/ISSUE_TEMPLATE/](.github/ISSUE_TEMPLATE/). Security issues go to [SECURITY.md](SECURITY.md).
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.0.
|
|
1
|
+
0.0.6
|
package/bin/mnemo.js
CHANGED
|
@@ -1,139 +1,551 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Mnemo CLI — interactive wizard + cross-platform installer runner.
|
|
6
|
+
*
|
|
7
|
+
* When stdin is a TTY (and --yes is not passed) the wizard:
|
|
8
|
+
* 1. Asks whether to enable vector/semantic search mode.
|
|
9
|
+
* 2. Asks which embedding provider (gemini / openai).
|
|
10
|
+
* 3. Checks for an existing API key (env / .env file), or lets the user
|
|
11
|
+
* enter one now (saved to project .env) or skip.
|
|
12
|
+
* 4. Checks all runtime dependencies and reports their status.
|
|
13
|
+
* 5. Runs memory.ps1 (Windows) or memory_mac.sh (POSIX).
|
|
14
|
+
*
|
|
15
|
+
* When --yes / -y is passed, or stdin is not a TTY, the wizard is skipped
|
|
16
|
+
* and the installer runs immediately using whatever flags were supplied.
|
|
17
|
+
*/
|
|
2
18
|
|
|
3
19
|
const { spawnSync } = require("child_process");
|
|
4
|
-
const fs
|
|
20
|
+
const fs = require("fs");
|
|
5
21
|
const path = require("path");
|
|
22
|
+
const rl = require("readline");
|
|
23
|
+
|
|
24
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
25
|
+
const PKG_ROOT = path.resolve(__dirname, "..");
|
|
26
|
+
const CWD = process.cwd();
|
|
27
|
+
const IS_WIN = process.platform === "win32";
|
|
28
|
+
const ARGV = process.argv.slice(2);
|
|
29
|
+
|
|
30
|
+
// ─── ANSI color helpers ───────────────────────────────────────────────────────
|
|
31
|
+
const HAS_COLOR = !!process.stdout.isTTY && !process.env.NO_COLOR;
|
|
32
|
+
const esc = (n) => HAS_COLOR ? `\x1b[${n}m` : "";
|
|
6
33
|
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
34
|
+
const R = esc(0); // reset
|
|
35
|
+
const BO = esc(1); // bold
|
|
36
|
+
const DI = esc(2); // dim
|
|
37
|
+
const CY = esc(36); // cyan
|
|
38
|
+
const GR = esc(32); // green
|
|
39
|
+
const YE = esc(33); // yellow
|
|
40
|
+
const RE = esc(31); // red
|
|
41
|
+
const WH = esc(97); // bright white
|
|
42
|
+
const BCY = esc(96); // bright cyan
|
|
43
|
+
const BGR = esc(92); // bright green
|
|
44
|
+
const BRE = esc(91); // bright red
|
|
45
|
+
const BYE = esc(93); // bright yellow
|
|
46
|
+
const MG = esc(35); // magenta
|
|
11
47
|
|
|
12
|
-
|
|
13
|
-
|
|
48
|
+
const bold = (s) => `${BO}${s}${R}`;
|
|
49
|
+
const dim = (s) => `${DI}${s}${R}`;
|
|
50
|
+
const cyan = (s) => `${CY}${s}${R}`;
|
|
51
|
+
const green = (s) => `${GR}${s}${R}`;
|
|
52
|
+
const yellow = (s) => `${YE}${s}${R}`;
|
|
14
53
|
|
|
15
|
-
|
|
16
|
-
|
|
54
|
+
const TICK = `${BGR}✓${R}`;
|
|
55
|
+
const CROSS = `${BRE}✗${R}`;
|
|
56
|
+
const WARN = `${BYE}⚠${R}`;
|
|
57
|
+
const ARROW = `${BCY}›${R}`;
|
|
17
58
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
`
|
|
59
|
+
// ─── Layout helpers ───────────────────────────────────────────────────────────
|
|
60
|
+
const W = 62; // box inner content width
|
|
61
|
+
|
|
62
|
+
function padR(s, n) { return s + " ".repeat(Math.max(0, n - s.length)); }
|
|
63
|
+
|
|
64
|
+
function banner(version) {
|
|
65
|
+
const bar = "═".repeat(W);
|
|
66
|
+
const t1 = `Mnemo v${version} · Memory Layer for AI Agents`;
|
|
67
|
+
const t2 = `Token-safe · Cursor · Claude Code · Codex & more`;
|
|
68
|
+
const pad1 = W - t1.length - 2;
|
|
69
|
+
const pad2 = W - t2.length - 2;
|
|
70
|
+
process.stdout.write("\n");
|
|
71
|
+
process.stdout.write(`${CY}╔${bar}╗${R}\n`);
|
|
72
|
+
process.stdout.write(`${CY}║${R} ${WH}${BO}${t1}${R}${" ".repeat(Math.max(0, pad1))}${CY}║${R}\n`);
|
|
73
|
+
process.stdout.write(`${CY}║${R} ${DI}${t2}${R}${" ".repeat(Math.max(0, pad2))}${CY}║${R}\n`);
|
|
74
|
+
process.stdout.write(`${CY}╚${bar}╝${R}\n`);
|
|
75
|
+
process.stdout.write("\n");
|
|
27
76
|
}
|
|
28
77
|
|
|
29
|
-
function
|
|
30
|
-
|
|
31
|
-
process.exit(1);
|
|
78
|
+
function divider() {
|
|
79
|
+
process.stdout.write(` ${DI}${"─".repeat(W - 2)}${R}\n`);
|
|
32
80
|
}
|
|
33
81
|
|
|
34
|
-
function
|
|
35
|
-
|
|
36
|
-
|
|
82
|
+
function sectionHeader(title, step, total) {
|
|
83
|
+
divider();
|
|
84
|
+
const stepLabel = total ? ` ${DI}Step ${step}/${total}${R}` : "";
|
|
85
|
+
process.stdout.write(`\n ${BCY}${BO}${title}${R}${stepLabel}\n\n`);
|
|
86
|
+
}
|
|
37
87
|
|
|
38
|
-
|
|
39
|
-
|
|
88
|
+
function successBox(vectorMode) {
|
|
89
|
+
const bar = "═".repeat(W);
|
|
90
|
+
const t1 = `Setup complete!`;
|
|
91
|
+
const pad1 = W - t1.length - 2;
|
|
92
|
+
process.stdout.write("\n");
|
|
93
|
+
process.stdout.write(`${BGR}╔${bar}╗${R}\n`);
|
|
94
|
+
process.stdout.write(`${BGR}║${R} ${WH}${BO}${t1}${R}${" ".repeat(Math.max(0, pad1))}${BGR}║${R}\n`);
|
|
95
|
+
if (vectorMode) {
|
|
96
|
+
const t2 = `Run vector_health → vector_sync in your IDE`;
|
|
97
|
+
const pad2 = W - t2.length - 2;
|
|
98
|
+
process.stdout.write(`${BGR}║${R} ${DI}${t2}${R}${" ".repeat(Math.max(0, pad2))}${BGR}║${R}\n`);
|
|
99
|
+
}
|
|
100
|
+
const t3 = `Skill: .cursor/skills/mnemo-codebase-optimizer/`;
|
|
101
|
+
const pad3 = W - t3.length - 2;
|
|
102
|
+
process.stdout.write(`${BGR}║${R} ${DI}${t3}${R}${" ".repeat(Math.max(0, pad3))}${BGR}║${R}\n`);
|
|
103
|
+
process.stdout.write(`${BGR}╚${bar}╝${R}\n`);
|
|
104
|
+
process.stdout.write("\n");
|
|
105
|
+
}
|
|
40
106
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
mapped.push("-VectorProvider", value);
|
|
57
|
-
i += 1;
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
if (arg === "--project-name") {
|
|
61
|
-
const value = args[i + 1];
|
|
62
|
-
if (!value) fail("Missing value for --project-name");
|
|
63
|
-
mapped.push("-ProjectName", value);
|
|
64
|
-
i += 1;
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
if (arg === "--repo-root") {
|
|
68
|
-
const value = args[i + 1];
|
|
69
|
-
if (!value) fail("Missing value for --repo-root");
|
|
70
|
-
mapped.push("-RepoRoot", value);
|
|
71
|
-
hasRepoRoot = true;
|
|
72
|
-
i += 1;
|
|
73
|
-
continue;
|
|
107
|
+
// ─── .env utilities ───────────────────────────────────────────────────────────
|
|
108
|
+
function readDotEnv(dir) {
|
|
109
|
+
const envPath = path.join(dir, ".env");
|
|
110
|
+
const result = {};
|
|
111
|
+
if (!fs.existsSync(envPath)) return result;
|
|
112
|
+
try {
|
|
113
|
+
const text = fs.readFileSync(envPath, "utf8").replace(/^\uFEFF/, "");
|
|
114
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
115
|
+
const line = raw.trim();
|
|
116
|
+
if (!line || line.startsWith("#")) continue;
|
|
117
|
+
const eq = line.indexOf("=");
|
|
118
|
+
if (eq < 1) continue;
|
|
119
|
+
const key = line.slice(0, eq).trim();
|
|
120
|
+
const val = line.slice(eq + 1).trim().replace(/^['"]|['"]$/g, "");
|
|
121
|
+
result[key] = val;
|
|
74
122
|
}
|
|
123
|
+
} catch { /* ignore */ }
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
75
126
|
|
|
76
|
-
|
|
77
|
-
|
|
127
|
+
function appendDotEnv(dir, key, value) {
|
|
128
|
+
const envPath = path.join(dir, ".env");
|
|
129
|
+
try {
|
|
130
|
+
if (fs.existsSync(envPath)) {
|
|
131
|
+
let content = fs.readFileSync(envPath, "utf8");
|
|
132
|
+
const re = new RegExp(`^${key}=.*$`, "m");
|
|
133
|
+
if (re.test(content)) {
|
|
134
|
+
fs.writeFileSync(envPath, content.replace(re, `${key}=${value}`));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const nl = content.endsWith("\n") ? "" : "\n";
|
|
138
|
+
fs.appendFileSync(envPath, `${nl}${key}=${value}\n`);
|
|
139
|
+
} else {
|
|
140
|
+
fs.writeFileSync(envPath, `${key}=${value}\n`);
|
|
78
141
|
}
|
|
79
|
-
|
|
142
|
+
} catch (e) {
|
|
143
|
+
process.stdout.write(` ${WARN} Could not write .env: ${e.message}\n`);
|
|
80
144
|
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Returns true if the value is a real, usable string —
|
|
149
|
+
* not empty, not an unresolved Cursor MCP placeholder like ${env:FOO}.
|
|
150
|
+
*/
|
|
151
|
+
function isRealValue(v) {
|
|
152
|
+
if (!v) return false;
|
|
153
|
+
const s = v.trim();
|
|
154
|
+
if (!s) return false;
|
|
155
|
+
if (s.startsWith("${env:") && s.endsWith("}")) return false;
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ─── Dependency detection ─────────────────────────────────────────────────────
|
|
160
|
+
function runCmd(cmd, args, opts = {}) {
|
|
161
|
+
return spawnSync(cmd, args, {
|
|
162
|
+
encoding: "utf8",
|
|
163
|
+
timeout: 15000,
|
|
164
|
+
windowsHide: true,
|
|
165
|
+
...opts,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function checkNode() {
|
|
170
|
+
return { ver: process.version, ok: true };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function checkGit() {
|
|
174
|
+
const r = runCmd("git", ["--version"]);
|
|
175
|
+
if (r.status !== 0) return { ver: null, ok: false };
|
|
176
|
+
const m = (r.stdout || "").match(/git version (.+)/);
|
|
177
|
+
return { ver: m ? m[1].trim() : "?", ok: true };
|
|
178
|
+
}
|
|
81
179
|
|
|
82
|
-
|
|
83
|
-
|
|
180
|
+
function findPython() {
|
|
181
|
+
const candidates = IS_WIN ? ["py", "python", "python3"] : ["python3", "python"];
|
|
182
|
+
for (const cmd of candidates) {
|
|
183
|
+
const r = runCmd(cmd, IS_WIN && cmd === "py" ? ["-3", "--version"] : ["--version"]);
|
|
184
|
+
if (r.status !== 0) continue;
|
|
185
|
+
const raw = (r.stdout || r.stderr || "").trim();
|
|
186
|
+
const m = raw.match(/Python (\d+)\.(\d+)\.(\d+)/);
|
|
187
|
+
if (!m) continue;
|
|
188
|
+
const [, maj, min] = m.map(Number);
|
|
189
|
+
return {
|
|
190
|
+
cmd,
|
|
191
|
+
ver: `${maj}.${min}.${m[3]}`,
|
|
192
|
+
ok: maj > 3 || (maj === 3 && min >= 10),
|
|
193
|
+
};
|
|
84
194
|
}
|
|
85
|
-
return
|
|
195
|
+
return { cmd: null, ver: null, ok: false };
|
|
86
196
|
}
|
|
87
197
|
|
|
88
|
-
function
|
|
89
|
-
const
|
|
90
|
-
|
|
198
|
+
function checkPip(pythonCmd) {
|
|
199
|
+
const args = IS_WIN && pythonCmd === "py" ? ["-3", "-m", "pip", "--version"] : ["-m", "pip", "--version"];
|
|
200
|
+
const r = runCmd(pythonCmd, args);
|
|
201
|
+
if (r.status !== 0) return { ver: null, ok: false };
|
|
202
|
+
const m = (r.stdout || "").match(/pip (\S+)/);
|
|
203
|
+
return { ver: m ? m[1] : "?", ok: true };
|
|
204
|
+
}
|
|
91
205
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
206
|
+
function checkPipPkg(pythonCmd, pkgName) {
|
|
207
|
+
const baseArgs = IS_WIN && pythonCmd === "py" ? ["-3"] : [];
|
|
208
|
+
const r = runCmd(pythonCmd, [...baseArgs, "-m", "pip", "show", pkgName]);
|
|
209
|
+
if (r.status !== 0) return { installed: false, ver: null };
|
|
210
|
+
const m = (r.stdout || "").match(/^Version:\s*(.+)$/m);
|
|
211
|
+
return { installed: true, ver: m ? m[1].trim() : "?" };
|
|
212
|
+
}
|
|
95
213
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
214
|
+
function depRow(label, verStr, statusStr) {
|
|
215
|
+
const lc = padR(label, 22);
|
|
216
|
+
const vc = padR(verStr, 14);
|
|
217
|
+
process.stdout.write(` ${DI}${lc}${R} ${CY}${vc}${R} ${statusStr}\n`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function runDependencyCheck(vectorMode, provider, pythonInfo) {
|
|
221
|
+
process.stdout.write("\n");
|
|
222
|
+
process.stdout.write(` ${BCY}${BO}Checking requirements${R}\n`);
|
|
223
|
+
process.stdout.write(` ${DI}${"─".repeat(50)}${R}\n`);
|
|
224
|
+
process.stdout.write("\n");
|
|
225
|
+
|
|
226
|
+
// Node.js — always present (we are running in it)
|
|
227
|
+
const node = checkNode();
|
|
228
|
+
depRow("Node.js", node.ver, `${TICK} ready`);
|
|
229
|
+
|
|
230
|
+
// Git
|
|
231
|
+
const git = checkGit();
|
|
232
|
+
depRow("Git", git.ver || "not found", git.ok ? `${TICK} ready` : `${WARN} recommended (not found)`);
|
|
233
|
+
|
|
234
|
+
if (vectorMode) {
|
|
235
|
+
// Python
|
|
236
|
+
const py = pythonInfo || findPython();
|
|
237
|
+
if (!py.ok) {
|
|
238
|
+
depRow(
|
|
239
|
+
"Python",
|
|
240
|
+
py.ver || "not found",
|
|
241
|
+
py.ver ? `${CROSS} Python 3.10+ required (found ${py.ver})` : `${CROSS} Python 3.10+ required`,
|
|
242
|
+
);
|
|
243
|
+
} else {
|
|
244
|
+
depRow("Python", py.ver, `${TICK} ready`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (py.cmd && py.ok) {
|
|
248
|
+
// pip
|
|
249
|
+
const pip = checkPip(py.cmd);
|
|
250
|
+
depRow("pip", pip.ver || "not found", pip.ok ? `${TICK} ready` : `${WARN} pip missing`);
|
|
251
|
+
|
|
252
|
+
if (pip.ok) {
|
|
253
|
+
process.stdout.write("\n");
|
|
254
|
+
process.stdout.write(` ${DI} Python packages (${provider} mode):${R}\n`);
|
|
255
|
+
process.stdout.write("\n");
|
|
256
|
+
|
|
257
|
+
const core = ["openai", "sqlite-vec", "mcp"];
|
|
258
|
+
const extra = provider === "gemini" ? ["google-genai"] : [];
|
|
259
|
+
const pkgs = [...core, ...extra];
|
|
260
|
+
|
|
261
|
+
for (const pkg of pkgs) {
|
|
262
|
+
// Show "checking…" then overwrite with result
|
|
263
|
+
const label = padR(pkg, 22);
|
|
264
|
+
process.stdout.write(` ${DI}${label}${R} ${DI}checking…${R}`);
|
|
265
|
+
const res = checkPipPkg(py.cmd, pkg);
|
|
266
|
+
// Overwrite the line
|
|
267
|
+
process.stdout.write(`\r${" ".repeat(W)}\r`);
|
|
268
|
+
depRow(
|
|
269
|
+
pkg,
|
|
270
|
+
res.ver || "",
|
|
271
|
+
res.installed ? `${TICK} installed` : `${WARN} will be installed by installer`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
101
275
|
}
|
|
102
276
|
}
|
|
103
277
|
|
|
104
|
-
|
|
105
|
-
|
|
278
|
+
process.stdout.write("\n");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ─── Interactive readline helpers ────────────────────────────────────────────
|
|
282
|
+
function prompt(question) {
|
|
283
|
+
return new Promise((resolve) => {
|
|
284
|
+
const iface = rl.createInterface({ input: process.stdin, output: process.stdout });
|
|
285
|
+
iface.question(question, (answer) => {
|
|
286
|
+
iface.close();
|
|
287
|
+
resolve(answer.trim());
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function askYesNo(question, defaultYes = false) {
|
|
293
|
+
const hint = defaultYes ? `${DI}[Y/n]${R}` : `${DI}[y/N]${R}`;
|
|
294
|
+
const ans = await prompt(` ${ARROW} ${question} ${hint} `);
|
|
295
|
+
if (!ans) return defaultYes;
|
|
296
|
+
return /^y(es)?$/i.test(ans);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function askChoice(question, choices, defaultIdx = 0) {
|
|
300
|
+
process.stdout.write(` ${ARROW} ${question}\n\n`);
|
|
301
|
+
choices.forEach((ch, i) => {
|
|
302
|
+
const active = i === defaultIdx;
|
|
303
|
+
const num = active ? `${BCY}${BO}[${i + 1}]${R}` : `${DI}[${i + 1}]${R}`;
|
|
304
|
+
process.stdout.write(` ${num} ${ch}\n`);
|
|
305
|
+
});
|
|
306
|
+
process.stdout.write("\n");
|
|
307
|
+
const ans = await prompt(` ${DI}Choice${R} ${DI}[${defaultIdx + 1}]${R}: `);
|
|
308
|
+
const num = parseInt(ans, 10);
|
|
309
|
+
if (!ans || isNaN(num) || num < 1 || num > choices.length) return defaultIdx;
|
|
310
|
+
return num - 1;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function askText(question, hint = "") {
|
|
314
|
+
const hintStr = hint ? ` ${DI}${hint}${R}` : "";
|
|
315
|
+
return prompt(` ${ARROW} ${question}${hintStr}: `);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ─── Flag parser ──────────────────────────────────────────────────────────────
|
|
319
|
+
function parseFlags(args) {
|
|
320
|
+
const flags = {
|
|
321
|
+
enableVector: false,
|
|
322
|
+
vectorProvider: null, // null = not yet specified
|
|
323
|
+
dryRun: false,
|
|
324
|
+
force: false,
|
|
325
|
+
projectName: null,
|
|
326
|
+
repoRoot: null,
|
|
327
|
+
yes: false,
|
|
328
|
+
help: false,
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
for (let i = 0; i < args.length; i++) {
|
|
332
|
+
const a = args[i].toLowerCase();
|
|
333
|
+
switch (a) {
|
|
334
|
+
case "--enable-vector":
|
|
335
|
+
case "-enablevector": flags.enableVector = true; break;
|
|
336
|
+
case "--dry-run":
|
|
337
|
+
case "-dryrun": flags.dryRun = true; break;
|
|
338
|
+
case "--force":
|
|
339
|
+
case "-force": flags.force = true; break;
|
|
340
|
+
case "--yes": case "-y": flags.yes = true; break;
|
|
341
|
+
case "--help": case "-h":flags.help = true; break;
|
|
342
|
+
case "--vector-provider":
|
|
343
|
+
case "-vectorprovider":
|
|
344
|
+
flags.vectorProvider = args[++i]; break;
|
|
345
|
+
case "--project-name":
|
|
346
|
+
case "-projectname":
|
|
347
|
+
flags.projectName = args[++i]; break;
|
|
348
|
+
case "--repo-root":
|
|
349
|
+
case "-reporoot":
|
|
350
|
+
flags.repoRoot = args[++i]; break;
|
|
351
|
+
default:
|
|
352
|
+
// ignore unknown flags gracefully
|
|
353
|
+
}
|
|
106
354
|
}
|
|
107
|
-
return
|
|
355
|
+
return flags;
|
|
108
356
|
}
|
|
109
357
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
358
|
+
// ─── Help ─────────────────────────────────────────────────────────────────────
|
|
359
|
+
function printHelp(version) {
|
|
360
|
+
banner(version);
|
|
361
|
+
process.stdout.write(`${BO}Usage:${R}\n`);
|
|
362
|
+
process.stdout.write(` npx @dinasor/mnemo-cli@latest [options]\n\n`);
|
|
363
|
+
process.stdout.write(`${BO}Options:${R}\n`);
|
|
364
|
+
const opt = (f, d) =>
|
|
365
|
+
process.stdout.write(` ${CY}${padR(f, 32)}${R} ${DI}${d}${R}\n`);
|
|
366
|
+
opt("--enable-vector", "Enable semantic vector search mode");
|
|
367
|
+
opt("--vector-provider <name>", "Embedding provider: gemini | openai");
|
|
368
|
+
opt("--dry-run", "Preview without writing any files");
|
|
369
|
+
opt("--force", "Overwrite existing Mnemo files");
|
|
370
|
+
opt("--project-name <name>", "Override the project name");
|
|
371
|
+
opt("--repo-root <path>", "Target directory (default: cwd)");
|
|
372
|
+
opt("--yes / -y", "Non-interactive — skip wizard prompts");
|
|
373
|
+
opt("--help", "Show this help message");
|
|
374
|
+
process.stdout.write("\n");
|
|
375
|
+
process.stdout.write(`${BO}Examples:${R}\n`);
|
|
376
|
+
process.stdout.write(` ${DI}# Interactive wizard (recommended first-time install)${R}\n`);
|
|
377
|
+
process.stdout.write(` npx @dinasor/mnemo-cli@latest\n\n`);
|
|
378
|
+
process.stdout.write(` ${DI}# Non-interactive with gemini vector mode${R}\n`);
|
|
379
|
+
process.stdout.write(` npx @dinasor/mnemo-cli@latest --enable-vector --vector-provider gemini --yes\n\n`);
|
|
380
|
+
process.stdout.write(` ${DI}# Dry-run to preview changes${R}\n`);
|
|
381
|
+
process.stdout.write(` npx @dinasor/mnemo-cli@latest --dry-run\n\n`);
|
|
113
382
|
}
|
|
114
383
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
384
|
+
// ─── Installer arg builders ───────────────────────────────────────────────────
|
|
385
|
+
function buildWindowsArgs(flags) {
|
|
386
|
+
const args = [];
|
|
387
|
+
if (flags.dryRun) args.push("-DryRun");
|
|
388
|
+
if (flags.force) args.push("-Force");
|
|
389
|
+
if (flags.enableVector) args.push("-EnableVector");
|
|
390
|
+
if (flags.vectorProvider) args.push("-VectorProvider", flags.vectorProvider);
|
|
391
|
+
if (flags.projectName) args.push("-ProjectName", flags.projectName);
|
|
392
|
+
args.push("-RepoRoot", flags.repoRoot || CWD);
|
|
393
|
+
return args;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function buildPosixArgs(flags) {
|
|
397
|
+
const args = [];
|
|
398
|
+
if (flags.dryRun) args.push("--dry-run");
|
|
399
|
+
if (flags.force) args.push("--force");
|
|
400
|
+
if (flags.enableVector) args.push("--enable-vector");
|
|
401
|
+
if (flags.vectorProvider) args.push("--vector-provider", flags.vectorProvider);
|
|
402
|
+
if (flags.projectName) args.push("--project-name", flags.projectName);
|
|
403
|
+
args.push("--repo-root", flags.repoRoot || CWD);
|
|
404
|
+
return args;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
408
|
+
async function main() {
|
|
409
|
+
const versionFile = path.join(PKG_ROOT, "VERSION");
|
|
410
|
+
const version = fs.existsSync(versionFile)
|
|
411
|
+
? fs.readFileSync(versionFile, "utf8").trim()
|
|
412
|
+
: "?";
|
|
413
|
+
|
|
414
|
+
const flags = parseFlags(ARGV);
|
|
415
|
+
const interactive = !flags.yes && !!process.stdin.isTTY;
|
|
416
|
+
|
|
417
|
+
if (flags.help) {
|
|
418
|
+
printHelp(version);
|
|
419
|
+
process.exit(0);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
banner(version);
|
|
423
|
+
|
|
424
|
+
// ── Step 1: Vector mode ────────────────────────────────────────────────────
|
|
425
|
+
let vectorMode = flags.enableVector;
|
|
426
|
+
|
|
427
|
+
if (!vectorMode && interactive) {
|
|
428
|
+
sectionHeader("Vector / Semantic Search Mode", 1, 3);
|
|
429
|
+
process.stdout.write(` ${DI}Enables semantic vector recall via embedding model APIs.${R}\n`);
|
|
430
|
+
process.stdout.write(` ${DI}Requires: Python 3.10+ · OpenAI or Gemini API key${R}\n\n`);
|
|
431
|
+
vectorMode = await askYesNo("Enable vector / semantic search mode?", false);
|
|
432
|
+
process.stdout.write("\n");
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ── Step 2: Provider ──────────────────────────────────────────────────────
|
|
436
|
+
let provider = flags.vectorProvider;
|
|
437
|
+
|
|
438
|
+
if (vectorMode && !provider && interactive) {
|
|
439
|
+
sectionHeader("Embedding Provider", 2, 3);
|
|
440
|
+
const choice = await askChoice("Which embedding provider do you want to use?", [
|
|
441
|
+
`${BGR}Gemini${R} ${DI}GEMINI_API_KEY · google-genai (recommended)${R}`,
|
|
442
|
+
`${CY}OpenAI${R} ${DI}OPENAI_API_KEY · openai${R}`,
|
|
443
|
+
], 0);
|
|
444
|
+
provider = choice === 0 ? "gemini" : "openai";
|
|
445
|
+
process.stdout.write("\n");
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (vectorMode && !provider) provider = "gemini"; // default
|
|
449
|
+
|
|
450
|
+
// ── Step 3: API key ───────────────────────────────────────────────────────
|
|
451
|
+
if (vectorMode && interactive) {
|
|
452
|
+
const keyName = provider === "gemini" ? "GEMINI_API_KEY" : "OPENAI_API_KEY";
|
|
453
|
+
const envVal = process.env[keyName];
|
|
454
|
+
const dotEnv = readDotEnv(flags.repoRoot || CWD);
|
|
455
|
+
|
|
456
|
+
const hasEnvKey = isRealValue(envVal);
|
|
457
|
+
const hasDotEnvKey = isRealValue(dotEnv[keyName]);
|
|
458
|
+
|
|
459
|
+
if (hasEnvKey || hasDotEnvKey) {
|
|
460
|
+
sectionHeader("API Key", 3, 3);
|
|
461
|
+
const src = hasEnvKey ? "shell environment" : ".env file";
|
|
462
|
+
process.stdout.write(` ${TICK} ${bold(keyName)} already present in ${src}\n\n`);
|
|
463
|
+
} else {
|
|
464
|
+
sectionHeader("API Key Setup", 3, 3);
|
|
465
|
+
process.stdout.write(` ${WARN} ${bold(keyName)} is not set in your environment.\n\n`);
|
|
466
|
+
|
|
467
|
+
const choice = await askChoice(
|
|
468
|
+
"How do you want to provide the API key?",
|
|
469
|
+
[
|
|
470
|
+
`Enter key now ${DI}→ appended to .env in project root${R}`,
|
|
471
|
+
`Skip (have .env) ${DI}→ .env already contains the key${R}`,
|
|
472
|
+
`Skip for now ${DI}→ set ${bold(keyName)} manually later${R}`,
|
|
473
|
+
],
|
|
474
|
+
0,
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
process.stdout.write("\n");
|
|
478
|
+
|
|
479
|
+
if (choice === 0) {
|
|
480
|
+
const apiKey = (await askText(`Paste your ${bold(keyName)}`)).trim();
|
|
481
|
+
process.stdout.write("\n");
|
|
482
|
+
if (apiKey) {
|
|
483
|
+
appendDotEnv(flags.repoRoot || CWD, keyName, apiKey);
|
|
484
|
+
process.env[keyName] = apiKey;
|
|
485
|
+
process.stdout.write(` ${TICK} Key appended to ${bold(".env")}\n`);
|
|
486
|
+
} else {
|
|
487
|
+
process.stdout.write(` ${WARN} No key entered — set ${bold(keyName)} before using vector tools\n`);
|
|
488
|
+
}
|
|
489
|
+
} else if (choice === 1) {
|
|
490
|
+
process.stdout.write(` ${TICK} Will load from ${bold(".env")} automatically\n`);
|
|
491
|
+
} else {
|
|
492
|
+
process.stdout.write(` ${WARN} Skipped — set ${bold(keyName)} in your shell or .env before first use\n`);
|
|
493
|
+
}
|
|
494
|
+
process.stdout.write("\n");
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// ── Dependency check ──────────────────────────────────────────────────────
|
|
499
|
+
const pythonInfo = findPython();
|
|
500
|
+
await runDependencyCheck(vectorMode, provider, pythonInfo);
|
|
501
|
+
|
|
502
|
+
// ── Run installer ─────────────────────────────────────────────────────────
|
|
503
|
+
flags.enableVector = vectorMode;
|
|
504
|
+
if (vectorMode) flags.vectorProvider = provider;
|
|
505
|
+
|
|
506
|
+
divider();
|
|
507
|
+
process.stdout.write(`\n ${BCY}${BO}Running Mnemo installer…${R}\n\n`);
|
|
508
|
+
divider();
|
|
509
|
+
process.stdout.write("\n");
|
|
510
|
+
|
|
511
|
+
let result;
|
|
512
|
+
|
|
513
|
+
if (IS_WIN) {
|
|
514
|
+
const installer = path.join(PKG_ROOT, "memory.ps1");
|
|
515
|
+
if (!fs.existsSync(installer)) {
|
|
516
|
+
process.stderr.write(`${CROSS} Installer not found: ${installer}\n`);
|
|
517
|
+
process.exit(1);
|
|
518
|
+
}
|
|
519
|
+
result = spawnSync(
|
|
520
|
+
"powershell",
|
|
521
|
+
["-ExecutionPolicy", "Bypass", "-File", installer, ...buildWindowsArgs(flags)],
|
|
522
|
+
{ stdio: "inherit" },
|
|
523
|
+
);
|
|
524
|
+
} else {
|
|
525
|
+
const installer = path.join(PKG_ROOT, "memory_mac.sh");
|
|
526
|
+
if (!fs.existsSync(installer)) {
|
|
527
|
+
process.stderr.write(`${CROSS} Installer not found: ${installer}\n`);
|
|
528
|
+
process.exit(1);
|
|
529
|
+
}
|
|
530
|
+
result = spawnSync("sh", [installer, ...buildPosixArgs(flags)], {
|
|
531
|
+
stdio: "inherit",
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (result.status === 0) {
|
|
536
|
+
successBox(vectorMode);
|
|
537
|
+
if (vectorMode) {
|
|
538
|
+
process.stdout.write(` ${ARROW} Open your IDE, restart MCP, and run ${bold("vector_health")} → ${bold("vector_sync")}\n`);
|
|
539
|
+
}
|
|
540
|
+
process.stdout.write(` ${ARROW} Use the ${bold("mnemo-codebase-optimizer")} skill to quickly seed memory for this codebase\n`);
|
|
541
|
+
process.stdout.write(` ${DI}.cursor/skills/mnemo-codebase-optimizer/SKILL.md${R}\n`);
|
|
542
|
+
process.stdout.write("\n");
|
|
119
543
|
}
|
|
120
544
|
|
|
121
|
-
const args = [
|
|
122
|
-
"-ExecutionPolicy",
|
|
123
|
-
"Bypass",
|
|
124
|
-
"-File",
|
|
125
|
-
installer,
|
|
126
|
-
...mapWindowsArgs(rawArgs)
|
|
127
|
-
];
|
|
128
|
-
const result = spawnSync("powershell", args, { stdio: "inherit" });
|
|
129
545
|
process.exit(result.status ?? 1);
|
|
130
546
|
}
|
|
131
547
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
const result = spawnSync("sh", [installer, ...mapPosixArgs(rawArgs)], {
|
|
137
|
-
stdio: "inherit"
|
|
548
|
+
main().catch((err) => {
|
|
549
|
+
process.stderr.write(`\n${CROSS} Fatal error: ${err.message}\n`);
|
|
550
|
+
process.exit(1);
|
|
138
551
|
});
|
|
139
|
-
process.exit(result.status ?? 1);
|
package/memory_mac.sh
CHANGED
|
@@ -1862,6 +1862,17 @@ def _resolve_memory_root() -> Path:
|
|
|
1862
1862
|
return script_repo / ".mnemo" / "memory"
|
|
1863
1863
|
|
|
1864
1864
|
|
|
1865
|
+
def _resolve_repo_root(memory_root: Path) -> Path:
|
|
1866
|
+
root = memory_root.resolve()
|
|
1867
|
+
if root.name == "memory" and root.parent.name in {".mnemo", ".cursor"}:
|
|
1868
|
+
return root.parent.parent
|
|
1869
|
+
cwd = Path.cwd().resolve()
|
|
1870
|
+
for candidate in (cwd, *cwd.parents):
|
|
1871
|
+
if candidate.joinpath(".mnemo", "memory").exists() or candidate.joinpath(".cursor", "memory").exists():
|
|
1872
|
+
return candidate
|
|
1873
|
+
return cwd
|
|
1874
|
+
|
|
1875
|
+
|
|
1865
1876
|
def _parse_env_line(raw_line: str):
|
|
1866
1877
|
line = raw_line.strip()
|
|
1867
1878
|
if not line or line.startswith("#"):
|
|
@@ -1882,10 +1893,26 @@ def _parse_env_line(raw_line: str):
|
|
|
1882
1893
|
return key, value
|
|
1883
1894
|
|
|
1884
1895
|
|
|
1885
|
-
def
|
|
1886
|
-
if
|
|
1887
|
-
return
|
|
1888
|
-
|
|
1896
|
+
def _is_missing_env_value(value):
|
|
1897
|
+
if value is None:
|
|
1898
|
+
return True
|
|
1899
|
+
stripped = str(value).strip()
|
|
1900
|
+
if not stripped:
|
|
1901
|
+
return True
|
|
1902
|
+
if stripped.startswith("${env:") and stripped.endswith("}"):
|
|
1903
|
+
return True
|
|
1904
|
+
return False
|
|
1905
|
+
|
|
1906
|
+
|
|
1907
|
+
def _get_env_value(name: str) -> str:
|
|
1908
|
+
value = os.getenv(name)
|
|
1909
|
+
if _is_missing_env_value(value):
|
|
1910
|
+
return ""
|
|
1911
|
+
return str(value).strip()
|
|
1912
|
+
|
|
1913
|
+
|
|
1914
|
+
def _load_project_env(repo_root: Path) -> None:
|
|
1915
|
+
env_path = repo_root / ".env"
|
|
1889
1916
|
if not env_path.exists():
|
|
1890
1917
|
return
|
|
1891
1918
|
try:
|
|
@@ -1894,16 +1921,28 @@ def _load_project_env() -> None:
|
|
|
1894
1921
|
if not parsed:
|
|
1895
1922
|
continue
|
|
1896
1923
|
key, value = parsed
|
|
1897
|
-
|
|
1924
|
+
key = key.lstrip("\ufeff")
|
|
1925
|
+
if _is_missing_env_value(os.getenv(key)):
|
|
1926
|
+
os.environ[key] = value
|
|
1898
1927
|
except OSError:
|
|
1899
1928
|
pass
|
|
1900
1929
|
|
|
1901
1930
|
|
|
1931
|
+
def _resolve_provider() -> str:
|
|
1932
|
+
configured = os.getenv("MNEMO_PROVIDER", "").strip().lower()
|
|
1933
|
+
if configured.startswith("${env:") and configured.endswith("}"):
|
|
1934
|
+
configured = ""
|
|
1935
|
+
if configured in {"openai", "gemini"}:
|
|
1936
|
+
return configured
|
|
1937
|
+
return "gemini" if _get_env_value("GEMINI_API_KEY") else "openai"
|
|
1938
|
+
|
|
1939
|
+
|
|
1902
1940
|
MEM_ROOT = _resolve_memory_root()
|
|
1941
|
+
REPO_ROOT = _resolve_repo_root(MEM_ROOT)
|
|
1903
1942
|
_DB_OVERRIDE = os.getenv("MNEMO_DB_PATH", "").strip()
|
|
1904
1943
|
DB_PATH = Path(_DB_OVERRIDE).expanduser().resolve() if _DB_OVERRIDE else (MEM_ROOT / "mnemo_vector.sqlite")
|
|
1905
|
-
_load_project_env()
|
|
1906
|
-
PROVIDER =
|
|
1944
|
+
_load_project_env(REPO_ROOT)
|
|
1945
|
+
PROVIDER = _resolve_provider()
|
|
1907
1946
|
|
|
1908
1947
|
SKIP_NAMES = {
|
|
1909
1948
|
"README.md",
|
|
@@ -1930,14 +1969,14 @@ def _get_embed_client():
|
|
|
1930
1969
|
return _EMBED_CLIENT
|
|
1931
1970
|
|
|
1932
1971
|
if PROVIDER == "gemini":
|
|
1933
|
-
key =
|
|
1972
|
+
key = _get_env_value("GEMINI_API_KEY")
|
|
1934
1973
|
if not key:
|
|
1935
1974
|
raise RuntimeError("GEMINI_API_KEY is not set")
|
|
1936
1975
|
from google import genai
|
|
1937
1976
|
_EMBED_CLIENT = genai.Client(api_key=key)
|
|
1938
1977
|
return _EMBED_CLIENT
|
|
1939
1978
|
|
|
1940
|
-
key =
|
|
1979
|
+
key = _get_env_value("OPENAI_API_KEY")
|
|
1941
1980
|
if not key:
|
|
1942
1981
|
raise RuntimeError("OPENAI_API_KEY is not set")
|
|
1943
1982
|
from openai import OpenAI
|
package/package.json
CHANGED
|
@@ -77,10 +77,26 @@ def _parse_env_line(raw_line: str) -> tuple[str, str] | None:
|
|
|
77
77
|
return key, value
|
|
78
78
|
|
|
79
79
|
|
|
80
|
+
def _is_missing_env_value(value: str | None) -> bool:
|
|
81
|
+
if value is None:
|
|
82
|
+
return True
|
|
83
|
+
stripped = value.strip()
|
|
84
|
+
if not stripped:
|
|
85
|
+
return True
|
|
86
|
+
# Cursor MCP placeholders can arrive as literal strings in some launches.
|
|
87
|
+
if stripped.startswith("${env:") and stripped.endswith("}"):
|
|
88
|
+
return True
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _get_env_value(name: str) -> str:
|
|
93
|
+
value = os.getenv(name)
|
|
94
|
+
if _is_missing_env_value(value):
|
|
95
|
+
return ""
|
|
96
|
+
return value.strip()
|
|
97
|
+
|
|
98
|
+
|
|
80
99
|
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
100
|
env_path = repo_root / ".env"
|
|
85
101
|
if not env_path.exists():
|
|
86
102
|
return
|
|
@@ -90,17 +106,28 @@ def _load_project_env(repo_root: Path) -> None:
|
|
|
90
106
|
if not parsed:
|
|
91
107
|
continue
|
|
92
108
|
key, value = parsed
|
|
93
|
-
|
|
109
|
+
key = key.lstrip("\ufeff")
|
|
110
|
+
if _is_missing_env_value(os.getenv(key)):
|
|
111
|
+
os.environ[key] = value
|
|
94
112
|
except OSError:
|
|
95
113
|
pass
|
|
96
114
|
|
|
97
115
|
|
|
116
|
+
def _resolve_provider() -> str:
|
|
117
|
+
configured = os.getenv("MNEMO_PROVIDER", "").strip().lower()
|
|
118
|
+
if configured.startswith("${env:") and configured.endswith("}"):
|
|
119
|
+
configured = ""
|
|
120
|
+
if configured in {"openai", "gemini"}:
|
|
121
|
+
return configured
|
|
122
|
+
return "gemini" if _get_env_value("GEMINI_API_KEY") else "openai"
|
|
123
|
+
|
|
124
|
+
|
|
98
125
|
MEM_ROOT = _resolve_memory_root()
|
|
99
126
|
REPO_ROOT = _resolve_repo_root(MEM_ROOT)
|
|
100
127
|
_load_project_env(REPO_ROOT)
|
|
101
128
|
_DB_OVERRIDE = os.getenv("MNEMO_DB_PATH", "").strip()
|
|
102
129
|
DB_PATH = Path(_DB_OVERRIDE).expanduser().resolve() if _DB_OVERRIDE else (MEM_ROOT / "mnemo_vector.sqlite")
|
|
103
|
-
PROVIDER =
|
|
130
|
+
PROVIDER = _resolve_provider()
|
|
104
131
|
|
|
105
132
|
SKIP_NAMES = {
|
|
106
133
|
"README.md", "index.md", "lessons-index.json",
|
|
@@ -158,14 +185,14 @@ def _get_embed_client():
|
|
|
158
185
|
return _EMBED_CLIENT
|
|
159
186
|
|
|
160
187
|
if PROVIDER == "gemini":
|
|
161
|
-
key =
|
|
188
|
+
key = _get_env_value("GEMINI_API_KEY")
|
|
162
189
|
if not key:
|
|
163
190
|
raise RuntimeError("GEMINI_API_KEY is not set")
|
|
164
191
|
from google import genai
|
|
165
192
|
_EMBED_CLIENT = genai.Client(api_key=key)
|
|
166
193
|
return _EMBED_CLIENT
|
|
167
194
|
|
|
168
|
-
key =
|
|
195
|
+
key = _get_env_value("OPENAI_API_KEY")
|
|
169
196
|
if not key:
|
|
170
197
|
raise RuntimeError("OPENAI_API_KEY is not set")
|
|
171
198
|
from openai import OpenAI
|