@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 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.4...HEAD
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
- # Installs into your current project folder
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
- # Optional flags (cross-platform)
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.4
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 = require("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 packageRoot = path.resolve(__dirname, "..");
8
- const cwd = process.cwd();
9
- const rawArgs = process.argv.slice(2);
10
- const wantsHelp = rawArgs.includes("--help") || rawArgs.includes("-h");
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
- function printHelp() {
13
- console.log(`Mnemo CLI
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
- Usage:
16
- npx @dinasor/mnemo-cli@latest [options]
54
+ const TICK = `${BGR}✓${R}`;
55
+ const CROSS = `${BRE}✗${R}`;
56
+ const WARN = `${BYE}⚠${R}`;
57
+ const ARROW = `${BCY}›${R}`;
17
58
 
18
- Options:
19
- --dry-run
20
- --force
21
- --enable-vector
22
- --vector-provider <openai|gemini>
23
- --project-name <name>
24
- --repo-root <path> (defaults to current directory)
25
- --help
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 fail(message) {
30
- console.error(`[mnemo] ${message}`);
31
- process.exit(1);
78
+ function divider() {
79
+ process.stdout.write(` ${DI}${"─".repeat(W - 2)}${R}\n`);
32
80
  }
33
81
 
34
- function mapWindowsArgs(args) {
35
- const mapped = [];
36
- let hasRepoRoot = false;
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
- for (let i = 0; i < args.length; i += 1) {
39
- const arg = args[i];
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
- if (arg === "--dry-run") {
42
- mapped.push("-DryRun");
43
- continue;
44
- }
45
- if (arg === "--force") {
46
- mapped.push("-Force");
47
- continue;
48
- }
49
- if (arg === "--enable-vector") {
50
- mapped.push("-EnableVector");
51
- continue;
52
- }
53
- if (arg === "--vector-provider") {
54
- const value = args[i + 1];
55
- if (!value) fail("Missing value for --vector-provider");
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
- if (arg.toLowerCase() === "-reporoot") {
77
- hasRepoRoot = true;
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
- mapped.push(arg);
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
- if (!hasRepoRoot && !wantsHelp) {
83
- mapped.push("-RepoRoot", cwd);
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 mapped;
195
+ return { cmd: null, ver: null, ok: false };
86
196
  }
87
197
 
88
- function mapPosixArgs(args) {
89
- const mapped = [];
90
- let hasRepoRoot = false;
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
- for (let i = 0; i < args.length; i += 1) {
93
- const arg = args[i];
94
- mapped.push(arg);
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
- if (arg === "--repo-root") {
97
- if (!args[i + 1]) fail("Missing value for --repo-root");
98
- mapped.push(args[i + 1]);
99
- hasRepoRoot = true;
100
- i += 1;
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
- if (!hasRepoRoot && !wantsHelp) {
105
- mapped.push("--repo-root", cwd);
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 mapped;
355
+ return flags;
108
356
  }
109
357
 
110
- if (wantsHelp) {
111
- printHelp();
112
- process.exit(0);
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
- if (process.platform === "win32") {
116
- const installer = path.join(packageRoot, "memory.ps1");
117
- if (!fs.existsSync(installer)) {
118
- fail(`Installer not found at ${installer}`);
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
- const installer = path.join(packageRoot, "memory_mac.sh");
133
- if (!fs.existsSync(installer)) {
134
- fail(`Installer not found at ${installer}`);
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 _load_project_env() -> None:
1886
- if os.getenv("GEMINI_API_KEY"):
1887
- return
1888
- env_path = Path(".env")
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
- os.environ.setdefault(key, value)
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 = os.getenv("MNEMO_PROVIDER", "gemini" if os.getenv("GEMINI_API_KEY") else "openai").lower()
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 = os.getenv("GEMINI_API_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 = os.getenv("OPENAI_API_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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dinasor/mnemo-cli",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Mnemo installer CLI for the current project folder.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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
- os.environ.setdefault(key, value)
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 = os.getenv("MNEMO_PROVIDER", "gemini" if os.getenv("GEMINI_API_KEY") else "openai").lower()
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 = os.getenv("GEMINI_API_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 = os.getenv("OPENAI_API_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