@xyrlan/mnemo 0.13.0

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/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # @xyrlan/mnemo (npm wrapper)
2
+
3
+ One-command installer for [mnemo](https://github.com/xyrlan/mnemo).
4
+
5
+ ```
6
+ npx @xyrlan/mnemo install # global, default
7
+ npx @xyrlan/mnemo install --project # only in <cwd>
8
+ npx @xyrlan/mnemo uninstall
9
+ ```
10
+
11
+ This package is a thin Node bootstrap. The actual mnemo runtime is a Python
12
+ package installed automatically via `uv`, `pipx`, or `pip --user` (whichever
13
+ is available). Python 3.8+ is required.
14
+
15
+ See the main project README for usage details.
package/bin/mnemo.js ADDED
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const { runInstall } = require("../lib/runInstall");
5
+ const { runUninstall } = require("../lib/runUninstall");
6
+
7
+ function printHelp() {
8
+ process.stdout.write([
9
+ "Usage: npx @xyrlan/mnemo <command> [flags]",
10
+ "",
11
+ "Commands:",
12
+ " install Install mnemo (Python) + register hooks/slash commands",
13
+ " uninstall Remove mnemo from this scope (vault preserved)",
14
+ " help Show this message",
15
+ "",
16
+ "Install flags:",
17
+ " --global Install globally (default)",
18
+ " --project, --local Install only in the current directory",
19
+ " --vault-root <path> Override vault location",
20
+ " --upgrade Force upgrade if already installed",
21
+ " --yes, -y Non-interactive (default: global)",
22
+ " --quiet Suppress informational output",
23
+ "",
24
+ "Uninstall flags:",
25
+ " --scope global|project|both",
26
+ " --yes, -y",
27
+ " --quiet",
28
+ "",
29
+ ].join("\n"));
30
+ }
31
+
32
+ async function main() {
33
+ const argv = process.argv.slice(2);
34
+ const cmd = argv[0];
35
+ const rest = argv.slice(1);
36
+ switch (cmd) {
37
+ case "install":
38
+ process.exit(await runInstall(rest));
39
+ case "uninstall":
40
+ process.exit(await runUninstall(rest));
41
+ case undefined:
42
+ case "help":
43
+ case "--help":
44
+ case "-h":
45
+ printHelp();
46
+ process.exit(0);
47
+ default:
48
+ process.stderr.write(`unknown command: ${cmd}\n`);
49
+ printHelp();
50
+ process.exit(2);
51
+ }
52
+ }
53
+
54
+ main().catch((err) => {
55
+ process.stderr.write(`fatal: ${err && err.message ? err.message : err}\n`);
56
+ process.exit(1);
57
+ });
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+
3
+ const { execSync, spawnSync } = require("node:child_process");
4
+ const { probe } = require("./detect");
5
+
6
+
7
+ const PIN_SPEC = "mnemo-claude>=0.12,<0.13";
8
+
9
+
10
+ function buildInstallCmd(installer, spec = PIN_SPEC) {
11
+ switch (installer) {
12
+ case "uv": return `uv tool install '${spec}'`;
13
+ case "pipx": return `pipx install '${spec}'`;
14
+ case "pip-user": return `python3 -m pip install --user '${spec}'`;
15
+ default: throw new Error(`unknown installer: ${installer}`);
16
+ }
17
+ }
18
+
19
+
20
+ function buildUpgradeCmd(installer) {
21
+ switch (installer) {
22
+ case "uv": return "uv tool upgrade mnemo-claude";
23
+ case "pipx": return "pipx upgrade mnemo-claude";
24
+ case "pip-user": return "python3 -m pip install --user --upgrade mnemo-claude";
25
+ default: throw new Error(`unknown installer: ${installer}`);
26
+ }
27
+ }
28
+
29
+
30
+ function isAlreadyInstalled(probeFn = probe) {
31
+ return probeFn("mnemo --version");
32
+ }
33
+
34
+
35
+ function runShell(cmd, { quiet = false } = {}) {
36
+ const result = spawnSync("sh", ["-c", cmd], { stdio: quiet ? "ignore" : "inherit" });
37
+ return result.status === null ? 1 : result.status;
38
+ }
39
+
40
+
41
+ function verifyOnPath() {
42
+ try {
43
+ execSync("mnemo --version", { stdio: "ignore" });
44
+ return true;
45
+ } catch (_e) {
46
+ return false;
47
+ }
48
+ }
49
+
50
+
51
+ function pathFixHint(installer) {
52
+ if (installer === "pipx") return "Run `pipx ensurepath` and reopen your shell.";
53
+ if (installer === "uv") return "Run `uv tool update-shell` and reopen your shell.";
54
+ if (installer === "pip-user") {
55
+ if (process.platform === "win32") return "Add %APPDATA%\\Python\\Scripts to PATH.";
56
+ return "Add ~/.local/bin to PATH (e.g. via your shell profile).";
57
+ }
58
+ return "Re-open your shell to refresh PATH.";
59
+ }
60
+
61
+
62
+ module.exports = {
63
+ PIN_SPEC,
64
+ buildInstallCmd,
65
+ buildUpgradeCmd,
66
+ isAlreadyInstalled,
67
+ runShell,
68
+ verifyOnPath,
69
+ pathFixHint,
70
+ };
package/lib/detect.js ADDED
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+
3
+ const { execSync } = require("node:child_process");
4
+
5
+
6
+ function parsePythonVersion(stdout) {
7
+ const m = /Python\s+(\d+)\.(\d+)(?:\.(\d+))?/.exec(stdout || "");
8
+ if (!m) return null;
9
+ return {
10
+ major: parseInt(m[1], 10),
11
+ minor: parseInt(m[2], 10),
12
+ patch: m[3] ? parseInt(m[3], 10) : 0,
13
+ };
14
+ }
15
+
16
+
17
+ function probe(cmd) {
18
+ try {
19
+ execSync(`${cmd} --version`, { stdio: "ignore" });
20
+ return true;
21
+ } catch (_e) {
22
+ return false;
23
+ }
24
+ }
25
+
26
+
27
+ function pickInstaller(probeFn = probe) {
28
+ if (probeFn("uv")) return "uv";
29
+ if (probeFn("pipx")) return "pipx";
30
+ if (probeFn("pip") || probeFn("pip3") || probeFn("python3 -m pip")) return "pip-user";
31
+ return null;
32
+ }
33
+
34
+
35
+ function detectPython() {
36
+ for (const cmd of ["python3", "python"]) {
37
+ try {
38
+ const out = execSync(`${cmd} --version`, { encoding: "utf8" });
39
+ const v = parsePythonVersion(out);
40
+ if (v && (v.major > 3 || (v.major === 3 && v.minor >= 8))) {
41
+ return { version: v, command: cmd };
42
+ }
43
+ } catch (_e) {
44
+ continue;
45
+ }
46
+ }
47
+ return null;
48
+ }
49
+
50
+
51
+ function isPep668(stderr) {
52
+ return /externally-managed-environment/i.test(stderr || "");
53
+ }
54
+
55
+
56
+ function pep668InstallHint() {
57
+ if (process.platform === "darwin") return "brew install pipx";
58
+ try {
59
+ const osr = require("node:fs").readFileSync("/etc/os-release", "utf8");
60
+ if (/ID=(?:debian|ubuntu)/i.test(osr)) return "sudo apt install pipx";
61
+ if (/ID=(?:fedora|rhel|centos)/i.test(osr)) return "sudo dnf install pipx";
62
+ } catch (_e) { /* ignore */ }
63
+ return "install pipx via your system package manager";
64
+ }
65
+
66
+
67
+ module.exports = { parsePythonVersion, probe, pickInstaller, detectPython, isPep668, pep668InstallHint };
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+
3
+ const C = {
4
+ green: "\x1b[32m",
5
+ yellow: "\x1b[33m",
6
+ red: "\x1b[31m",
7
+ dim: "\x1b[2m",
8
+ reset: "\x1b[0m",
9
+ };
10
+
11
+ function ok(msg) { process.stdout.write(`${C.green}✓${C.reset} ${msg}\n`); }
12
+ function warn(msg) { process.stdout.write(`${C.yellow}⚠${C.reset} ${msg}\n`); }
13
+ function err(msg) { process.stderr.write(`${C.red}✗${C.reset} ${msg}\n`); }
14
+ function info(msg) { process.stdout.write(`${C.dim}↻${C.reset} ${msg}\n`); }
15
+ function plain(msg) { process.stdout.write(`${msg}\n`); }
16
+
17
+ module.exports = { ok, warn, err, info, plain };
package/lib/prompt.js ADDED
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+
3
+ const readline = require("node:readline");
4
+
5
+
6
+ function promptScope({ stdin = process.stdin, stdout = process.stdout } = {}) {
7
+ const rl = readline.createInterface({ input: stdin, output: stdout, terminal: false });
8
+ const question = [
9
+ "",
10
+ "Where should mnemo install hooks?",
11
+ " [1] Global — every Claude Code session (recommended)",
12
+ " [2] Project — only in this directory",
13
+ "choice [1]: ",
14
+ ].join("\n");
15
+ return new Promise((resolve) => {
16
+ rl.question(question, (answer) => {
17
+ rl.close();
18
+ const trimmed = (answer || "").trim();
19
+ if (trimmed === "2") return resolve("project");
20
+ return resolve("global");
21
+ });
22
+ });
23
+ }
24
+
25
+
26
+ module.exports = { promptScope };
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+
3
+ const { detectPython, pickInstaller, pep668InstallHint } = require("./detect");
4
+ const {
5
+ PIN_SPEC,
6
+ buildInstallCmd,
7
+ buildUpgradeCmd,
8
+ isAlreadyInstalled,
9
+ runShell,
10
+ verifyOnPath,
11
+ pathFixHint,
12
+ } = require("./bootstrap");
13
+ const { promptScope } = require("./prompt");
14
+ const { buildInitArgs, runMnemo } = require("./runMnemo");
15
+ const m = require("./messages");
16
+
17
+
18
+ function parseFlags(argv) {
19
+ const flags = { scope: null, vaultRoot: null, upgrade: false, yes: false, quiet: false };
20
+ for (let i = 0; i < argv.length; i++) {
21
+ const a = argv[i];
22
+ if (a === "--global") flags.scope = "global";
23
+ else if (a === "--project" || a === "--local") flags.scope = "project";
24
+ else if (a === "--upgrade") flags.upgrade = true;
25
+ else if (a === "--yes" || a === "-y") flags.yes = true;
26
+ else if (a === "--quiet") flags.quiet = true;
27
+ else if (a === "--vault-root") { flags.vaultRoot = argv[++i]; }
28
+ }
29
+ return flags;
30
+ }
31
+
32
+
33
+ async function runInstall(argv) {
34
+ const flags = parseFlags(argv);
35
+
36
+ const py = detectPython();
37
+ if (!py) {
38
+ m.err("Python 3.8+ not found.");
39
+ m.plain(" → Install Python 3.8 or newer (https://www.python.org/downloads/) and retry.");
40
+ return 1;
41
+ }
42
+ if (!flags.quiet) m.ok(`Python ${py.version.major}.${py.version.minor} detected`);
43
+
44
+ const installer = pickInstaller();
45
+ if (!installer) {
46
+ m.err("No Python installer (uv, pipx, or pip) found on PATH.");
47
+ m.plain(` → ${pep668InstallHint()}`);
48
+ return 1;
49
+ }
50
+ if (!flags.quiet) m.ok(`installer: ${installer}`);
51
+
52
+ const installed = isAlreadyInstalled();
53
+ if (installed && !flags.upgrade) {
54
+ if (!flags.quiet) m.ok("mnemo already installed. Skipping installer step. (use --upgrade to force)");
55
+ } else {
56
+ const cmd = installed ? buildUpgradeCmd(installer) : buildInstallCmd(installer, PIN_SPEC);
57
+ if (!flags.quiet) m.info(`Running: ${cmd}`);
58
+ const status = runShell(cmd, { quiet: flags.quiet });
59
+ if (status !== 0) {
60
+ m.err(`Installer command failed (exit ${status}).`);
61
+ return status;
62
+ }
63
+ if (!verifyOnPath()) {
64
+ m.err("`mnemo --version` not reachable on PATH after install.");
65
+ m.plain(` → ${pathFixHint(installer)}`);
66
+ return 2;
67
+ }
68
+ if (!flags.quiet) m.ok("mnemo on PATH");
69
+ }
70
+
71
+ let scope = flags.scope;
72
+ if (!scope) {
73
+ if (flags.yes) scope = "global";
74
+ else scope = await promptScope();
75
+ }
76
+
77
+ const args = buildInitArgs({ scope, vaultRoot: flags.vaultRoot, quiet: flags.quiet, yes: true });
78
+ const status = runMnemo(args, { quiet: flags.quiet });
79
+ if (status !== 0) return status;
80
+
81
+ if (!flags.quiet) {
82
+ if (scope === "project") {
83
+ m.plain(`\nDone. Launch \`claude\` in ${process.cwd()} to activate the local hooks.`);
84
+ } else {
85
+ m.plain("\nDone. Open Claude Code anywhere; mnemo is active.");
86
+ }
87
+ }
88
+ return 0;
89
+ }
90
+
91
+
92
+ module.exports = { runInstall, parseFlags };
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+
3
+ const { spawnSync } = require("node:child_process");
4
+
5
+
6
+ function buildInitArgs({ scope, vaultRoot, quiet, yes = true }) {
7
+ const args = ["init"];
8
+ if (scope === "project") args.push("--project");
9
+ if (yes) args.push("--yes");
10
+ if (quiet) args.push("--quiet");
11
+ if (vaultRoot) { args.push("--vault-root", vaultRoot); }
12
+ return args;
13
+ }
14
+
15
+
16
+ function buildUninstallArgs({ scope, quiet, yes = true }) {
17
+ const args = ["uninstall"];
18
+ if (scope === "project") args.push("--project");
19
+ if (yes) args.push("--yes");
20
+ if (quiet) args.push("--quiet");
21
+ return args;
22
+ }
23
+
24
+
25
+ function runMnemo(args, { quiet = false } = {}) {
26
+ const result = spawnSync("mnemo", args, { stdio: quiet ? "ignore" : "inherit" });
27
+ return result.status === null ? 1 : result.status;
28
+ }
29
+
30
+
31
+ module.exports = { buildInitArgs, buildUninstallArgs, runMnemo };
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+ const os = require("node:os");
6
+
7
+ const { pickInstaller } = require("./detect");
8
+ const { runShell } = require("./bootstrap");
9
+ const { buildUninstallArgs, runMnemo } = require("./runMnemo");
10
+ const m = require("./messages");
11
+
12
+
13
+ function parseUninstallFlags(argv) {
14
+ const f = { scope: null, yes: false, quiet: false };
15
+ for (let i = 0; i < argv.length; i++) {
16
+ const a = argv[i];
17
+ if (a === "--scope") f.scope = argv[++i];
18
+ else if (a === "--yes" || a === "-y") f.yes = true;
19
+ else if (a === "--quiet") f.quiet = true;
20
+ }
21
+ return f;
22
+ }
23
+
24
+
25
+ function _hasMnemoInSettings(settingsPath) {
26
+ if (!fs.existsSync(settingsPath)) return false;
27
+ try {
28
+ const data = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
29
+ const hooks = (data && data.hooks) || {};
30
+ for (const ev of Object.keys(hooks)) {
31
+ const entries = hooks[ev] || [];
32
+ for (const e of entries) {
33
+ for (const h of (e.hooks || [])) {
34
+ if (typeof h.command === "string" && h.command.includes("mnemo.hooks.")) return true;
35
+ }
36
+ }
37
+ }
38
+ return false;
39
+ } catch (_e) {
40
+ return false;
41
+ }
42
+ }
43
+
44
+
45
+ function detectScopes() {
46
+ return {
47
+ project: _hasMnemoInSettings(path.join(process.cwd(), ".claude", "settings.json")),
48
+ global: _hasMnemoInSettings(path.join(os.homedir(), ".claude", "settings.json")),
49
+ };
50
+ }
51
+
52
+
53
+ function resolveUninstallScope(flag, present, nonInteractive) {
54
+ if (flag) {
55
+ if (!["project", "global", "both"].includes(flag)) {
56
+ throw new Error(`invalid --scope: ${flag}`);
57
+ }
58
+ return flag;
59
+ }
60
+ const both = present.project && present.global;
61
+ if (both && nonInteractive) {
62
+ throw new Error("both project and global installs detected; pass --scope explicitly");
63
+ }
64
+ if (present.project && !present.global) return "project";
65
+ if (present.global && !present.project) return "global";
66
+ return null;
67
+ }
68
+
69
+
70
+ async function _promptUninstallScope() {
71
+ const readline = require("node:readline");
72
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
73
+ const question = [
74
+ "",
75
+ "Both project and global mnemo installs detected.",
76
+ " [1] project (this directory)",
77
+ " [2] global",
78
+ " [3] both",
79
+ "choice [1]: ",
80
+ ].join("\n");
81
+ return new Promise((resolve) => {
82
+ rl.question(question, (answer) => {
83
+ rl.close();
84
+ const t = (answer || "").trim();
85
+ if (t === "2") return resolve("global");
86
+ if (t === "3") return resolve("both");
87
+ return resolve("project");
88
+ });
89
+ });
90
+ }
91
+
92
+
93
+ async function runUninstall(argv) {
94
+ const flags = parseUninstallFlags(argv);
95
+ const present = detectScopes();
96
+ if (!present.project && !present.global) {
97
+ m.warn("No mnemo install detected (no hooks in project or global settings).");
98
+ return 0;
99
+ }
100
+ let scope;
101
+ try {
102
+ scope = resolveUninstallScope(flags.scope, present, flags.yes) || await _promptUninstallScope();
103
+ } catch (e) {
104
+ m.err(e.message);
105
+ return 2;
106
+ }
107
+
108
+ const scopes = scope === "both" ? ["project", "global"] : [scope];
109
+ for (const s of scopes) {
110
+ const status = runMnemo(buildUninstallArgs({ scope: s, quiet: flags.quiet, yes: true }), { quiet: flags.quiet });
111
+ if (status !== 0) return status;
112
+ }
113
+
114
+ const installer = pickInstaller();
115
+ if (installer) {
116
+ const cmd = installer === "uv" ? "uv tool uninstall mnemo-claude"
117
+ : installer === "pipx" ? "pipx uninstall mnemo-claude"
118
+ : "python3 -m pip uninstall -y --user mnemo-claude";
119
+ if (!flags.quiet) m.info(`Running: ${cmd}`);
120
+ runShell(cmd, { quiet: flags.quiet });
121
+ }
122
+ if (!flags.quiet) m.plain("\nDone. Vault preserved.");
123
+ return 0;
124
+ }
125
+
126
+
127
+ module.exports = { runUninstall, parseUninstallFlags, resolveUninstallScope, detectScopes };
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@xyrlan/mnemo",
3
+ "version": "0.13.0",
4
+ "description": "One-command installer for mnemo (the Obsidian that populates itself).",
5
+ "bin": {
6
+ "mnemo": "bin/mnemo.js"
7
+ },
8
+ "engines": {
9
+ "node": ">=18"
10
+ },
11
+ "license": "MIT",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/xyrlan/mnemo.git"
15
+ },
16
+ "homepage": "https://github.com/xyrlan/mnemo",
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "scripts": {
21
+ "test": "node --test test/"
22
+ },
23
+ "files": [
24
+ "bin/",
25
+ "lib/",
26
+ "README.md"
27
+ ]
28
+ }