@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.
- package/README.md +175 -0
- package/bin/mneme.mjs +275 -0
- package/package.json +31 -0
- package/src/commands/auto.mjs +654 -0
- package/src/commands/compact.mjs +137 -0
- package/src/commands/doctor.mjs +91 -0
- package/src/commands/facts.mjs +148 -0
- package/src/commands/init.mjs +344 -0
- package/src/commands/propose.mjs +150 -0
- package/src/commands/review.mjs +210 -0
- package/src/commands/status.mjs +164 -0
- package/src/opencode-client.mjs +126 -0
- package/src/templates/AGENTS.md +259 -0
- package/src/templates/facts-architecture.md +6 -0
- package/src/templates/facts-invariants.md +6 -0
- package/src/templates/facts-performance_rules.md +5 -0
- package/src/templates/facts-pitfalls.md +5 -0
- package/src/templates/gitignore +23 -0
- package/src/templates/opencode-prompt.md +48 -0
- package/src/utils.mjs +90 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mneme init — Initialize three-layer memory architecture in the current directory.
|
|
3
|
+
*
|
|
4
|
+
* Steps:
|
|
5
|
+
* 1. Install dependencies (git, dolt, bd)
|
|
6
|
+
* 2. git init (if needed)
|
|
7
|
+
* 3. Scaffold .openclaw/facts/, .opencode/prompt.md, AGENTS.md, .gitignore
|
|
8
|
+
* 4. Start dolt server + bd init
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from "node:fs";
|
|
12
|
+
import { join, dirname } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { has, run, runLive, log, color, getPlatform } from "../utils.mjs";
|
|
15
|
+
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = dirname(__filename);
|
|
18
|
+
const TEMPLATES_DIR = join(__dirname, "..", "templates");
|
|
19
|
+
|
|
20
|
+
// ── Template scaffolding ────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Map of destination path (relative to project root) -> template filename.
|
|
24
|
+
*/
|
|
25
|
+
const SCAFFOLD = {
|
|
26
|
+
"AGENTS.md": "AGENTS.md",
|
|
27
|
+
".opencode/prompt.md": "opencode-prompt.md",
|
|
28
|
+
".openclaw/facts/architecture.md": "facts-architecture.md",
|
|
29
|
+
".openclaw/facts/invariants.md": "facts-invariants.md",
|
|
30
|
+
".openclaw/facts/performance_rules.md": "facts-performance_rules.md",
|
|
31
|
+
".openclaw/facts/pitfalls.md": "facts-pitfalls.md",
|
|
32
|
+
".gitignore": "gitignore",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function scaffoldFiles() {
|
|
36
|
+
let created = 0;
|
|
37
|
+
let skipped = 0;
|
|
38
|
+
|
|
39
|
+
for (const [dest, templateName] of Object.entries(SCAFFOLD)) {
|
|
40
|
+
if (existsSync(dest)) {
|
|
41
|
+
log.ok(`${dest} ${color.dim("(already exists)")}`);
|
|
42
|
+
skipped++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Ensure parent directory exists
|
|
47
|
+
const dir = dirname(dest);
|
|
48
|
+
if (dir !== "." && !existsSync(dir)) {
|
|
49
|
+
mkdirSync(dir, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const templatePath = join(TEMPLATES_DIR, templateName);
|
|
53
|
+
const content = readFileSync(templatePath, "utf-8");
|
|
54
|
+
|
|
55
|
+
// For .gitignore, we append rather than overwrite if it already exists
|
|
56
|
+
writeFileSync(dest, content, "utf-8");
|
|
57
|
+
log.ok(`${dest} ${color.dim("(created)")}`);
|
|
58
|
+
created++;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// If .gitignore already existed, ensure our entries are in it
|
|
62
|
+
if (existsSync(".gitignore") && skipped > 0) {
|
|
63
|
+
const existing = readFileSync(".gitignore", "utf-8");
|
|
64
|
+
const templateContent = readFileSync(
|
|
65
|
+
join(TEMPLATES_DIR, "gitignore"),
|
|
66
|
+
"utf-8",
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// Check if our marker entries are present
|
|
70
|
+
if (!existing.includes(".beads/dolt/")) {
|
|
71
|
+
appendFileSync(".gitignore", `\n# Added by mneme init\n${templateContent}`);
|
|
72
|
+
log.ok(`.gitignore ${color.dim("(mneme entries appended)")}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { created, skipped };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Dependency installation ─────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
function installGit() {
|
|
82
|
+
if (has("git")) {
|
|
83
|
+
const ver = run("git --version") ?? "";
|
|
84
|
+
log.ok(`git ${color.dim(ver)}`);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
log.info("Installing git...");
|
|
89
|
+
const { os } = getPlatform();
|
|
90
|
+
|
|
91
|
+
if (os === "darwin") {
|
|
92
|
+
runLive("xcode-select --install 2>/dev/null || true");
|
|
93
|
+
} else if (os === "linux") {
|
|
94
|
+
if (has("apt-get")) runLive("sudo apt-get update -qq && sudo apt-get install -y -qq git");
|
|
95
|
+
else if (has("dnf")) runLive("sudo dnf install -y git");
|
|
96
|
+
else if (has("yum")) runLive("sudo yum install -y git");
|
|
97
|
+
else if (has("pacman")) runLive("sudo pacman -S --noconfirm git");
|
|
98
|
+
else {
|
|
99
|
+
log.fail("Cannot auto-install git. Please install manually.");
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
log.fail(`Unsupported platform for auto-install: ${os}. Please install git manually.`);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return has("git");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function checkOpencode() {
|
|
111
|
+
if (has("opencode")) {
|
|
112
|
+
const ver = run("opencode --version 2>/dev/null | head -1") ?? "";
|
|
113
|
+
log.ok(`opencode ${color.dim(ver)}`);
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
log.warn(
|
|
118
|
+
"opencode not installed — mneme wraps opencode for the AI agent experience",
|
|
119
|
+
);
|
|
120
|
+
log.info(" Install: https://opencode.ai");
|
|
121
|
+
log.info(' After installing, run `mneme` to start or `mneme doctor` to verify');
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function installDolt() {
|
|
126
|
+
if (has("dolt")) {
|
|
127
|
+
const ver = run("dolt version") ?? "";
|
|
128
|
+
log.ok(`dolt ${color.dim(ver)}`);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
log.info("Installing dolt...");
|
|
133
|
+
const code = runLive(
|
|
134
|
+
"curl -fsSL https://github.com/dolthub/dolt/releases/latest/download/install.sh | bash",
|
|
135
|
+
{ timeout: 120_000 },
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
if (code === 0 && has("dolt")) {
|
|
139
|
+
log.ok("dolt installed");
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
log.fail("Failed to install dolt. See https://github.com/dolthub/dolt");
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function installBd() {
|
|
148
|
+
if (has("bd")) {
|
|
149
|
+
const ver = run("bd version 2>/dev/null | head -1") ?? "";
|
|
150
|
+
log.ok(`bd ${color.dim(ver)}`);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
log.info("Installing bd (beads)...");
|
|
155
|
+
|
|
156
|
+
// Strategy: brew > npm > GitHub release binary
|
|
157
|
+
if (has("brew")) {
|
|
158
|
+
log.info(" via Homebrew...");
|
|
159
|
+
if (runLive("brew install beads") === 0 && has("bd")) {
|
|
160
|
+
log.ok("bd installed via brew");
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (has("npm")) {
|
|
166
|
+
log.info(" via npm...");
|
|
167
|
+
if (runLive("npm install -g @beads/bd") === 0 && has("bd")) {
|
|
168
|
+
log.ok("bd installed via npm");
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Fallback: download binary from GitHub releases
|
|
174
|
+
log.info(" via GitHub release binary...");
|
|
175
|
+
const { os, arch } = getPlatform();
|
|
176
|
+
const osMap = { linux: "linux", darwin: "darwin", win32: "windows" };
|
|
177
|
+
const archMap = { x64: "amd64", arm64: "arm64" };
|
|
178
|
+
const osStr = osMap[os];
|
|
179
|
+
const archStr = archMap[arch];
|
|
180
|
+
|
|
181
|
+
if (!osStr || !archStr) {
|
|
182
|
+
log.fail(`Unsupported platform: ${os}/${arch}. Please install bd manually.`);
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Fetch latest version tag
|
|
187
|
+
const latestJson = run(
|
|
188
|
+
'curl -fsSL https://api.github.com/repos/steveyegge/beads/releases/latest',
|
|
189
|
+
{ timeout: 30_000 },
|
|
190
|
+
);
|
|
191
|
+
if (!latestJson) {
|
|
192
|
+
log.fail("Failed to fetch beads release info.");
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let version;
|
|
197
|
+
try {
|
|
198
|
+
const data = JSON.parse(latestJson);
|
|
199
|
+
version = data.tag_name?.replace(/^v/, "");
|
|
200
|
+
} catch {
|
|
201
|
+
log.fail("Failed to parse beads release info.");
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const ext = os === "win32" ? "zip" : "tar.gz";
|
|
206
|
+
const url = `https://github.com/steveyegge/beads/releases/download/v${version}/beads_${version}_${osStr}_${archStr}.${ext}`;
|
|
207
|
+
|
|
208
|
+
const dlCmd =
|
|
209
|
+
ext === "tar.gz"
|
|
210
|
+
? `curl -fsSL "${url}" -o /tmp/beads.tar.gz && tar xzf /tmp/beads.tar.gz -C /tmp/ && install /tmp/bd /usr/local/bin/bd`
|
|
211
|
+
: `curl -fsSL "${url}" -o /tmp/beads.zip && unzip -o /tmp/beads.zip -d /tmp/beads && install /tmp/beads/bd.exe /usr/local/bin/bd`;
|
|
212
|
+
|
|
213
|
+
if (runLive(dlCmd, { timeout: 60_000 }) === 0 && has("bd")) {
|
|
214
|
+
log.ok("bd installed via GitHub release");
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
log.fail("Failed to install bd. See https://github.com/steveyegge/beads");
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── Dolt server + bd init ───────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
function ensureDoltServer() {
|
|
225
|
+
// Check if bd can already talk to dolt
|
|
226
|
+
const test = run("bd list --status=open 2>&1");
|
|
227
|
+
if (test !== null && !test.includes("unreachable") && !test.includes("connection refused")) {
|
|
228
|
+
log.ok("dolt server already running");
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
log.info("Starting dolt server...");
|
|
233
|
+
const dataDir = existsSync(".beads/dolt") ? ".beads/dolt" : `${process.env.HOME}/.dolt/databases`;
|
|
234
|
+
|
|
235
|
+
if (!existsSync(dataDir)) {
|
|
236
|
+
mkdirSync(dataDir, { recursive: true });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Start in background
|
|
240
|
+
run(
|
|
241
|
+
`nohup dolt sql-server --host 127.0.0.1 --port 3307 --data-dir "${dataDir}" > /tmp/dolt-server.log 2>&1 &`,
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// Wait for server to be ready (up to 10s)
|
|
245
|
+
for (let i = 0; i < 10; i++) {
|
|
246
|
+
run("sleep 1");
|
|
247
|
+
const check = run("bd list --status=open 2>&1");
|
|
248
|
+
if (check !== null && !check.includes("unreachable") && !check.includes("connection refused")) {
|
|
249
|
+
log.ok("dolt server started");
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
log.fail("dolt server failed to start. Check /tmp/dolt-server.log");
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function initBeads() {
|
|
259
|
+
if (existsSync(".beads/config.yaml")) {
|
|
260
|
+
log.ok(`.beads/ ${color.dim("(already initialized)")}`);
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
log.info("Initializing beads...");
|
|
265
|
+
const code = runLive("bd init");
|
|
266
|
+
if (code === 0) {
|
|
267
|
+
log.ok("beads initialized");
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
log.fail("bd init failed");
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function initGit() {
|
|
276
|
+
if (existsSync(".git")) {
|
|
277
|
+
log.ok(`.git/ ${color.dim("(already initialized)")}`);
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
log.info("Initializing git...");
|
|
282
|
+
if (runLive("git init -q") === 0) {
|
|
283
|
+
log.ok("git initialized");
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
log.fail("git init failed");
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ── Main ────────────────────────────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
export async function init() {
|
|
294
|
+
console.log(`
|
|
295
|
+
${color.bold("mneme init")} — Three-layer memory architecture for AI agents
|
|
296
|
+
`);
|
|
297
|
+
|
|
298
|
+
const { os, arch } = getPlatform();
|
|
299
|
+
log.info(`Platform: ${os} ${arch}`);
|
|
300
|
+
|
|
301
|
+
// Step 1: Dependencies
|
|
302
|
+
log.step(1, 4, "Install dependencies");
|
|
303
|
+
const gitOk = installGit();
|
|
304
|
+
if (!gitOk) {
|
|
305
|
+
log.fail("git is required. Aborting.");
|
|
306
|
+
process.exit(1);
|
|
307
|
+
}
|
|
308
|
+
checkOpencode();
|
|
309
|
+
const doltOk = installDolt();
|
|
310
|
+
const bdOk = installBd();
|
|
311
|
+
|
|
312
|
+
// Step 2: Git init
|
|
313
|
+
log.step(2, 4, "Initialize git");
|
|
314
|
+
initGit();
|
|
315
|
+
|
|
316
|
+
// Step 3: Scaffold files
|
|
317
|
+
log.step(3, 4, "Scaffold project structure");
|
|
318
|
+
const { created, skipped } = scaffoldFiles();
|
|
319
|
+
log.info(` ${created} file(s) created, ${skipped} already existed`);
|
|
320
|
+
|
|
321
|
+
// Step 4: Dolt + Beads
|
|
322
|
+
log.step(4, 4, "Initialize beads");
|
|
323
|
+
if (doltOk && bdOk) {
|
|
324
|
+
const serverOk = ensureDoltServer();
|
|
325
|
+
if (serverOk) {
|
|
326
|
+
initBeads();
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
log.warn("Skipping beads init (dolt or bd not installed)");
|
|
330
|
+
log.warn("Run `mneme doctor` after installing dependencies to check status");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Done
|
|
334
|
+
console.log(`
|
|
335
|
+
${color.bold("===============================")}
|
|
336
|
+
${color.green("mneme init complete")}
|
|
337
|
+
${color.bold("===============================")}
|
|
338
|
+
|
|
339
|
+
${color.bold("Next steps:")}
|
|
340
|
+
mneme # Start coding with AI agent
|
|
341
|
+
bd ready # Check available tasks
|
|
342
|
+
mneme doctor # Verify everything is healthy
|
|
343
|
+
`);
|
|
344
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mneme propose — Propose a new fact for OpenClaw.
|
|
3
|
+
*
|
|
4
|
+
* Creates a pending proposal in `.openclaw/proposals/` that requires
|
|
5
|
+
* human review via `mneme review` before being written to facts.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* mneme propose --file=architecture --content="..." --reason="..."
|
|
9
|
+
* mneme propose --file=pitfalls --content="..." --reason="..."
|
|
10
|
+
*
|
|
11
|
+
* Options:
|
|
12
|
+
* --file Target facts file name (without .md extension)
|
|
13
|
+
* --content The fact content to append
|
|
14
|
+
* --reason Why this qualifies as a long-term fact
|
|
15
|
+
* --action append (default) | create
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { existsSync, mkdirSync, writeFileSync, readdirSync } from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { createHash } from "node:crypto";
|
|
21
|
+
import { log, color } from "../utils.mjs";
|
|
22
|
+
|
|
23
|
+
const PROPOSALS_DIR = ".openclaw/proposals";
|
|
24
|
+
const FACTS_DIR = ".openclaw/facts";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse --key=value and --key "value" style args.
|
|
28
|
+
*/
|
|
29
|
+
function parseArgs(args) {
|
|
30
|
+
const result = {};
|
|
31
|
+
for (let i = 0; i < args.length; i++) {
|
|
32
|
+
const arg = args[i];
|
|
33
|
+
if (!arg.startsWith("--")) continue;
|
|
34
|
+
|
|
35
|
+
const eqIdx = arg.indexOf("=");
|
|
36
|
+
if (eqIdx !== -1) {
|
|
37
|
+
// --key=value
|
|
38
|
+
const key = arg.slice(2, eqIdx);
|
|
39
|
+
result[key] = arg.slice(eqIdx + 1);
|
|
40
|
+
} else {
|
|
41
|
+
// --key value
|
|
42
|
+
const key = arg.slice(2);
|
|
43
|
+
const next = args[i + 1];
|
|
44
|
+
if (next && !next.startsWith("--")) {
|
|
45
|
+
result[key] = next;
|
|
46
|
+
i++;
|
|
47
|
+
} else {
|
|
48
|
+
result[key] = true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Generate a short proposal ID from timestamp + content hash.
|
|
57
|
+
*/
|
|
58
|
+
function generateId(content) {
|
|
59
|
+
const ts = Date.now().toString(36);
|
|
60
|
+
const hash = createHash("sha256")
|
|
61
|
+
.update(content + ts)
|
|
62
|
+
.digest("hex")
|
|
63
|
+
.slice(0, 4);
|
|
64
|
+
return `p-${ts.slice(-4)}${hash}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function propose(args) {
|
|
68
|
+
const opts = parseArgs(args);
|
|
69
|
+
|
|
70
|
+
// Validate required fields
|
|
71
|
+
if (!opts.file) {
|
|
72
|
+
console.error(`
|
|
73
|
+
Usage: mneme propose --file=<facts-file> --content="<fact>" --reason="<why>"
|
|
74
|
+
|
|
75
|
+
Options:
|
|
76
|
+
--file Target facts file (e.g. architecture, invariants, pitfalls)
|
|
77
|
+
--content The fact content to append
|
|
78
|
+
--reason Why this qualifies as a long-term fact
|
|
79
|
+
--action append (default) | create
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
mneme propose --file=pitfalls --content="bd export does not exist in v0.56.1" --reason="Verified by testing; agents will hit this repeatedly"
|
|
83
|
+
`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!opts.content) {
|
|
88
|
+
log.fail('--content is required. What fact do you want to propose?');
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!opts.reason) {
|
|
93
|
+
log.fail('--reason is required. Why is this a long-term fact?');
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const action = opts.action || "append";
|
|
98
|
+
if (action !== "append" && action !== "create") {
|
|
99
|
+
log.fail(`Invalid action: ${action}. Use "append" or "create".`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Validate target file exists (for append) or doesn't exist (for create)
|
|
104
|
+
const targetFile = opts.file.endsWith(".md") ? opts.file : `${opts.file}.md`;
|
|
105
|
+
const targetPath = join(FACTS_DIR, targetFile);
|
|
106
|
+
|
|
107
|
+
if (action === "append" && !existsSync(targetPath)) {
|
|
108
|
+
log.fail(`Facts file not found: ${targetPath}`);
|
|
109
|
+
const available = existsSync(FACTS_DIR)
|
|
110
|
+
? readdirSync(FACTS_DIR)
|
|
111
|
+
.filter((f) => f.endsWith(".md"))
|
|
112
|
+
.map((f) => f.replace(/\.md$/, ""))
|
|
113
|
+
: [];
|
|
114
|
+
if (available.length > 0) {
|
|
115
|
+
console.log(`Available files: ${available.join(", ")}`);
|
|
116
|
+
}
|
|
117
|
+
console.log('Use --action=create to propose a new facts file.');
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Create proposals directory
|
|
122
|
+
if (!existsSync(PROPOSALS_DIR)) {
|
|
123
|
+
mkdirSync(PROPOSALS_DIR, { recursive: true });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Build proposal
|
|
127
|
+
const id = generateId(opts.content);
|
|
128
|
+
const proposal = {
|
|
129
|
+
id,
|
|
130
|
+
file: targetFile,
|
|
131
|
+
action,
|
|
132
|
+
content: opts.content,
|
|
133
|
+
reason: opts.reason,
|
|
134
|
+
status: "pending",
|
|
135
|
+
created: new Date().toISOString(),
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Write proposal file
|
|
139
|
+
const proposalPath = join(PROPOSALS_DIR, `${id}.json`);
|
|
140
|
+
writeFileSync(proposalPath, JSON.stringify(proposal, null, 2) + "\n", "utf-8");
|
|
141
|
+
|
|
142
|
+
log.ok(`Proposal created: ${color.bold(id)}`);
|
|
143
|
+
console.log(` File: ${targetFile}`);
|
|
144
|
+
console.log(` Action: ${action}`);
|
|
145
|
+
console.log(` Content: ${opts.content.slice(0, 80)}${opts.content.length > 80 ? "..." : ""}`);
|
|
146
|
+
console.log(` Reason: ${opts.reason}`);
|
|
147
|
+
console.log();
|
|
148
|
+
console.log(color.dim(`Review with: mneme review`));
|
|
149
|
+
console.log(color.dim(`Approve: mneme review ${id} --approve`));
|
|
150
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mneme review — Review, approve, or reject OpenClaw fact proposals.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* mneme review List all pending proposals
|
|
6
|
+
* mneme review <id> --approve Approve and write to facts
|
|
7
|
+
* mneme review <id> --reject Reject proposal
|
|
8
|
+
* mneme review <id> Show proposal details
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
existsSync,
|
|
13
|
+
readdirSync,
|
|
14
|
+
readFileSync,
|
|
15
|
+
writeFileSync,
|
|
16
|
+
appendFileSync,
|
|
17
|
+
mkdirSync,
|
|
18
|
+
} from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { log, color } from "../utils.mjs";
|
|
21
|
+
|
|
22
|
+
const PROPOSALS_DIR = ".openclaw/proposals";
|
|
23
|
+
const FACTS_DIR = ".openclaw/facts";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Load all proposals, optionally filtered by status.
|
|
27
|
+
*/
|
|
28
|
+
function loadProposals(status) {
|
|
29
|
+
if (!existsSync(PROPOSALS_DIR)) return [];
|
|
30
|
+
|
|
31
|
+
return readdirSync(PROPOSALS_DIR)
|
|
32
|
+
.filter((f) => f.endsWith(".json"))
|
|
33
|
+
.map((f) => {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(readFileSync(join(PROPOSALS_DIR, f), "utf-8"));
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
.filter(Boolean)
|
|
41
|
+
.filter((p) => !status || p.status === status)
|
|
42
|
+
.sort((a, b) => new Date(a.created) - new Date(b.created));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Load a single proposal by ID.
|
|
47
|
+
*/
|
|
48
|
+
function loadProposal(id) {
|
|
49
|
+
const filePath = join(PROPOSALS_DIR, `${id}.json`);
|
|
50
|
+
if (!existsSync(filePath)) return null;
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(readFileSync(filePath, "utf-8"));
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Save a proposal back to disk.
|
|
60
|
+
*/
|
|
61
|
+
function saveProposal(proposal) {
|
|
62
|
+
const filePath = join(PROPOSALS_DIR, `${proposal.id}.json`);
|
|
63
|
+
writeFileSync(filePath, JSON.stringify(proposal, null, 2) + "\n", "utf-8");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* List pending proposals.
|
|
68
|
+
*/
|
|
69
|
+
function listPending() {
|
|
70
|
+
const pending = loadProposals("pending");
|
|
71
|
+
|
|
72
|
+
if (pending.length === 0) {
|
|
73
|
+
log.ok("No pending proposals");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(
|
|
78
|
+
`\n${color.bold("Pending proposals")} (${pending.length})\n`,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
for (const p of pending) {
|
|
82
|
+
const date = p.created.split("T")[0];
|
|
83
|
+
console.log(
|
|
84
|
+
` ${color.yellow("○")} ${color.bold(p.id)} → ${p.file} (${p.action}) ${color.dim(date)}`,
|
|
85
|
+
);
|
|
86
|
+
console.log(` ${p.content.slice(0, 100)}${p.content.length > 100 ? "..." : ""}`);
|
|
87
|
+
console.log(` ${color.dim(`Reason: ${p.reason}`)}`);
|
|
88
|
+
console.log();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log(color.dim(" Approve: mneme review <id> --approve"));
|
|
92
|
+
console.log(color.dim(" Reject: mneme review <id> --reject"));
|
|
93
|
+
console.log(color.dim(" Detail: mneme review <id>"));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Show full detail of a proposal.
|
|
98
|
+
*/
|
|
99
|
+
function showProposal(proposal) {
|
|
100
|
+
const statusColor =
|
|
101
|
+
proposal.status === "pending"
|
|
102
|
+
? color.yellow
|
|
103
|
+
: proposal.status === "approved"
|
|
104
|
+
? color.green
|
|
105
|
+
: color.red;
|
|
106
|
+
|
|
107
|
+
console.log(`
|
|
108
|
+
${color.bold("Proposal")} ${color.bold(proposal.id)} [${statusColor(proposal.status.toUpperCase())}]
|
|
109
|
+
|
|
110
|
+
Target: ${proposal.file} (${proposal.action})
|
|
111
|
+
Created: ${proposal.created}
|
|
112
|
+
|
|
113
|
+
${color.bold("Content:")}
|
|
114
|
+
${proposal.content}
|
|
115
|
+
|
|
116
|
+
${color.bold("Reason:")}
|
|
117
|
+
${proposal.reason}
|
|
118
|
+
`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Approve a proposal: append/create fact content.
|
|
123
|
+
*/
|
|
124
|
+
function approveProposal(proposal) {
|
|
125
|
+
const targetPath = join(FACTS_DIR, proposal.file);
|
|
126
|
+
|
|
127
|
+
if (proposal.action === "append") {
|
|
128
|
+
if (!existsSync(targetPath)) {
|
|
129
|
+
log.fail(`Target file not found: ${targetPath}`);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
// Append with a blank line separator
|
|
133
|
+
appendFileSync(targetPath, `\n${proposal.content}\n`, "utf-8");
|
|
134
|
+
log.ok(`Appended to ${proposal.file}`);
|
|
135
|
+
} else if (proposal.action === "create") {
|
|
136
|
+
if (existsSync(targetPath)) {
|
|
137
|
+
log.warn(`File already exists: ${targetPath} — appending instead`);
|
|
138
|
+
appendFileSync(targetPath, `\n${proposal.content}\n`, "utf-8");
|
|
139
|
+
} else {
|
|
140
|
+
const dir = join(FACTS_DIR);
|
|
141
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
142
|
+
writeFileSync(targetPath, proposal.content + "\n", "utf-8");
|
|
143
|
+
log.ok(`Created ${proposal.file}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Update proposal status
|
|
148
|
+
proposal.status = "approved";
|
|
149
|
+
proposal.reviewed = new Date().toISOString();
|
|
150
|
+
saveProposal(proposal);
|
|
151
|
+
|
|
152
|
+
log.ok(`Proposal ${color.bold(proposal.id)} approved`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Reject a proposal.
|
|
157
|
+
*/
|
|
158
|
+
function rejectProposal(proposal) {
|
|
159
|
+
proposal.status = "rejected";
|
|
160
|
+
proposal.reviewed = new Date().toISOString();
|
|
161
|
+
saveProposal(proposal);
|
|
162
|
+
|
|
163
|
+
log.ok(`Proposal ${color.bold(proposal.id)} rejected`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Main ────────────────────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
export async function review(args) {
|
|
169
|
+
const doApprove = args.includes("--approve");
|
|
170
|
+
const doReject = args.includes("--reject");
|
|
171
|
+
const id = args.find((a) => !a.startsWith("-"));
|
|
172
|
+
|
|
173
|
+
// No ID → list pending
|
|
174
|
+
if (!id) {
|
|
175
|
+
listPending();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Load proposal
|
|
180
|
+
const proposal = loadProposal(id);
|
|
181
|
+
if (!proposal) {
|
|
182
|
+
log.fail(`Proposal not found: ${id}`);
|
|
183
|
+
const pending = loadProposals("pending");
|
|
184
|
+
if (pending.length > 0) {
|
|
185
|
+
console.log(
|
|
186
|
+
`\nPending: ${pending.map((p) => p.id).join(", ")}`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Action: approve or reject
|
|
193
|
+
if (doApprove) {
|
|
194
|
+
if (proposal.status !== "pending") {
|
|
195
|
+
log.warn(`Proposal already ${proposal.status}`);
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
showProposal(proposal);
|
|
199
|
+
approveProposal(proposal);
|
|
200
|
+
} else if (doReject) {
|
|
201
|
+
if (proposal.status !== "pending") {
|
|
202
|
+
log.warn(`Proposal already ${proposal.status}`);
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
rejectProposal(proposal);
|
|
206
|
+
} else {
|
|
207
|
+
// Just show detail
|
|
208
|
+
showProposal(proposal);
|
|
209
|
+
}
|
|
210
|
+
}
|