@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.
Files changed (2) hide show
  1. package/dist/index.mjs +341 -42
  2. 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, needs_review, created_at, updated_at)
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.needsReview ? 1 : 0, rec.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(), rec.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString());
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
- const allPinned = [...ctx.pinnedGlobal, ...ctx.pinnedProject];
176
- if (allPinned.length > 0) {
177
- const memLines = allPinned.map((m) => ` <memory type="${m.type}">${xmlEscape(m.content)}</memory>`);
178
- parts.push(`<pinned-memories>\n${memLines.join("\n")}\n</pinned-memories>`);
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
- return formatContext(new SessionContextBuilder(db).getSessionContext(resolved.hash));
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
- if (stats.needsReview > 0) process.stdout.write(` ${chalk.yellow("⚠")} ${"needs_review".padEnd(12)} ${stats.needsReview}\n\n`);
428
- else process.stdout.write(` ${" needs_review".padEnd(14)} ${stats.needsReview}\n\n`);
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
- event: "SessionStart",
778
- command: "npx -y @membank/cli inject --harness claude-code",
779
- existingCommand: extractInjectCommand(sessionStartInner) || null
780
- }, {
781
- event: "UserPromptSubmit",
782
- command: "npx -y @membank/cli inject --harness claude-code --event user-prompt-submit",
783
- existingCommand: extractInjectCommand(userPromptSubmitInner) || null
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
- event: "sessionStart",
826
- command: "npx -y @membank/cli inject --harness copilot-cli",
827
- existingCommand: extractInjectCommand(sessionStart) || null
828
- }, {
829
- event: "userPromptSubmitted",
830
- command: "npx -y @membank/cli inject --harness copilot-cli --event user-prompt-submit",
831
- existingCommand: extractInjectCommand(userPromptSubmitted) || null
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
- event: "SessionStart",
871
- command: "npx -y @membank/cli inject --harness codex",
872
- existingCommand: extractInjectCommand(sessionStartInner) || null
873
- }, {
874
- event: "UserPromptSubmit",
875
- command: "npx -y @membank/cli inject --harness codex --event user-prompt-submit",
876
- existingCommand: extractInjectCommand(userPromptSubmitInner) || null
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.8.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.7.0",
25
- "@membank/mcp": "0.9.0",
26
- "@membank/dashboard": "0.4.1"
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",