@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/CHANGELOG.md +33 -1
- package/README.md +55 -3
- package/VERSION +1 -1
- package/bin/mnemo.js +513 -101
- package/memory.ps1 +1 -0
- package/memory_mac.sh +70 -10
- package/package.json +1 -1
- package/scripts/memory/installer/features/gitignore_setup.ps1 +69 -68
- package/scripts/memory/installer/features/memory_scaffold.ps1 +10 -0
- package/scripts/memory/installer/templates/mnemo_vector.py +34 -7
- package/scripts/memory/installer/templates/skills/mnemo-codebase-optimizer/SKILL.md +137 -0
- package/scripts/memory/installer/templates/skills/mnemo-codebase-optimizer/reference.md +138 -0
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.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)) {
|