@dinasor/mnemo-cli 0.0.3 → 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/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.ps1 CHANGED
@@ -154,6 +154,7 @@ Write-Host " Query: scripts\memory\query-memory.ps1 -Query ""..."" [-UseS
154
154
  Write-Host " Lint: scripts\memory\lint-memory.ps1" -ForegroundColor DarkGray
155
155
  Write-Host " Clear: scripts\memory\clear-active.ps1" -ForegroundColor DarkGray
156
156
  Write-Host " Rebuild: scripts\memory\rebuild-memory-index.ps1" -ForegroundColor DarkGray
157
+ Write-Host " Skill: .cursor\skills\mnemo-codebase-optimizer\SKILL.md" -ForegroundColor DarkGray
157
158
  Write-Host ""
158
159
 
159
160
  if ($EnableVector -and (-not $DryRun)) {