@xqli02/mneme 0.1.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.
@@ -0,0 +1,137 @@
1
+ /**
2
+ * mneme compact — Pre-compaction persistence checklist.
3
+ *
4
+ * Checks that state is properly persisted before context compaction:
5
+ * - In-progress beads have recent notes
6
+ * - No uncommitted changes
7
+ * - No unpushed commits
8
+ * - Suggests actions if anything is out of sync
9
+ */
10
+
11
+ import { existsSync } from "node:fs";
12
+ import { run, log, color } from "../utils.mjs";
13
+
14
+ export async function compact() {
15
+ console.log(
16
+ `\n${color.bold("mneme compact")} — pre-compaction persistence check\n`,
17
+ );
18
+
19
+ let warnings = 0;
20
+ let passes = 0;
21
+
22
+ // Check 1: Uncommitted changes
23
+ console.log(color.bold("Git state:"));
24
+ const status = run("git status --porcelain 2>&1");
25
+ if (status === null) {
26
+ log.warn("Not a git repository");
27
+ warnings++;
28
+ } else if (status === "") {
29
+ log.ok("Working tree clean");
30
+ passes++;
31
+ } else {
32
+ const lines = status.split("\n").filter(Boolean);
33
+ log.warn(
34
+ `${lines.length} uncommitted change(s) — commit before compaction`,
35
+ );
36
+ for (const line of lines.slice(0, 5)) {
37
+ console.log(` ${line}`);
38
+ }
39
+ if (lines.length > 5) {
40
+ console.log(color.dim(` ... and ${lines.length - 5} more`));
41
+ }
42
+ warnings++;
43
+ }
44
+
45
+ // Check 2: Unpushed commits
46
+ const unpushed = run("git log @{u}..HEAD --oneline 2>&1");
47
+ if (unpushed && !unpushed.includes("no upstream") && unpushed !== "") {
48
+ const count = unpushed.split("\n").filter(Boolean).length;
49
+ log.warn(`${count} unpushed commit(s) — push before compaction`);
50
+ warnings++;
51
+ } else if (unpushed === "") {
52
+ log.ok("All commits pushed");
53
+ passes++;
54
+ }
55
+
56
+ // Check 3: In-progress beads
57
+ console.log(`\n${color.bold("Beads state:")}`);
58
+ if (!existsSync(".beads/config.yaml")) {
59
+ log.warn("Beads not initialized");
60
+ warnings++;
61
+ } else {
62
+ const ipOut = run("bd list --status=in_progress 2>&1");
63
+ if (!ipOut || ipOut.includes("No issues")) {
64
+ log.ok("No in-progress beads (nothing to persist)");
65
+ passes++;
66
+ } else {
67
+ const beadLines = ipOut
68
+ .split("\n")
69
+ .filter((l) => /\b(mneme|bd)-[a-z0-9]+\b/.test(l) || /^[○◐●]/.test(l.trim()));
70
+
71
+ if (beadLines.length === 0) {
72
+ log.ok("No in-progress beads");
73
+ passes++;
74
+ } else {
75
+ log.warn(
76
+ `${beadLines.length} in-progress bead(s) — update notes with current progress:`,
77
+ );
78
+ for (const line of beadLines) {
79
+ console.log(` ${line.trim()}`);
80
+ }
81
+ console.log(
82
+ color.dim(
83
+ '\n Use: bd update <id> --notes="current progress..."',
84
+ ),
85
+ );
86
+ warnings++;
87
+ }
88
+ }
89
+
90
+ // Check for open beads that might need status update
91
+ const openOut = run("bd list --status=open 2>&1");
92
+ if (openOut && !openOut.includes("No issues")) {
93
+ const openLines = openOut
94
+ .split("\n")
95
+ .filter((l) => /\b(mneme|bd)-[a-z0-9]+\b/.test(l) || /^[○◐●]/.test(l.trim()));
96
+ if (openLines.length > 0) {
97
+ log.info(
98
+ `${openLines.length} open bead(s) — review if any should be closed or updated`,
99
+ );
100
+ }
101
+ }
102
+ }
103
+
104
+ // Check 4: OpenClaw facts
105
+ console.log(`\n${color.bold("OpenClaw state:")}`);
106
+ if (existsSync(".openclaw/facts/")) {
107
+ log.ok("Facts directory exists");
108
+ log.info(
109
+ "Review: any new long-term facts discovered this session? Propose with human confirmation.",
110
+ );
111
+ passes++;
112
+ } else {
113
+ log.warn("No .openclaw/facts/ directory");
114
+ warnings++;
115
+ }
116
+
117
+ // Summary
118
+ console.log();
119
+ if (warnings === 0) {
120
+ log.ok(
121
+ color.bold(
122
+ "All clear — safe to compact. State is fully persisted.",
123
+ ),
124
+ );
125
+ } else {
126
+ log.warn(
127
+ color.bold(
128
+ `${warnings} warning(s) — persist state before allowing compaction.`,
129
+ ),
130
+ );
131
+ console.log(
132
+ color.dim(
133
+ "\n Remember: you can lose reasoning, but never lose state or facts.",
134
+ ),
135
+ );
136
+ }
137
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * mneme doctor — Check dependencies and project health.
3
+ */
4
+
5
+ import { has, run, log, color } from "../utils.mjs";
6
+ import { existsSync } from "node:fs";
7
+ import { readdirSync } from "node:fs";
8
+
9
+ /**
10
+ * Check a single dependency. Returns true if OK.
11
+ */
12
+ function checkCmd(name, versionCmd) {
13
+ if (!has(name)) {
14
+ log.fail(`${name} not installed`);
15
+ return false;
16
+ }
17
+ const ver = run(versionCmd) ?? "unknown";
18
+ log.ok(`${name} ${color.dim(ver)}`);
19
+ return true;
20
+ }
21
+
22
+ /**
23
+ * Check if dolt server is reachable by bd.
24
+ */
25
+ function checkDoltServer() {
26
+ if (!has("bd")) return false;
27
+ const result = run("bd list --status=open 2>&1");
28
+ if (result !== null && !result.includes("unreachable") && !result.includes("Error")) {
29
+ log.ok("dolt server reachable");
30
+ return true;
31
+ }
32
+ log.warn("dolt server not running or not reachable");
33
+ return false;
34
+ }
35
+
36
+ /**
37
+ * Check project structure.
38
+ */
39
+ function checkStructure() {
40
+ const checks = [
41
+ [".git/", "git repository"],
42
+ [".openclaw/facts/", "OpenClaw facts directory"],
43
+ [".opencode/prompt.md", "OpenCode session prompt"],
44
+ ["AGENTS.md", "Agent behavior rules"],
45
+ [".beads/", "Beads data directory"],
46
+ ];
47
+
48
+ let allOk = true;
49
+ for (const [path, label] of checks) {
50
+ if (existsSync(path)) {
51
+ log.ok(label);
52
+ } else {
53
+ log.warn(`${label} ${color.dim(`(${path} not found)`)}`);
54
+ allOk = false;
55
+ }
56
+ }
57
+
58
+ // Check facts files
59
+ if (existsSync(".openclaw/facts/")) {
60
+ const files = readdirSync(".openclaw/facts/").filter((f) =>
61
+ f.endsWith(".md"),
62
+ );
63
+ log.info(` ${files.length} facts file(s): ${files.join(", ")}`);
64
+ }
65
+
66
+ return allOk;
67
+ }
68
+
69
+ export async function doctor() {
70
+ console.log(`\n${color.bold("mneme doctor")} — checking health\n`);
71
+
72
+ console.log(color.bold("Dependencies:"));
73
+ const gitOk = checkCmd("git", "git --version");
74
+ const opencodeOk = checkCmd("opencode", "opencode --version 2>/dev/null | head -1");
75
+ const doltOk = checkCmd("dolt", "dolt version");
76
+ const bdOk = checkCmd("bd", "bd version 2>/dev/null | head -1");
77
+ const serverOk = checkDoltServer();
78
+
79
+ console.log(`\n${color.bold("Project structure:")}`);
80
+ const structOk = checkStructure();
81
+
82
+ console.log();
83
+ if (gitOk && opencodeOk && doltOk && bdOk && serverOk && structOk) {
84
+ log.ok(color.bold("All checks passed"));
85
+ } else {
86
+ log.warn(
87
+ color.bold("Some checks failed. Run `mneme init` to fix."),
88
+ );
89
+ process.exit(1);
90
+ }
91
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * mneme facts — View and manage OpenClaw facts.
3
+ *
4
+ * Usage:
5
+ * mneme facts List all facts files with summaries
6
+ * mneme facts <name> Show contents of a specific facts file
7
+ * mneme facts --stats Show statistics (line counts, budget usage)
8
+ */
9
+
10
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { log, color } from "../utils.mjs";
13
+
14
+ const FACTS_DIR = ".openclaw/facts";
15
+ const PROPOSALS_DIR = ".openclaw/proposals";
16
+ const LINE_BUDGET_PER_FILE = 200;
17
+ const LINE_BUDGET_TOTAL = 800;
18
+
19
+ /**
20
+ * List all facts files with line counts.
21
+ */
22
+ function listFacts(showStats) {
23
+ if (!existsSync(FACTS_DIR)) {
24
+ log.fail(
25
+ ".openclaw/facts/ not found — run `mneme init` to create it.",
26
+ );
27
+ process.exit(1);
28
+ }
29
+
30
+ const files = readdirSync(FACTS_DIR)
31
+ .filter((f) => f.endsWith(".md"))
32
+ .sort();
33
+
34
+ if (files.length === 0) {
35
+ log.warn("No facts files found. Create them with `mneme init`.");
36
+ return;
37
+ }
38
+
39
+ let totalLines = 0;
40
+
41
+ console.log(color.bold("\nOpenClaw Facts"));
42
+ console.log(color.dim("──────────────────────────────────────────"));
43
+
44
+ for (const file of files) {
45
+ const filePath = join(FACTS_DIR, file);
46
+ const content = readFileSync(filePath, "utf-8");
47
+ const lines = content.split("\n").length;
48
+ totalLines += lines;
49
+
50
+ const name = file.replace(/\.md$/, "");
51
+ const pct = Math.round((lines / LINE_BUDGET_PER_FILE) * 100);
52
+
53
+ // Extract first heading as description
54
+ const heading = content
55
+ .split("\n")
56
+ .find((l) => l.startsWith("# "));
57
+ const desc = heading ? heading.replace(/^#\s*/, "").trim() : "";
58
+
59
+ const budgetColor =
60
+ pct > 80 ? color.red : pct > 60 ? color.yellow : color.green;
61
+
62
+ console.log(
63
+ ` ${color.green("●")} ${color.bold(name)} ${color.dim(desc)}`,
64
+ );
65
+ if (showStats) {
66
+ console.log(
67
+ ` ${lines} lines ${budgetColor(`(${pct}% of ${LINE_BUDGET_PER_FILE}-line budget)`)}`,
68
+ );
69
+ }
70
+ }
71
+
72
+ console.log(color.dim("──────────────────────────────────────────"));
73
+
74
+ const totalPct = Math.round((totalLines / LINE_BUDGET_TOTAL) * 100);
75
+ const totalColor =
76
+ totalPct > 80 ? color.red : totalPct > 60 ? color.yellow : color.green;
77
+
78
+ console.log(
79
+ ` ${files.length} file(s), ${totalColor(`${totalLines}/${LINE_BUDGET_TOTAL} lines (${totalPct}%)`)}`,
80
+ );
81
+ console.log();
82
+
83
+ if (totalPct > 80) {
84
+ log.warn(
85
+ "Facts approaching size budget — review and prune stale entries.",
86
+ );
87
+ }
88
+
89
+ // Show pending proposals count
90
+ if (existsSync(PROPOSALS_DIR)) {
91
+ const pending = readdirSync(PROPOSALS_DIR)
92
+ .filter((f) => f.endsWith(".json"))
93
+ .filter((f) => {
94
+ try {
95
+ const p = JSON.parse(readFileSync(join(PROPOSALS_DIR, f), "utf-8"));
96
+ return p.status === "pending";
97
+ } catch {
98
+ return false;
99
+ }
100
+ });
101
+ if (pending.length > 0) {
102
+ log.warn(
103
+ `${pending.length} pending proposal(s) — run ${color.bold("mneme review")} to review`,
104
+ );
105
+ }
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Show contents of a specific facts file.
111
+ */
112
+ function showFact(name) {
113
+ // Allow with or without .md extension
114
+ const fileName = name.endsWith(".md") ? name : `${name}.md`;
115
+ const filePath = join(FACTS_DIR, fileName);
116
+
117
+ if (!existsSync(filePath)) {
118
+ log.fail(`Facts file not found: ${filePath}`);
119
+
120
+ // Suggest available files
121
+ if (existsSync(FACTS_DIR)) {
122
+ const available = readdirSync(FACTS_DIR)
123
+ .filter((f) => f.endsWith(".md"))
124
+ .map((f) => f.replace(/\.md$/, ""));
125
+ if (available.length > 0) {
126
+ console.log(`\nAvailable: ${available.join(", ")}`);
127
+ }
128
+ }
129
+ process.exit(1);
130
+ }
131
+
132
+ const content = readFileSync(filePath, "utf-8");
133
+ console.log();
134
+ console.log(content);
135
+ }
136
+
137
+ // ── Main ────────────────────────────────────────────────────────────────────
138
+
139
+ export async function facts(args) {
140
+ const showStats = args.includes("--stats") || args.includes("-s");
141
+ const filtered = args.filter((a) => !a.startsWith("-") && a !== "facts");
142
+
143
+ if (filtered.length > 0) {
144
+ showFact(filtered[0]);
145
+ } else {
146
+ listFacts(showStats);
147
+ }
148
+ }