@membank/cli 0.7.1 → 0.9.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 +229 -106
  2. package/package.json +5 -4
package/dist/index.mjs CHANGED
@@ -1,20 +1,60 @@
1
1
  #!/usr/bin/env node
2
2
  import { cancel, confirm, intro, isCancel, multiselect, note, outro } from "@clack/prompts";
3
- import { DatabaseManager, EmbeddingService, MEMORY_TYPE_VALUES, MIGRATIONS, MemoryRepository, ProjectRepository, QueryEngine, SessionContextBuilder, resolveProject, runScopeToProjectsMigration } from "@membank/core";
3
+ import { DatabaseManager, EmbeddingService, MIGRATIONS, MemoryRepository, MemoryTypeSchema, MemoryTypeSchema as MemoryTypeSchema$1, ProjectRepository, QueryEngine, SessionContextBuilder, 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
+ import { z } from "zod";
8
9
  import { startDashboard } from "@membank/dashboard";
9
10
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
10
11
  import { dirname, join } from "node:path";
11
12
  import Table from "cli-table3";
12
- import { homedir, tmpdir } from "node:os";
13
13
  import { execFile } from "node:child_process";
14
14
  import { promisify } from "node:util";
15
+ import { homedir, tmpdir } from "node:os";
15
16
  import { EventEmitter } from "node:events";
16
17
  import { pipeline } from "@huggingface/transformers";
17
18
  import { createInterface } from "node:readline";
19
+ //#region src/schemas.ts
20
+ const SETUP_HARNESS_VALUES = [
21
+ "claude-code",
22
+ "copilot",
23
+ "codex",
24
+ "opencode"
25
+ ];
26
+ const SetupHarnessSchema = z.enum(SETUP_HARNESS_VALUES);
27
+ const InjectionHarnessSchema = z.enum([
28
+ "claude-code",
29
+ "copilot-cli",
30
+ "codex",
31
+ "opencode"
32
+ ]);
33
+ const MigrateModeSchema = z.enum(["list", "run"]);
34
+ const LimitSchema = z.coerce.number().int().positive();
35
+ const PortSchema = z.coerce.number().int().min(1).max(65535);
36
+ const OptionalNumberSchema = z.number().optional().catch(void 0);
37
+ const MutableJsonObjectSchema = z.record(z.string(), z.unknown());
38
+ const MaybeJsonObjectSchema = z.record(z.string(), z.unknown()).optional().catch(void 0);
39
+ const ExportRecordSchema = z.object({
40
+ id: z.string().min(1),
41
+ content: z.string(),
42
+ type: MemoryTypeSchema,
43
+ tags: z.array(z.string()).optional().default([]),
44
+ sourceHarness: z.string().nullable().optional().default(null),
45
+ accessCount: z.number().optional().default(0),
46
+ pinned: z.boolean().optional().default(false),
47
+ needsReview: z.boolean().optional().default(false),
48
+ createdAt: z.string().optional(),
49
+ updatedAt: z.string().optional(),
50
+ embedding: z.string().nullable().optional().default(null)
51
+ });
52
+ const ExportFileSchema = z.object({
53
+ version: z.literal(1),
54
+ exportedAt: z.string().optional(),
55
+ memories: z.array(ExportRecordSchema)
56
+ });
57
+ //#endregion
18
58
  //#region src/commands/add.ts
19
59
  async function addCommand(content, options, formatter, db, embeddingService) {
20
60
  const ownDb = db === void 0;
@@ -26,7 +66,7 @@ async function addCommand(content, options, formatter, db, embeddingService) {
26
66
  const spinner = formatter.isJson ? null : ora("Saving memory…").start();
27
67
  const memory = await repo.save({
28
68
  content,
29
- type: options.type,
69
+ type: MemoryTypeSchema$1.parse(options.type),
30
70
  tags,
31
71
  projectScope
32
72
  });
@@ -39,7 +79,7 @@ async function addCommand(content, options, formatter, db, embeddingService) {
39
79
  //#endregion
40
80
  //#region src/commands/dashboard.ts
41
81
  async function dashboardCommand(opts) {
42
- await startDashboard({ port: opts.port !== void 0 ? parseInt(opts.port, 10) : void 0 });
82
+ await startDashboard({ port: opts.port !== void 0 ? PortSchema.parse(opts.port) : void 0 });
43
83
  }
44
84
  //#endregion
45
85
  //#region src/commands/delete.ts
@@ -59,11 +99,10 @@ function exportCommand(db, formatter, opts) {
59
99
  id: row.id,
60
100
  content: row.content,
61
101
  type: row.type,
62
- tags: JSON.parse(row.tags),
102
+ tags: TagsRowSchema.parse(JSON.parse(row.tags)),
63
103
  sourceHarness: row.source,
64
104
  accessCount: row.access_count,
65
105
  pinned: row.pinned !== 0,
66
- needsReview: row.needs_review !== 0,
67
106
  createdAt: row.created_at,
68
107
  updatedAt: row.updated_at,
69
108
  embedding: row.embedding !== null ? Buffer.from(row.embedding).toString("base64") : null
@@ -83,17 +122,6 @@ function exportCommand(db, formatter, opts) {
83
122
  }
84
123
  //#endregion
85
124
  //#region src/commands/import.ts
86
- const MEMORY_TYPES = new Set(MEMORY_TYPE_VALUES);
87
- function isValidRecord(r) {
88
- if (typeof r !== "object" || r === null) return false;
89
- const rec = r;
90
- return typeof rec.id === "string" && rec.id.length > 0 && typeof rec.content === "string" && typeof rec.type === "string" && MEMORY_TYPES.has(rec.type);
91
- }
92
- function isExportFile(parsed) {
93
- if (typeof parsed !== "object" || parsed === null) return false;
94
- const obj = parsed;
95
- return obj.version === 1 && Array.isArray(obj.memories);
96
- }
97
125
  async function importCommand(filePath, db, formatter, prompt) {
98
126
  let raw;
99
127
  try {
@@ -109,25 +137,21 @@ async function importCommand(filePath, db, formatter, prompt) {
109
137
  formatter.error(`Invalid JSON in file: ${filePath}`);
110
138
  process.exit(1);
111
139
  }
112
- if (!isExportFile(parsed)) {
113
- formatter.error(`Invalid export file format: expected version 1 and memories array`);
114
- process.exit(1);
115
- }
116
- const invalidIndex = parsed.memories.findIndex((r) => !isValidRecord(r));
117
- if (invalidIndex !== -1) {
118
- formatter.error(`Invalid memory record at index ${invalidIndex}: must have id, content, and type`);
140
+ const parseResult = ExportFileSchema.safeParse(parsed);
141
+ if (!parseResult.success) {
142
+ formatter.error(`Invalid export file: ${parseResult.error.issues[0]?.message ?? "unknown error"}`);
119
143
  process.exit(1);
120
144
  }
121
- const count = parsed.memories.length;
145
+ const count = parseResult.data.memories.length;
122
146
  if (formatter.isJson) process.stdout.write(`${JSON.stringify({ found: count })}\n`);
123
147
  else process.stdout.write(`Found ${count} memories to import.\n`);
124
148
  if (!await prompt.confirm("Import?")) return;
125
- const insertMemory = db.db.prepare(`INSERT OR REPLACE INTO memories (id, content, type, tags, source, access_count, pinned, needs_review, created_at, updated_at)
126
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
149
+ const insertMemory = db.db.prepare(`INSERT OR REPLACE INTO memories (id, content, type, tags, source, access_count, pinned, created_at, updated_at)
150
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
127
151
  const insertEmbedding = db.db.prepare(`INSERT OR REPLACE INTO embeddings (rowid, embedding) SELECT m.rowid, ? FROM memories m WHERE m.id = ?`);
128
152
  db.db.transaction(() => {
129
- for (const rec of parsed.memories) {
130
- 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());
153
+ for (const rec of parseResult.data.memories) {
154
+ 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());
131
155
  if (rec.embedding !== null && rec.embedding !== void 0) {
132
156
  const buf = Buffer.from(rec.embedding, "base64");
133
157
  insertEmbedding.run(buf, rec.id);
@@ -139,17 +163,21 @@ async function importCommand(filePath, db, formatter, prompt) {
139
163
  }
140
164
  //#endregion
141
165
  //#region src/commands/inject.ts
142
- const MEMORY_GUIDANCE = "[Memory Guidance]: Call save_memory when ANY of these happen: (1) user states a preference or makes a decision; (2) user corrects you; (3) you discover a working fix after a tool error; (4) you learn a non-obvious project fact. Type ∈ correction|preference|decision|learning|fact. Call query_memory before answering anything that might touch prior decisions. When unsure, save.";
166
+ const MEMORY_GUIDANCE = ["Save (call save_memory) when: (1) user states a preference or makes a decision; (2) user corrects you; (3) you discover a working fix after a tool error; (4) you learn a non-obvious project fact. Type ∈ correction|preference|decision|learning|fact. When unsure, save.", "Query (call query_memory) before: answering anything that touches prior decisions, and before exploration tasks (file reads, searches, web lookups) where past corrections or preferences may apply. Skip when clearly irrelevant (e.g. trivial arithmetic). Soft guideline, not a hard rule."].join("\n");
167
+ function xmlEscape(s) {
168
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
169
+ }
143
170
  function formatContext(ctx) {
144
- const lines = [];
171
+ const parts = [];
145
172
  const statParts = Object.entries(ctx.stats).filter(([, count]) => count > 0).map(([type, count]) => `${count} ${type}${count !== 1 ? "s" : ""}`);
146
- if (statParts.length > 0) lines.push(`[Memory Stats]: ${statParts.join(", ")}`);
147
- else lines.push("[Memory Stats]: no memories saved yet");
148
- const formatMemory = (m) => `"${m.content}" (${m.type})`;
149
- for (const m of ctx.pinnedGlobal) lines.push(`[Pinned Global]: ${formatMemory(m)}`);
150
- for (const m of ctx.pinnedProject) lines.push(`[Pinned Project]: ${formatMemory(m)}`);
151
- lines.push(MEMORY_GUIDANCE);
152
- return lines.join("\n");
173
+ if (statParts.length > 0) parts.push(`<memory-stats>\n${statParts.join(", ")}\n</memory-stats>`);
174
+ const allPinned = [...ctx.pinnedGlobal, ...ctx.pinnedProject];
175
+ if (allPinned.length > 0) {
176
+ const memLines = allPinned.map((m) => ` <memory type="${m.type}">${xmlEscape(m.content)}</memory>`);
177
+ parts.push(`<pinned-memories>\n${memLines.join("\n")}\n</pinned-memories>`);
178
+ }
179
+ parts.push(`<memory-guidance>\n${MEMORY_GUIDANCE}\n</memory-guidance>`);
180
+ return parts.join("\n");
153
181
  }
154
182
  function outputAdditionalContext(text, harness, eventName) {
155
183
  if (harness === "claude-code") {
@@ -165,22 +193,36 @@ function outputAdditionalContext(text, harness, eventName) {
165
193
  }
166
194
  process.stdout.write(`${text}\n`);
167
195
  }
168
- async function handleSessionStart(opts) {
169
- const projectScope = (await resolveProject()).hash;
196
+ async function buildText() {
197
+ const resolved = await resolveProject();
170
198
  const db = DatabaseManager.open();
171
- let text;
172
199
  try {
173
- text = formatContext(new SessionContextBuilder(db).getSessionContext(projectScope));
200
+ return formatContext(new SessionContextBuilder(db).getSessionContext(resolved.hash));
174
201
  } finally {
175
202
  db.close();
176
203
  }
177
- if (!text) process.exit(0);
178
- const harness = opts.harness;
179
- outputAdditionalContext(text, harness, "SessionStart");
204
+ }
205
+ async function handleEvent(harness, eventName) {
206
+ const text = await buildText().catch((err) => {
207
+ const msg = err instanceof Error ? err.message : String(err);
208
+ process.stderr.write(`membank inject: ${msg}\n`);
209
+ return null;
210
+ });
211
+ if (text === null) process.exit(0);
212
+ outputAdditionalContext(text, harness, eventName);
180
213
  }
181
214
  async function injectCommand(opts) {
182
- if (opts.event !== void 0 && opts.event !== "session-start") process.exit(0);
183
- await handleSessionStart(opts);
215
+ const harnessResult = InjectionHarnessSchema.safeParse(opts.harness);
216
+ const harness = harnessResult.success ? harnessResult.data : void 0;
217
+ if (opts.event === "session-start" || opts.event === void 0) {
218
+ await handleEvent(harness, "SessionStart");
219
+ return;
220
+ }
221
+ if (opts.event === "user-prompt-submit") {
222
+ await handleEvent(harness, "UserPromptSubmit");
223
+ return;
224
+ }
225
+ process.exit(0);
184
226
  }
185
227
  //#endregion
186
228
  //#region src/commands/list.ts
@@ -188,7 +230,7 @@ async function listCommand(options, formatter) {
188
230
  const db = DatabaseManager.open();
189
231
  try {
190
232
  const memories = new MemoryRepository(db, new EmbeddingService(), new ProjectRepository(db)).list({
191
- type: options.type,
233
+ type: options.type !== void 0 ? MemoryTypeSchema$1.parse(options.type) : void 0,
192
234
  pinned: options.pinned
193
235
  });
194
236
  formatter.outputMemories(memories);
@@ -255,12 +297,13 @@ async function queryCommand(queryText, options, formatter) {
255
297
  try {
256
298
  const embedding = new EmbeddingService();
257
299
  const engine = new QueryEngine(db, embedding, new MemoryRepository(db, embedding, new ProjectRepository(db)));
258
- const limit = options.limit !== void 0 ? Number.parseInt(options.limit, 10) : 10;
300
+ const limit = options.limit !== void 0 ? LimitSchema.parse(options.limit) : 10;
259
301
  const spinner = formatter.isJson ? null : ora("Searching memories…").start();
260
302
  const results = await engine.query({
261
303
  query: queryText,
262
- type: options.type,
263
- limit
304
+ type: options.type !== void 0 ? MemoryTypeSchema$1.parse(options.type) : void 0,
305
+ limit,
306
+ includePinned: options.includePinned
264
307
  });
265
308
  spinner?.succeed(`${results.length} result${results.length === 1 ? "" : "s"} found`);
266
309
  formatter.outputQueryResults(results);
@@ -269,6 +312,24 @@ async function queryCommand(queryText, options, formatter) {
269
312
  }
270
313
  }
271
314
  //#endregion
315
+ //#region src/commands/review.ts
316
+ async function reviewCommand(opts, formatter) {
317
+ const db = DatabaseManager.open();
318
+ try {
319
+ const repo = new MemoryRepository(db, new EmbeddingService(), new ProjectRepository(db));
320
+ if (opts.resolve !== void 0) {
321
+ repo.resolveReviewEvents(opts.resolve);
322
+ if (!formatter.isJson) process.stdout.write(`Resolved review events for memory ${opts.resolve}\n`);
323
+ else process.stdout.write(`${JSON.stringify({ resolved: opts.resolve })}\n`);
324
+ return;
325
+ }
326
+ const flagged = repo.listFlagged();
327
+ formatter.outputReview(flagged);
328
+ } finally {
329
+ db.close();
330
+ }
331
+ }
332
+ //#endregion
272
333
  //#region src/commands/stats.ts
273
334
  async function statsCommand(formatter) {
274
335
  const db = DatabaseManager.open();
@@ -419,6 +480,30 @@ var Formatter = class Formatter {
419
480
  }
420
481
  process.stdout.write(`\n${table.toString()}\n\n`);
421
482
  }
483
+ outputReview(memories) {
484
+ if (this.#isJson) {
485
+ process.stdout.write(`${JSON.stringify(memories)}\n`);
486
+ return;
487
+ }
488
+ if (memories.length === 0) {
489
+ process.stdout.write(`${chalk.dim("No memories flagged for review.")}\n`);
490
+ return;
491
+ }
492
+ for (const m of memories) {
493
+ process.stdout.write("\n");
494
+ process.stdout.write(` ${colorType(m.type)} ${chalk.dim(m.id)}\n`);
495
+ process.stdout.write(` ${truncate(m.content, 80)}\n`);
496
+ for (const event of m.reviewEvents) this.#outputReviewEvent(event);
497
+ }
498
+ process.stdout.write("\n");
499
+ }
500
+ #outputReviewEvent(event) {
501
+ const pct = `${Math.round(event.similarity * 100)}%`;
502
+ const conflictRef = event.conflictingMemoryId ? chalk.dim(event.conflictingMemoryId) : chalk.dim("(deleted)");
503
+ const ts = new Date(event.createdAt).toLocaleString();
504
+ process.stdout.write(` ${chalk.yellow("⚠")} ${pct} similarity conflict: ${conflictRef} ${chalk.dim(ts)}\n`);
505
+ if (event.conflictContentSnapshot) process.stdout.write(` ${chalk.dim("snapshot:")} ${truncate(event.conflictContentSnapshot, 60)}\n`);
506
+ }
422
507
  error(msg) {
423
508
  if (this.#isJson) process.stderr.write(`${JSON.stringify({ error: msg })}\n`);
424
509
  else process.stderr.write(`${chalk.red("Error:")} ${msg}\n`);
@@ -473,6 +558,21 @@ async function execFileNoThrow(cmd, args) {
473
558
  }
474
559
  }
475
560
  //#endregion
561
+ //#region src/utils/json.ts
562
+ function readJson(path) {
563
+ try {
564
+ return MutableJsonObjectSchema.parse(JSON.parse(readFileSync(path, "utf8")));
565
+ } catch {
566
+ return {};
567
+ }
568
+ }
569
+ function writeJsonAtomic(path, data) {
570
+ mkdirSync(dirname(path), { recursive: true });
571
+ const tmp = join(mkdtempSync(join(tmpdir(), "membank-")), "cfg.json");
572
+ writeFileSync(tmp, JSON.stringify(data, null, 2));
573
+ renameSync(tmp, path);
574
+ }
575
+ //#endregion
476
576
  //#region src/setup/harness-config-writer.ts
477
577
  var CommandError = class extends Error {
478
578
  command;
@@ -490,21 +590,8 @@ const defaultPathResolver$1 = {
490
590
  },
491
591
  cwd: () => process.cwd()
492
592
  };
493
- function readJson$1(path) {
494
- try {
495
- return JSON.parse(readFileSync(path, "utf8"));
496
- } catch {
497
- return {};
498
- }
499
- }
500
- function writeJsonAtomic$1(path, data) {
501
- mkdirSync(dirname(path), { recursive: true });
502
- const tmp = join(mkdtempSync(join(tmpdir(), "membank-")), "cfg.json");
503
- writeFileSync(tmp, JSON.stringify(data, null, 2));
504
- renameSync(tmp, path);
505
- }
506
593
  function hasKey(container, key) {
507
- return container !== null && typeof container === "object" && key in container;
594
+ return container !== null && typeof container === "object" && Object.hasOwn(container, key);
508
595
  }
509
596
  function assertCliFound(result, cli, command) {
510
597
  if (result.exitCode === 127) throw new CommandError(`${cli} CLI not found — install ${cli} first`, command);
@@ -524,7 +611,7 @@ const writers$1 = {
524
611
  };
525
612
  },
526
613
  async write(resolver, run, { overwrite = false } = {}) {
527
- const configured = hasKey(readJson$1(join(resolver.home(), ".claude.json")).mcpServers, "membank");
614
+ const configured = hasKey(readJson(join(resolver.home(), ".claude.json")).mcpServers, "membank");
528
615
  if (configured && !overwrite) return { status: "already-configured" };
529
616
  if (configured) {
530
617
  const removeArgs = [
@@ -564,12 +651,12 @@ const writers$1 = {
564
651
  },
565
652
  async write(resolver, _run, { overwrite = false } = {}) {
566
653
  const cfgPath = join(resolver.home(), ".copilot", "mcp-config.json");
567
- const cfg = readJson$1(cfgPath);
654
+ const cfg = readJson(cfgPath);
568
655
  if (hasKey(cfg.mcpServers, "membank") && !overwrite) return { status: "already-configured" };
569
- writeJsonAtomic$1(cfgPath, {
656
+ writeJsonAtomic(cfgPath, {
570
657
  ...cfg,
571
658
  mcpServers: {
572
- ...cfg.mcpServers,
659
+ ...MaybeJsonObjectSchema.parse(cfg.mcpServers),
573
660
  membank: {
574
661
  command: "npx",
575
662
  args: [
@@ -630,12 +717,12 @@ const writers$1 = {
630
717
  },
631
718
  async write(resolver, _run, { overwrite = false } = {}) {
632
719
  const cfgPath = join(resolver.home(), ".config", "opencode", "opencode.json");
633
- const cfg = readJson$1(cfgPath);
720
+ const cfg = readJson(cfgPath);
634
721
  if (hasKey(cfg.mcp, "membank") && !overwrite) return { status: "already-configured" };
635
- writeJsonAtomic$1(cfgPath, {
722
+ writeJsonAtomic(cfgPath, {
636
723
  ...cfg,
637
724
  mcp: {
638
- ...cfg.mcp,
725
+ ...MaybeJsonObjectSchema.parse(cfg.mcp),
639
726
  membank: {
640
727
  type: "local",
641
728
  command: [
@@ -651,7 +738,7 @@ const writers$1 = {
651
738
  }
652
739
  }
653
740
  };
654
- const SUPPORTED_HARNESSES = Object.keys(writers$1);
741
+ const SUPPORTED_HARNESSES = SETUP_HARNESS_VALUES;
655
742
  var HarnessConfigWriter = class {
656
743
  #resolver;
657
744
  #run;
@@ -677,23 +764,10 @@ const defaultPathResolver = { home: () => {
677
764
  if (!h) throw new Error("Cannot determine home directory");
678
765
  return h;
679
766
  } };
680
- function readJson(path) {
681
- try {
682
- return JSON.parse(readFileSync(path, "utf8"));
683
- } catch {
684
- return {};
685
- }
686
- }
687
- function writeJsonAtomic(path, data) {
688
- mkdirSync(dirname(path), { recursive: true });
689
- const tmp = join(mkdtempSync(join(tmpdir(), "membank-hook-")), "cfg.json");
690
- writeFileSync(tmp, JSON.stringify(data, null, 2));
691
- renameSync(tmp, path);
692
- }
693
767
  function getHooksArray(group) {
694
768
  if (typeof group !== "object" || group === null) return [];
695
- const h = group.hooks;
696
- return Array.isArray(h) ? h : [];
769
+ if (!("hooks" in group)) return [];
770
+ return Array.isArray(group.hooks) ? group.hooks : [];
697
771
  }
698
772
  function findMembankHookCommand(hooks, pattern) {
699
773
  for (const h of hooks) {
@@ -733,23 +807,29 @@ const writers = {
733
807
  "claude-code": {
734
808
  inspect(resolver) {
735
809
  const cfgPath = join(resolver.home(), ".claude", "settings.json");
736
- const hooks = readJson(cfgPath).hooks ?? {};
810
+ const cfg = readJson(cfgPath);
811
+ const hooks = MaybeJsonObjectSchema.parse(cfg.hooks) ?? {};
812
+ const sessionStartInner = (Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []).flatMap(getHooksArray);
813
+ const userPromptSubmitInner = (Array.isArray(hooks.UserPromptSubmit) ? hooks.UserPromptSubmit : []).flatMap(getHooksArray);
737
814
  return {
738
815
  status: "ready",
739
816
  configPath: cfgPath,
740
817
  hooks: [{
741
818
  event: "SessionStart",
742
819
  command: "npx -y @membank/cli inject --harness claude-code",
743
- existingCommand: extractInjectCommand((Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []).flatMap(getHooksArray)) || null
820
+ existingCommand: extractInjectCommand(sessionStartInner) || null
821
+ }, {
822
+ event: "UserPromptSubmit",
823
+ command: "npx -y @membank/cli inject --harness claude-code --event user-prompt-submit",
824
+ existingCommand: extractInjectCommand(userPromptSubmitInner) || null
744
825
  }]
745
826
  };
746
827
  },
747
828
  write(resolver, events) {
748
829
  const cfgPath = join(resolver.home(), ".claude", "settings.json");
749
830
  const cfg = readJson(cfgPath);
750
- const hooks = cfg.hooks ?? {};
831
+ const hooks = MaybeJsonObjectSchema.parse(cfg.hooks) ?? {};
751
832
  const newHooks = { ...hooks };
752
- pruneNestedEvent(newHooks, "UserPromptSubmit");
753
833
  pruneNestedEvent(newHooks, "PostToolUseFailure");
754
834
  if (events.includes("SessionStart")) newHooks.SessionStart = [...filterOutMembank(Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []), {
755
835
  matcher: "",
@@ -758,6 +838,13 @@ const writers = {
758
838
  command: "npx -y @membank/cli inject --harness claude-code"
759
839
  }]
760
840
  }];
841
+ if (events.includes("UserPromptSubmit")) newHooks.UserPromptSubmit = [...filterOutMembank(Array.isArray(hooks.UserPromptSubmit) ? hooks.UserPromptSubmit : []), {
842
+ matcher: "",
843
+ hooks: [{
844
+ type: "command",
845
+ command: "npx -y @membank/cli inject --harness claude-code --event user-prompt-submit"
846
+ }]
847
+ }];
761
848
  writeJsonAtomic(cfgPath, {
762
849
  ...cfg,
763
850
  hooks: newHooks
@@ -768,31 +855,42 @@ const writers = {
768
855
  "copilot-cli": {
769
856
  inspect(resolver) {
770
857
  const cfgPath = join(resolver.home(), ".copilot", "settings.json");
771
- const hooks = readJson(cfgPath).hooks ?? {};
858
+ const cfg = readJson(cfgPath);
859
+ const hooks = MaybeJsonObjectSchema.parse(cfg.hooks) ?? {};
860
+ const sessionStart = Array.isArray(hooks.sessionStart) ? hooks.sessionStart : [];
861
+ const userPromptSubmitted = Array.isArray(hooks.userPromptSubmitted) ? hooks.userPromptSubmitted : [];
772
862
  return {
773
863
  status: "ready",
774
864
  configPath: cfgPath,
775
865
  hooks: [{
776
866
  event: "sessionStart",
777
867
  command: "npx -y @membank/cli inject --harness copilot-cli",
778
- existingCommand: extractInjectCommand(Array.isArray(hooks.sessionStart) ? hooks.sessionStart : []) || null
868
+ existingCommand: extractInjectCommand(sessionStart) || null
869
+ }, {
870
+ event: "userPromptSubmitted",
871
+ command: "npx -y @membank/cli inject --harness copilot-cli --event user-prompt-submit",
872
+ existingCommand: extractInjectCommand(userPromptSubmitted) || null
779
873
  }]
780
874
  };
781
875
  },
782
876
  write(resolver, events) {
783
877
  const cfgPath = join(resolver.home(), ".copilot", "settings.json");
784
878
  const cfg = readJson(cfgPath);
785
- const hooks = cfg.hooks ?? {};
879
+ const hooks = MaybeJsonObjectSchema.parse(cfg.hooks) ?? {};
786
880
  const newHooks = { ...hooks };
787
- pruneFlatEvent(newHooks, "userPromptSubmitted");
788
881
  pruneFlatEvent(newHooks, "postToolUseFailure");
789
882
  if (events.includes("sessionStart")) newHooks.sessionStart = [...filterOutMembankFlat(Array.isArray(hooks.sessionStart) ? hooks.sessionStart : []), {
790
883
  type: "command",
791
884
  bash: "npx -y @membank/cli inject --harness copilot-cli",
792
885
  timeoutSec: 30
793
886
  }];
887
+ if (events.includes("userPromptSubmitted")) newHooks.userPromptSubmitted = [...filterOutMembankFlat(Array.isArray(hooks.userPromptSubmitted) ? hooks.userPromptSubmitted : []), {
888
+ type: "command",
889
+ bash: "npx -y @membank/cli inject --harness copilot-cli --event user-prompt-submit",
890
+ timeoutSec: 30
891
+ }];
794
892
  writeJsonAtomic(cfgPath, {
795
- version: cfg.version ?? 1,
893
+ version: OptionalNumberSchema.parse(cfg.version) ?? 1,
796
894
  ...cfg,
797
895
  hooks: newHooks
798
896
  });
@@ -802,23 +900,29 @@ const writers = {
802
900
  codex: {
803
901
  inspect(resolver) {
804
902
  const cfgPath = join(resolver.home(), ".codex", "hooks.json");
805
- const hooks = readJson(cfgPath).hooks ?? {};
903
+ const cfg = readJson(cfgPath);
904
+ const hooks = MaybeJsonObjectSchema.parse(cfg.hooks) ?? {};
905
+ const sessionStartInner = (Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []).flatMap(getHooksArray);
906
+ const userPromptSubmitInner = (Array.isArray(hooks.UserPromptSubmit) ? hooks.UserPromptSubmit : []).flatMap(getHooksArray);
806
907
  return {
807
908
  status: "ready",
808
909
  configPath: cfgPath,
809
910
  hooks: [{
810
911
  event: "SessionStart",
811
912
  command: "npx -y @membank/cli inject --harness codex",
812
- existingCommand: extractInjectCommand((Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []).flatMap(getHooksArray)) || null
913
+ existingCommand: extractInjectCommand(sessionStartInner) || null
914
+ }, {
915
+ event: "UserPromptSubmit",
916
+ command: "npx -y @membank/cli inject --harness codex --event user-prompt-submit",
917
+ existingCommand: extractInjectCommand(userPromptSubmitInner) || null
813
918
  }]
814
919
  };
815
920
  },
816
921
  write(resolver, events) {
817
922
  const cfgPath = join(resolver.home(), ".codex", "hooks.json");
818
923
  const cfg = readJson(cfgPath);
819
- const hooks = cfg.hooks ?? {};
924
+ const hooks = MaybeJsonObjectSchema.parse(cfg.hooks) ?? {};
820
925
  const newHooks = { ...hooks };
821
- pruneNestedEvent(newHooks, "UserPromptSubmit");
822
926
  pruneNestedEvent(newHooks, "PostToolUse");
823
927
  if (events.includes("SessionStart")) newHooks.SessionStart = [...filterOutMembank(Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []), {
824
928
  matcher: "",
@@ -828,6 +932,14 @@ const writers = {
828
932
  timeout: 30
829
933
  }]
830
934
  }];
935
+ if (events.includes("UserPromptSubmit")) newHooks.UserPromptSubmit = [...filterOutMembank(Array.isArray(hooks.UserPromptSubmit) ? hooks.UserPromptSubmit : []), {
936
+ matcher: "",
937
+ hooks: [{
938
+ type: "command",
939
+ command: "npx -y @membank/cli inject --harness codex --event user-prompt-submit",
940
+ timeout: 30
941
+ }]
942
+ }];
831
943
  writeJsonAtomic(cfgPath, {
832
944
  ...cfg,
833
945
  hooks: newHooks
@@ -1034,7 +1146,7 @@ var SetupOrchestrator = class {
1034
1146
  const out = json ? () => {} : this.#out;
1035
1147
  let detected;
1036
1148
  if (harness !== void 0) detected = [{
1037
- name: harness,
1149
+ name: SetupHarnessSchema.parse(harness),
1038
1150
  configPath: ""
1039
1151
  }];
1040
1152
  else detected = this.#detector();
@@ -1241,7 +1353,7 @@ var SetupOrchestrator = class {
1241
1353
  if (process.argv.includes("--mcp")) await startServer();
1242
1354
  const program = new Command();
1243
1355
  program.name("membank").description("LLM memory management system").option("--json", "emit machine-readable JSON only").option("-y, --yes", "skip all confirmation prompts").option("--mcp", "start the MCP stdio server (for harness integration)");
1244
- program.command("query <queryText>").description("search memories by semantic similarity").option("--type <type>", "filter by memory type (correction|preference|decision|learning|fact)").option("--limit <n>", "maximum number of results", "10").action(async (queryText, cmdOptions) => {
1356
+ program.command("query <queryText>").description("search memories by semantic similarity").option("--type <type>", "filter by memory type (correction|preference|decision|learning|fact)").option("--limit <n>", "maximum number of results", "10").option("--include-pinned", "include pinned memories in results (excluded by default)").action(async (queryText, cmdOptions) => {
1245
1357
  const globalOpts = program.opts();
1246
1358
  const formatter = Formatter.create(globalOpts.json === true);
1247
1359
  try {
@@ -1356,7 +1468,7 @@ program.command("setup").description("detect installed harnesses and write MCP c
1356
1468
  const formatter = Formatter.create(globalOpts.json === true);
1357
1469
  const interactive = !formatter.isJson && !autoYes && cmdOptions.harness === void 0;
1358
1470
  if (cmdOptions.harness !== void 0) {
1359
- if (!SUPPORTED_HARNESSES.some((h) => h === cmdOptions.harness)) {
1471
+ if (!SetupHarnessSchema.safeParse(cmdOptions.harness).success) {
1360
1472
  formatter.error(`Unknown harness: "${cmdOptions.harness}". Supported: ${SUPPORTED_HARNESSES.join(", ")}`);
1361
1473
  process.exit(1);
1362
1474
  }
@@ -1418,15 +1530,26 @@ program.command("setup").description("detect installed harnesses and write MCP c
1418
1530
  process.exit(2);
1419
1531
  }
1420
1532
  });
1533
+ 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) => {
1534
+ const globalOpts = program.opts();
1535
+ const formatter = Formatter.create(globalOpts.json === true);
1536
+ try {
1537
+ await reviewCommand(cmdOptions, formatter);
1538
+ } catch (err) {
1539
+ formatter.error(err instanceof Error ? err.message : String(err));
1540
+ process.exit(2);
1541
+ }
1542
+ });
1421
1543
  program.command("migrate <mode> [name]").description("list or run a named data migration (modes: list, run)").action(async (mode, name) => {
1422
- if (mode !== "list" && mode !== "run") {
1544
+ const modeResult = MigrateModeSchema.safeParse(mode);
1545
+ if (!modeResult.success) {
1423
1546
  process.stderr.write(`Error: mode must be "list" or "run"\n`);
1424
1547
  process.exit(1);
1425
1548
  }
1426
1549
  const globalOpts = program.opts();
1427
1550
  const formatter = Formatter.create(globalOpts.json === true);
1428
1551
  try {
1429
- await migrateCommand(mode, name, formatter);
1552
+ await migrateCommand(modeResult.data, name, formatter);
1430
1553
  } catch (err) {
1431
1554
  formatter.error(err instanceof Error ? err.message : String(err));
1432
1555
  process.exit(2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@membank/cli",
3
- "version": "0.7.1",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -20,9 +20,10 @@
20
20
  "cli-table3": "^0.6.5",
21
21
  "commander": "^14.0.3",
22
22
  "ora": "^9.4.0",
23
- "@membank/core": "0.6.1",
24
- "@membank/mcp": "0.8.0",
25
- "@membank/dashboard": "0.4.0"
23
+ "zod": "^4.4.3",
24
+ "@membank/dashboard": "0.5.0",
25
+ "@membank/core": "0.8.0",
26
+ "@membank/mcp": "0.10.0"
26
27
  },
27
28
  "devDependencies": {
28
29
  "@types/node": "^25.6.0",