@membank/cli 0.8.0 → 0.10.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/dist/index.mjs +341 -42
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { cancel, confirm, intro, isCancel, multiselect, note, outro } from "@clack/prompts";
|
|
3
|
-
import { DatabaseManager, EmbeddingService, MIGRATIONS, MemoryRepository, MemoryTypeSchema, MemoryTypeSchema as MemoryTypeSchema$1, ProjectRepository, QueryEngine, SessionContextBuilder, TagsJsonSchema as TagsRowSchema, resolveProject, runScopeToProjectsMigration } from "@membank/core";
|
|
3
|
+
import { DatabaseManager, EmbeddingService, MIGRATIONS, MemoryRepository, MemoryTypeSchema, MemoryTypeSchema as MemoryTypeSchema$1, PIN_BUDGET_THRESHOLD, ProjectRepository, QueryEngine, SessionContextBuilder, SynthesisRepository, TagsJsonSchema as TagsRowSchema, resolveProject, runScopeToProjectsMigration } from "@membank/core";
|
|
4
4
|
import { startServer } from "@membank/mcp";
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
import ora from "ora";
|
|
8
8
|
import { z } from "zod";
|
|
9
|
-
import { startDashboard } from "@membank/dashboard";
|
|
10
9
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { homedir, tmpdir } from "node:os";
|
|
11
11
|
import { dirname, join } from "node:path";
|
|
12
|
+
import { startDashboard } from "@membank/dashboard";
|
|
12
13
|
import Table from "cli-table3";
|
|
13
14
|
import { execFile } from "node:child_process";
|
|
14
15
|
import { promisify } from "node:util";
|
|
15
|
-
import { homedir, tmpdir } from "node:os";
|
|
16
16
|
import { EventEmitter } from "node:events";
|
|
17
17
|
import { pipeline } from "@huggingface/transformers";
|
|
18
18
|
import { createInterface } from "node:readline";
|
|
@@ -77,6 +77,80 @@ async function addCommand(content, options, formatter, db, embeddingService) {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
//#endregion
|
|
80
|
+
//#region src/config/manager.ts
|
|
81
|
+
function defaultConfigPath() {
|
|
82
|
+
return join(homedir(), ".membank", "config.json");
|
|
83
|
+
}
|
|
84
|
+
function readConfigFile(path) {
|
|
85
|
+
if (!existsSync(path)) return {};
|
|
86
|
+
try {
|
|
87
|
+
const raw = readFileSync(path, "utf-8");
|
|
88
|
+
return JSON.parse(raw);
|
|
89
|
+
} catch {
|
|
90
|
+
return {};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const ConfigManager = {
|
|
94
|
+
getConfigPath() {
|
|
95
|
+
return defaultConfigPath();
|
|
96
|
+
},
|
|
97
|
+
load() {
|
|
98
|
+
return readConfigFile(ConfigManager.getConfigPath());
|
|
99
|
+
},
|
|
100
|
+
get(key) {
|
|
101
|
+
const config = ConfigManager.load();
|
|
102
|
+
return key.split(".").reduce((obj, part) => {
|
|
103
|
+
if (obj !== null && typeof obj === "object" && part in obj) return obj[part];
|
|
104
|
+
}, config);
|
|
105
|
+
},
|
|
106
|
+
set(key, value) {
|
|
107
|
+
const config = ConfigManager.load();
|
|
108
|
+
const parts = key.split(".");
|
|
109
|
+
let cursor = config;
|
|
110
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
111
|
+
const part = parts[i];
|
|
112
|
+
if (!(part in cursor) || typeof cursor[part] !== "object" || cursor[part] === null) cursor[part] = {};
|
|
113
|
+
cursor = cursor[part];
|
|
114
|
+
}
|
|
115
|
+
const lastPart = parts[parts.length - 1];
|
|
116
|
+
cursor[lastPart] = value;
|
|
117
|
+
ConfigManager.write(config);
|
|
118
|
+
},
|
|
119
|
+
write(config) {
|
|
120
|
+
writeFileSync(ConfigManager.getConfigPath(), JSON.stringify(config, null, 2), "utf-8");
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/commands/config.ts
|
|
125
|
+
function parseValue(raw) {
|
|
126
|
+
if (raw === "true") return true;
|
|
127
|
+
if (raw === "false") return false;
|
|
128
|
+
const n = Number(raw);
|
|
129
|
+
if (!Number.isNaN(n) && raw.trim() !== "") return n;
|
|
130
|
+
return raw;
|
|
131
|
+
}
|
|
132
|
+
function configGetCommand(key, formatter) {
|
|
133
|
+
const value = ConfigManager.get(key);
|
|
134
|
+
if (formatter.isJson) process.stdout.write(`${JSON.stringify({
|
|
135
|
+
key,
|
|
136
|
+
value
|
|
137
|
+
})}\n`);
|
|
138
|
+
else process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
139
|
+
}
|
|
140
|
+
function configSetCommand(key, rawValue, formatter) {
|
|
141
|
+
const value = parseValue(rawValue);
|
|
142
|
+
ConfigManager.set(key, value);
|
|
143
|
+
if (formatter.isJson) process.stdout.write(`${JSON.stringify({
|
|
144
|
+
key,
|
|
145
|
+
value
|
|
146
|
+
})}\n`);
|
|
147
|
+
else process.stdout.write(`Set ${key} = ${JSON.stringify(value)}\n`);
|
|
148
|
+
}
|
|
149
|
+
function configShowCommand(formatter) {
|
|
150
|
+
const config = ConfigManager.load();
|
|
151
|
+
process.stdout.write(`${JSON.stringify(config, null, formatter.isJson ? 0 : 2)}\n`);
|
|
152
|
+
}
|
|
153
|
+
//#endregion
|
|
80
154
|
//#region src/commands/dashboard.ts
|
|
81
155
|
async function dashboardCommand(opts) {
|
|
82
156
|
await startDashboard({ port: opts.port !== void 0 ? PortSchema.parse(opts.port) : void 0 });
|
|
@@ -103,7 +177,6 @@ function exportCommand(db, formatter, opts) {
|
|
|
103
177
|
sourceHarness: row.source,
|
|
104
178
|
accessCount: row.access_count,
|
|
105
179
|
pinned: row.pinned !== 0,
|
|
106
|
-
needsReview: row.needs_review !== 0,
|
|
107
180
|
createdAt: row.created_at,
|
|
108
181
|
updatedAt: row.updated_at,
|
|
109
182
|
embedding: row.embedding !== null ? Buffer.from(row.embedding).toString("base64") : null
|
|
@@ -147,12 +220,12 @@ async function importCommand(filePath, db, formatter, prompt) {
|
|
|
147
220
|
if (formatter.isJson) process.stdout.write(`${JSON.stringify({ found: count })}\n`);
|
|
148
221
|
else process.stdout.write(`Found ${count} memories to import.\n`);
|
|
149
222
|
if (!await prompt.confirm("Import?")) return;
|
|
150
|
-
const insertMemory = db.db.prepare(`INSERT OR REPLACE INTO memories (id, content, type, tags, source, access_count, pinned,
|
|
151
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?,
|
|
223
|
+
const insertMemory = db.db.prepare(`INSERT OR REPLACE INTO memories (id, content, type, tags, source, access_count, pinned, created_at, updated_at)
|
|
224
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
152
225
|
const insertEmbedding = db.db.prepare(`INSERT OR REPLACE INTO embeddings (rowid, embedding) SELECT m.rowid, ? FROM memories m WHERE m.id = ?`);
|
|
153
226
|
db.db.transaction(() => {
|
|
154
227
|
for (const rec of parseResult.data.memories) {
|
|
155
|
-
insertMemory.run(rec.id, rec.content, rec.type, JSON.stringify(rec.tags ?? []), rec.sourceHarness ?? null, rec.accessCount ?? 0, rec.pinned ? 1 : 0, rec.
|
|
228
|
+
insertMemory.run(rec.id, rec.content, rec.type, JSON.stringify(rec.tags ?? []), rec.sourceHarness ?? null, rec.accessCount ?? 0, rec.pinned ? 1 : 0, rec.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(), rec.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString());
|
|
156
229
|
if (rec.embedding !== null && rec.embedding !== void 0) {
|
|
157
230
|
const buf = Buffer.from(rec.embedding, "base64");
|
|
158
231
|
insertEmbedding.run(buf, rec.id);
|
|
@@ -172,10 +245,13 @@ function formatContext(ctx) {
|
|
|
172
245
|
const parts = [];
|
|
173
246
|
const statParts = Object.entries(ctx.stats).filter(([, count]) => count > 0).map(([type, count]) => `${count} ${type}${count !== 1 ? "s" : ""}`);
|
|
174
247
|
if (statParts.length > 0) parts.push(`<memory-stats>\n${statParts.join(", ")}\n</memory-stats>`);
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
248
|
+
if (ctx.synthesis !== void 0 && ctx.synthesis.length > 0) parts.push(`<synthesis>\n${ctx.synthesis}\n</synthesis>`);
|
|
249
|
+
else {
|
|
250
|
+
const allPinned = [...ctx.pinnedGlobal, ...ctx.pinnedProject];
|
|
251
|
+
if (allPinned.length > 0) {
|
|
252
|
+
const memLines = allPinned.map((m) => ` <memory type="${m.type}">${xmlEscape(m.content)}</memory>`);
|
|
253
|
+
parts.push(`<pinned-memories>\n${memLines.join("\n")}\n</pinned-memories>`);
|
|
254
|
+
}
|
|
179
255
|
}
|
|
180
256
|
parts.push(`<memory-guidance>\n${MEMORY_GUIDANCE}\n</memory-guidance>`);
|
|
181
257
|
return parts.join("\n");
|
|
@@ -194,11 +270,19 @@ function outputAdditionalContext(text, harness, eventName) {
|
|
|
194
270
|
}
|
|
195
271
|
process.stdout.write(`${text}\n`);
|
|
196
272
|
}
|
|
273
|
+
function pickBestSynthesis(globalSynthesis, projectSynthesis) {
|
|
274
|
+
return projectSynthesis ?? globalSynthesis;
|
|
275
|
+
}
|
|
197
276
|
async function buildText() {
|
|
198
277
|
const resolved = await resolveProject();
|
|
199
278
|
const db = DatabaseManager.open();
|
|
200
279
|
try {
|
|
201
|
-
|
|
280
|
+
const builder = new SessionContextBuilder(db);
|
|
281
|
+
const synthRepo = new SynthesisRepository(db);
|
|
282
|
+
const globalRow = synthRepo.getSynthesis("global");
|
|
283
|
+
const projectRow = synthRepo.getSynthesis(resolved.hash);
|
|
284
|
+
const synthesis = pickBestSynthesis(globalRow?.inFlightSince === null ? globalRow.content : void 0, projectRow?.inFlightSince === null ? projectRow.content : void 0);
|
|
285
|
+
return formatContext(builder.getSessionContext(resolved.hash, synthesis));
|
|
202
286
|
} finally {
|
|
203
287
|
db.close();
|
|
204
288
|
}
|
|
@@ -223,6 +307,10 @@ async function injectCommand(opts) {
|
|
|
223
307
|
await handleEvent(harness, "UserPromptSubmit");
|
|
224
308
|
return;
|
|
225
309
|
}
|
|
310
|
+
if (opts.event === "session-stop" || opts.event === "stop") {
|
|
311
|
+
await handleEvent(harness, "Stop");
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
226
314
|
process.exit(0);
|
|
227
315
|
}
|
|
228
316
|
//#endregion
|
|
@@ -313,6 +401,24 @@ async function queryCommand(queryText, options, formatter) {
|
|
|
313
401
|
}
|
|
314
402
|
}
|
|
315
403
|
//#endregion
|
|
404
|
+
//#region src/commands/review.ts
|
|
405
|
+
async function reviewCommand(opts, formatter) {
|
|
406
|
+
const db = DatabaseManager.open();
|
|
407
|
+
try {
|
|
408
|
+
const repo = new MemoryRepository(db, new EmbeddingService(), new ProjectRepository(db));
|
|
409
|
+
if (opts.resolve !== void 0) {
|
|
410
|
+
repo.resolveReviewEvents(opts.resolve);
|
|
411
|
+
if (!formatter.isJson) process.stdout.write(`Resolved review events for memory ${opts.resolve}\n`);
|
|
412
|
+
else process.stdout.write(`${JSON.stringify({ resolved: opts.resolve })}\n`);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
const flagged = repo.listFlagged();
|
|
416
|
+
formatter.outputReview(flagged);
|
|
417
|
+
} finally {
|
|
418
|
+
db.close();
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
//#endregion
|
|
316
422
|
//#region src/commands/stats.ts
|
|
317
423
|
async function statsCommand(formatter) {
|
|
318
424
|
const db = DatabaseManager.open();
|
|
@@ -324,6 +430,69 @@ async function statsCommand(formatter) {
|
|
|
324
430
|
}
|
|
325
431
|
}
|
|
326
432
|
//#endregion
|
|
433
|
+
//#region src/commands/synthesize.ts
|
|
434
|
+
function hasSynthesesTable(db) {
|
|
435
|
+
return db.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='syntheses'").get() !== void 0;
|
|
436
|
+
}
|
|
437
|
+
function synthesizeShowCommand(opts, formatter) {
|
|
438
|
+
const db = DatabaseManager.open();
|
|
439
|
+
try {
|
|
440
|
+
if (!hasSynthesesTable(db)) {
|
|
441
|
+
if (formatter.isJson) process.stdout.write(`${JSON.stringify(null)}\n`);
|
|
442
|
+
else process.stdout.write("No synthesis data available.\n");
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const scope = opts.scope ?? "global";
|
|
446
|
+
const row = db.db.prepare("SELECT * FROM syntheses WHERE scope = ? ORDER BY synthesized_at DESC LIMIT 1").get(scope);
|
|
447
|
+
if (row === void 0) {
|
|
448
|
+
if (formatter.isJson) process.stdout.write(`${JSON.stringify(null)}\n`);
|
|
449
|
+
else process.stdout.write(`No synthesis found for scope: ${scope}\n`);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (formatter.isJson) process.stdout.write(`${JSON.stringify(row)}\n`);
|
|
453
|
+
else {
|
|
454
|
+
process.stdout.write(`\nScope: ${row.scope}\n`);
|
|
455
|
+
if (row.synthesized_at !== null) process.stdout.write(`Synthesized: ${new Date(row.synthesized_at).toLocaleString()}\n`);
|
|
456
|
+
if (row.expires_at !== null) process.stdout.write(`Expires: ${new Date(row.expires_at).toLocaleString()}\n`);
|
|
457
|
+
if (row.in_flight_since !== null) process.stdout.write(`In-flight since: ${new Date(row.in_flight_since).toLocaleString()}\n`);
|
|
458
|
+
process.stdout.write(`\n${row.content}\n\n`);
|
|
459
|
+
}
|
|
460
|
+
} finally {
|
|
461
|
+
db.close();
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
function synthesizeStatusCommand(formatter) {
|
|
465
|
+
const db = DatabaseManager.open();
|
|
466
|
+
try {
|
|
467
|
+
if (!hasSynthesesTable(db)) {
|
|
468
|
+
if (formatter.isJson) process.stdout.write(`${JSON.stringify([])}\n`);
|
|
469
|
+
else process.stdout.write("No synthesis data available.\n");
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const rows = db.db.prepare("SELECT * FROM syntheses ORDER BY scope").all();
|
|
473
|
+
if (formatter.isJson) {
|
|
474
|
+
process.stdout.write(`${JSON.stringify(rows)}\n`);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (rows.length === 0) {
|
|
478
|
+
process.stdout.write("No syntheses found.\n");
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
process.stdout.write("\n");
|
|
482
|
+
for (const row of rows) {
|
|
483
|
+
const inFlight = row.in_flight_since !== null ? " [in-flight]" : "";
|
|
484
|
+
const synthesized = row.synthesized_at !== null ? new Date(row.synthesized_at).toLocaleString() : "(never)";
|
|
485
|
+
const expires = row.expires_at !== null ? new Date(row.expires_at).toLocaleString() : "(none)";
|
|
486
|
+
process.stdout.write(` ${row.scope}${inFlight}\n`);
|
|
487
|
+
process.stdout.write(` synthesized_at: ${synthesized}\n`);
|
|
488
|
+
process.stdout.write(` expires_at: ${expires}\n`);
|
|
489
|
+
}
|
|
490
|
+
process.stdout.write("\n");
|
|
491
|
+
} finally {
|
|
492
|
+
db.close();
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
//#endregion
|
|
327
496
|
//#region src/commands/unpin.ts
|
|
328
497
|
function unpinCommand(id, db) {
|
|
329
498
|
const ownDb = db === void 0;
|
|
@@ -347,6 +516,10 @@ const TYPE_COLORS = {
|
|
|
347
516
|
function colorType(type) {
|
|
348
517
|
return TYPE_COLORS[type](type);
|
|
349
518
|
}
|
|
519
|
+
function statsRow(label, value, warn) {
|
|
520
|
+
if (warn) process.stdout.write(` ${chalk.yellow("⚠")} ${label.padEnd(12)} ${value}\n`);
|
|
521
|
+
else process.stdout.write(` ${" ".concat(label).padEnd(14)} ${value}\n`);
|
|
522
|
+
}
|
|
350
523
|
function truncate(str, max) {
|
|
351
524
|
return str.length > max ? `${str.slice(0, max - 1)}…` : str;
|
|
352
525
|
}
|
|
@@ -424,8 +597,9 @@ var Formatter = class Formatter {
|
|
|
424
597
|
for (const type of types) process.stdout.write(` ${TYPE_COLORS[type](type.padEnd(14))} ${stats.byType[type]}\n`);
|
|
425
598
|
process.stdout.write(`\n ${chalk.dim("─".repeat(24))}\n`);
|
|
426
599
|
process.stdout.write(` ${"total".padEnd(14)} ${stats.total}\n`);
|
|
427
|
-
|
|
428
|
-
|
|
600
|
+
statsRow("needs_review", String(stats.needsReview), stats.needsReview > 0);
|
|
601
|
+
statsRow("pin_budget", `${stats.pinBudgetChars} / ${PIN_BUDGET_THRESHOLD} chars`, stats.pinBudgetChars > PIN_BUDGET_THRESHOLD);
|
|
602
|
+
process.stdout.write("\n");
|
|
429
603
|
}
|
|
430
604
|
outputQueryResults(results) {
|
|
431
605
|
if (this.#isJson) {
|
|
@@ -463,6 +637,30 @@ var Formatter = class Formatter {
|
|
|
463
637
|
}
|
|
464
638
|
process.stdout.write(`\n${table.toString()}\n\n`);
|
|
465
639
|
}
|
|
640
|
+
outputReview(memories) {
|
|
641
|
+
if (this.#isJson) {
|
|
642
|
+
process.stdout.write(`${JSON.stringify(memories)}\n`);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if (memories.length === 0) {
|
|
646
|
+
process.stdout.write(`${chalk.dim("No memories flagged for review.")}\n`);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
for (const m of memories) {
|
|
650
|
+
process.stdout.write("\n");
|
|
651
|
+
process.stdout.write(` ${colorType(m.type)} ${chalk.dim(m.id)}\n`);
|
|
652
|
+
process.stdout.write(` ${truncate(m.content, 80)}\n`);
|
|
653
|
+
for (const event of m.reviewEvents) this.#outputReviewEvent(event);
|
|
654
|
+
}
|
|
655
|
+
process.stdout.write("\n");
|
|
656
|
+
}
|
|
657
|
+
#outputReviewEvent(event) {
|
|
658
|
+
const pct = `${Math.round(event.similarity * 100)}%`;
|
|
659
|
+
const conflictRef = event.conflictingMemoryId ? chalk.dim(event.conflictingMemoryId) : chalk.dim("(deleted)");
|
|
660
|
+
const ts = new Date(event.createdAt).toLocaleString();
|
|
661
|
+
process.stdout.write(` ${chalk.yellow("⚠")} ${pct} similarity conflict: ${conflictRef} ${chalk.dim(ts)}\n`);
|
|
662
|
+
if (event.conflictContentSnapshot) process.stdout.write(` ${chalk.dim("snapshot:")} ${truncate(event.conflictContentSnapshot, 60)}\n`);
|
|
663
|
+
}
|
|
466
664
|
error(msg) {
|
|
467
665
|
if (this.#isJson) process.stderr.write(`${JSON.stringify({ error: msg })}\n`);
|
|
468
666
|
else process.stderr.write(`${chalk.red("Error:")} ${msg}\n`);
|
|
@@ -471,6 +669,7 @@ var Formatter = class Formatter {
|
|
|
471
669
|
//#endregion
|
|
472
670
|
//#region src/prompt-helper.ts
|
|
473
671
|
var PromptHelper = class {
|
|
672
|
+
autoConfirm;
|
|
474
673
|
constructor(autoConfirm) {
|
|
475
674
|
this.autoConfirm = autoConfirm;
|
|
476
675
|
}
|
|
@@ -770,18 +969,27 @@ const writers = {
|
|
|
770
969
|
const hooks = MaybeJsonObjectSchema.parse(cfg.hooks) ?? {};
|
|
771
970
|
const sessionStartInner = (Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []).flatMap(getHooksArray);
|
|
772
971
|
const userPromptSubmitInner = (Array.isArray(hooks.UserPromptSubmit) ? hooks.UserPromptSubmit : []).flatMap(getHooksArray);
|
|
972
|
+
const stopInner = (Array.isArray(hooks.Stop) ? hooks.Stop : []).flatMap(getHooksArray);
|
|
773
973
|
return {
|
|
774
974
|
status: "ready",
|
|
775
975
|
configPath: cfgPath,
|
|
776
|
-
hooks: [
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
976
|
+
hooks: [
|
|
977
|
+
{
|
|
978
|
+
event: "SessionStart",
|
|
979
|
+
command: "npx -y @membank/cli inject --harness claude-code",
|
|
980
|
+
existingCommand: extractInjectCommand(sessionStartInner) || null
|
|
981
|
+
},
|
|
982
|
+
{
|
|
983
|
+
event: "UserPromptSubmit",
|
|
984
|
+
command: "npx -y @membank/cli inject --harness claude-code --event user-prompt-submit",
|
|
985
|
+
existingCommand: extractInjectCommand(userPromptSubmitInner) || null
|
|
986
|
+
},
|
|
987
|
+
{
|
|
988
|
+
event: "Stop",
|
|
989
|
+
command: "npx -y @membank/cli inject --harness claude-code --event session-stop",
|
|
990
|
+
existingCommand: extractInjectCommand(stopInner) || null
|
|
991
|
+
}
|
|
992
|
+
]
|
|
785
993
|
};
|
|
786
994
|
},
|
|
787
995
|
write(resolver, events) {
|
|
@@ -804,6 +1012,13 @@ const writers = {
|
|
|
804
1012
|
command: "npx -y @membank/cli inject --harness claude-code --event user-prompt-submit"
|
|
805
1013
|
}]
|
|
806
1014
|
}];
|
|
1015
|
+
if (events.includes("Stop")) newHooks.Stop = [...filterOutMembank(Array.isArray(hooks.Stop) ? hooks.Stop : []), {
|
|
1016
|
+
matcher: "",
|
|
1017
|
+
hooks: [{
|
|
1018
|
+
type: "command",
|
|
1019
|
+
command: "npx -y @membank/cli inject --harness claude-code --event session-stop"
|
|
1020
|
+
}]
|
|
1021
|
+
}];
|
|
807
1022
|
writeJsonAtomic(cfgPath, {
|
|
808
1023
|
...cfg,
|
|
809
1024
|
hooks: newHooks
|
|
@@ -818,18 +1033,27 @@ const writers = {
|
|
|
818
1033
|
const hooks = MaybeJsonObjectSchema.parse(cfg.hooks) ?? {};
|
|
819
1034
|
const sessionStart = Array.isArray(hooks.sessionStart) ? hooks.sessionStart : [];
|
|
820
1035
|
const userPromptSubmitted = Array.isArray(hooks.userPromptSubmitted) ? hooks.userPromptSubmitted : [];
|
|
1036
|
+
const sessionEnd = Array.isArray(hooks.sessionEnd) ? hooks.sessionEnd : [];
|
|
821
1037
|
return {
|
|
822
1038
|
status: "ready",
|
|
823
1039
|
configPath: cfgPath,
|
|
824
|
-
hooks: [
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
1040
|
+
hooks: [
|
|
1041
|
+
{
|
|
1042
|
+
event: "sessionStart",
|
|
1043
|
+
command: "npx -y @membank/cli inject --harness copilot-cli",
|
|
1044
|
+
existingCommand: extractInjectCommand(sessionStart) || null
|
|
1045
|
+
},
|
|
1046
|
+
{
|
|
1047
|
+
event: "userPromptSubmitted",
|
|
1048
|
+
command: "npx -y @membank/cli inject --harness copilot-cli --event user-prompt-submit",
|
|
1049
|
+
existingCommand: extractInjectCommand(userPromptSubmitted) || null
|
|
1050
|
+
},
|
|
1051
|
+
{
|
|
1052
|
+
event: "sessionEnd",
|
|
1053
|
+
command: "npx -y @membank/cli inject --harness copilot-cli --event session-stop",
|
|
1054
|
+
existingCommand: extractInjectCommand(sessionEnd) || null
|
|
1055
|
+
}
|
|
1056
|
+
]
|
|
833
1057
|
};
|
|
834
1058
|
},
|
|
835
1059
|
write(resolver, events) {
|
|
@@ -848,6 +1072,11 @@ const writers = {
|
|
|
848
1072
|
bash: "npx -y @membank/cli inject --harness copilot-cli --event user-prompt-submit",
|
|
849
1073
|
timeoutSec: 30
|
|
850
1074
|
}];
|
|
1075
|
+
if (events.includes("sessionEnd")) newHooks.sessionEnd = [...filterOutMembankFlat(Array.isArray(hooks.sessionEnd) ? hooks.sessionEnd : []), {
|
|
1076
|
+
type: "command",
|
|
1077
|
+
bash: "npx -y @membank/cli inject --harness copilot-cli --event session-stop",
|
|
1078
|
+
timeoutSec: 30
|
|
1079
|
+
}];
|
|
851
1080
|
writeJsonAtomic(cfgPath, {
|
|
852
1081
|
version: OptionalNumberSchema.parse(cfg.version) ?? 1,
|
|
853
1082
|
...cfg,
|
|
@@ -863,18 +1092,27 @@ const writers = {
|
|
|
863
1092
|
const hooks = MaybeJsonObjectSchema.parse(cfg.hooks) ?? {};
|
|
864
1093
|
const sessionStartInner = (Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []).flatMap(getHooksArray);
|
|
865
1094
|
const userPromptSubmitInner = (Array.isArray(hooks.UserPromptSubmit) ? hooks.UserPromptSubmit : []).flatMap(getHooksArray);
|
|
1095
|
+
const stopInner = (Array.isArray(hooks.Stop) ? hooks.Stop : []).flatMap(getHooksArray);
|
|
866
1096
|
return {
|
|
867
1097
|
status: "ready",
|
|
868
1098
|
configPath: cfgPath,
|
|
869
|
-
hooks: [
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
1099
|
+
hooks: [
|
|
1100
|
+
{
|
|
1101
|
+
event: "SessionStart",
|
|
1102
|
+
command: "npx -y @membank/cli inject --harness codex",
|
|
1103
|
+
existingCommand: extractInjectCommand(sessionStartInner) || null
|
|
1104
|
+
},
|
|
1105
|
+
{
|
|
1106
|
+
event: "UserPromptSubmit",
|
|
1107
|
+
command: "npx -y @membank/cli inject --harness codex --event user-prompt-submit",
|
|
1108
|
+
existingCommand: extractInjectCommand(userPromptSubmitInner) || null
|
|
1109
|
+
},
|
|
1110
|
+
{
|
|
1111
|
+
event: "Stop",
|
|
1112
|
+
command: "npx -y @membank/cli inject --harness codex --event session-stop",
|
|
1113
|
+
existingCommand: extractInjectCommand(stopInner) || null
|
|
1114
|
+
}
|
|
1115
|
+
]
|
|
878
1116
|
};
|
|
879
1117
|
},
|
|
880
1118
|
write(resolver, events) {
|
|
@@ -899,6 +1137,14 @@ const writers = {
|
|
|
899
1137
|
timeout: 30
|
|
900
1138
|
}]
|
|
901
1139
|
}];
|
|
1140
|
+
if (events.includes("Stop")) newHooks.Stop = [...filterOutMembank(Array.isArray(hooks.Stop) ? hooks.Stop : []), {
|
|
1141
|
+
matcher: "",
|
|
1142
|
+
hooks: [{
|
|
1143
|
+
type: "command",
|
|
1144
|
+
command: "npx -y @membank/cli inject --harness codex --event session-stop",
|
|
1145
|
+
timeout: 30
|
|
1146
|
+
}]
|
|
1147
|
+
}];
|
|
902
1148
|
writeJsonAtomic(cfgPath, {
|
|
903
1149
|
...cfg,
|
|
904
1150
|
hooks: newHooks
|
|
@@ -1090,6 +1336,7 @@ var SetupOrchestrator = class {
|
|
|
1090
1336
|
#modelDownloader;
|
|
1091
1337
|
#out;
|
|
1092
1338
|
#progressWrite;
|
|
1339
|
+
#synthesisOptIn;
|
|
1093
1340
|
constructor(deps) {
|
|
1094
1341
|
this.#detector = deps.detector ?? (() => detectHarnesses());
|
|
1095
1342
|
this.#writer = deps.writer;
|
|
@@ -1098,6 +1345,7 @@ var SetupOrchestrator = class {
|
|
|
1098
1345
|
this.#harnessSelector = deps.harnessSelector;
|
|
1099
1346
|
this.#modelDownloader = deps.modelDownloader;
|
|
1100
1347
|
this.#out = deps.out ?? ((msg) => process.stdout.write(`${msg}\n`));
|
|
1348
|
+
this.#synthesisOptIn = deps.synthesisOptIn ?? false;
|
|
1101
1349
|
this.#progressWrite = deps.progressWrite ?? ((text) => process.stdout.write(text));
|
|
1102
1350
|
}
|
|
1103
1351
|
async run(opts = {}) {
|
|
@@ -1234,6 +1482,12 @@ var SetupOrchestrator = class {
|
|
|
1234
1482
|
out(`Step ${this.#hookWriter !== void 0 ? 3 : 2}/${totalSteps} Embedding model`);
|
|
1235
1483
|
modelDownloaded = !(await this.#runModelDownload(this.#modelDownloader, out)).skipped;
|
|
1236
1484
|
} else out("Model download step: see DRA-52");
|
|
1485
|
+
if (this.#synthesisOptIn && !yes && !json) {
|
|
1486
|
+
if (await this.#prompter("Enable memory synthesis? (experimental — summarizes memories at session start using Claude Haiku, requires ANTHROPIC_API_KEY)")) {
|
|
1487
|
+
ConfigManager.set("synthesis.enabled", true);
|
|
1488
|
+
out(" ✓ Memory synthesis enabled.");
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1237
1491
|
const written = results.filter((r) => r.status === "written").length;
|
|
1238
1492
|
const skipped = results.filter((r) => r.status === "already-configured").length;
|
|
1239
1493
|
const errors = results.filter((r) => r.status === "error").length;
|
|
@@ -1470,7 +1724,8 @@ program.command("setup").description("detect installed harnesses and write MCP c
|
|
|
1470
1724
|
prompter: (question) => promptHelper.confirm(question),
|
|
1471
1725
|
harnessSelector,
|
|
1472
1726
|
modelDownloader: new ModelDownloader(),
|
|
1473
|
-
out: formatter.isJson ? void 0 : decoratedOut
|
|
1727
|
+
out: formatter.isJson ? void 0 : decoratedOut,
|
|
1728
|
+
synthesisOptIn: true
|
|
1474
1729
|
});
|
|
1475
1730
|
try {
|
|
1476
1731
|
const results = await orchestrator.run({
|
|
@@ -1489,6 +1744,16 @@ program.command("setup").description("detect installed harnesses and write MCP c
|
|
|
1489
1744
|
process.exit(2);
|
|
1490
1745
|
}
|
|
1491
1746
|
});
|
|
1747
|
+
program.command("review").description("list memories flagged for review, or resolve review events").option("--resolve <id>", "resolve all open review events for the given memory id").action(async (cmdOptions) => {
|
|
1748
|
+
const globalOpts = program.opts();
|
|
1749
|
+
const formatter = Formatter.create(globalOpts.json === true);
|
|
1750
|
+
try {
|
|
1751
|
+
await reviewCommand(cmdOptions, formatter);
|
|
1752
|
+
} catch (err) {
|
|
1753
|
+
formatter.error(err instanceof Error ? err.message : String(err));
|
|
1754
|
+
process.exit(2);
|
|
1755
|
+
}
|
|
1756
|
+
});
|
|
1492
1757
|
program.command("migrate <mode> [name]").description("list or run a named data migration (modes: list, run)").action(async (mode, name) => {
|
|
1493
1758
|
const modeResult = MigrateModeSchema.safeParse(mode);
|
|
1494
1759
|
if (!modeResult.success) {
|
|
@@ -1512,6 +1777,40 @@ program.command("dashboard").description("open the memory management dashboard i
|
|
|
1512
1777
|
process.exit(2);
|
|
1513
1778
|
}
|
|
1514
1779
|
});
|
|
1780
|
+
const configCmd = program.command("config").description("manage membank configuration");
|
|
1781
|
+
configCmd.command("get <key>").description("print a config value as JSON").action((key) => {
|
|
1782
|
+
const globalOpts = program.opts();
|
|
1783
|
+
configGetCommand(key, Formatter.create(globalOpts.json === true));
|
|
1784
|
+
});
|
|
1785
|
+
configCmd.command("set <key> <value>").description("set a config value and persist").action((key, value) => {
|
|
1786
|
+
const globalOpts = program.opts();
|
|
1787
|
+
configSetCommand(key, value, Formatter.create(globalOpts.json === true));
|
|
1788
|
+
});
|
|
1789
|
+
configCmd.command("show").description("print the entire config as formatted JSON").action(() => {
|
|
1790
|
+
const globalOpts = program.opts();
|
|
1791
|
+
configShowCommand(Formatter.create(globalOpts.json === true));
|
|
1792
|
+
});
|
|
1793
|
+
const synthesizeCmd = program.command("synthesize").description("view synthesis state");
|
|
1794
|
+
synthesizeCmd.command("show").description("display current synthesis for a scope").option("--scope <scope>", "scope to show (default: global)").action((cmdOptions) => {
|
|
1795
|
+
const globalOpts = program.opts();
|
|
1796
|
+
const formatter = Formatter.create(globalOpts.json === true);
|
|
1797
|
+
try {
|
|
1798
|
+
synthesizeShowCommand(cmdOptions, formatter);
|
|
1799
|
+
} catch (err) {
|
|
1800
|
+
formatter.error(err instanceof Error ? err.message : String(err));
|
|
1801
|
+
process.exit(2);
|
|
1802
|
+
}
|
|
1803
|
+
});
|
|
1804
|
+
synthesizeCmd.command("status").description("show all scopes with synthesis status").action(() => {
|
|
1805
|
+
const globalOpts = program.opts();
|
|
1806
|
+
const formatter = Formatter.create(globalOpts.json === true);
|
|
1807
|
+
try {
|
|
1808
|
+
synthesizeStatusCommand(formatter);
|
|
1809
|
+
} catch (err) {
|
|
1810
|
+
formatter.error(err instanceof Error ? err.message : String(err));
|
|
1811
|
+
process.exit(2);
|
|
1812
|
+
}
|
|
1813
|
+
});
|
|
1515
1814
|
program.on("command:*", () => {
|
|
1516
1815
|
program.outputHelp();
|
|
1517
1816
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@membank/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
"commander": "^14.0.3",
|
|
22
22
|
"ora": "^9.4.0",
|
|
23
23
|
"zod": "^4.4.3",
|
|
24
|
-
"@membank/core": "0.
|
|
25
|
-
"@membank/
|
|
26
|
-
"@membank/
|
|
24
|
+
"@membank/core": "0.9.0",
|
|
25
|
+
"@membank/dashboard": "0.5.1",
|
|
26
|
+
"@membank/mcp": "0.11.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^25.6.0",
|