@membank/cli 0.7.1 → 0.8.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 +174 -102
  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,7 +99,7 @@ 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,
@@ -83,17 +123,6 @@ function exportCommand(db, formatter, opts) {
83
123
  }
84
124
  //#endregion
85
125
  //#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
126
  async function importCommand(filePath, db, formatter, prompt) {
98
127
  let raw;
99
128
  try {
@@ -109,16 +138,12 @@ async function importCommand(filePath, db, formatter, prompt) {
109
138
  formatter.error(`Invalid JSON in file: ${filePath}`);
110
139
  process.exit(1);
111
140
  }
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`);
141
+ const parseResult = ExportFileSchema.safeParse(parsed);
142
+ if (!parseResult.success) {
143
+ formatter.error(`Invalid export file: ${parseResult.error.issues[0]?.message ?? "unknown error"}`);
119
144
  process.exit(1);
120
145
  }
121
- const count = parsed.memories.length;
146
+ const count = parseResult.data.memories.length;
122
147
  if (formatter.isJson) process.stdout.write(`${JSON.stringify({ found: count })}\n`);
123
148
  else process.stdout.write(`Found ${count} memories to import.\n`);
124
149
  if (!await prompt.confirm("Import?")) return;
@@ -126,7 +151,7 @@ async function importCommand(filePath, db, formatter, prompt) {
126
151
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
127
152
  const insertEmbedding = db.db.prepare(`INSERT OR REPLACE INTO embeddings (rowid, embedding) SELECT m.rowid, ? FROM memories m WHERE m.id = ?`);
128
153
  db.db.transaction(() => {
129
- for (const rec of parsed.memories) {
154
+ for (const rec of parseResult.data.memories) {
130
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());
131
156
  if (rec.embedding !== null && rec.embedding !== void 0) {
132
157
  const buf = Buffer.from(rec.embedding, "base64");
@@ -139,17 +164,21 @@ async function importCommand(filePath, db, formatter, prompt) {
139
164
  }
140
165
  //#endregion
141
166
  //#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.";
167
+ 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");
168
+ function xmlEscape(s) {
169
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
170
+ }
143
171
  function formatContext(ctx) {
144
- const lines = [];
172
+ const parts = [];
145
173
  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");
174
+ 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>`);
179
+ }
180
+ parts.push(`<memory-guidance>\n${MEMORY_GUIDANCE}\n</memory-guidance>`);
181
+ return parts.join("\n");
153
182
  }
154
183
  function outputAdditionalContext(text, harness, eventName) {
155
184
  if (harness === "claude-code") {
@@ -165,22 +194,36 @@ function outputAdditionalContext(text, harness, eventName) {
165
194
  }
166
195
  process.stdout.write(`${text}\n`);
167
196
  }
168
- async function handleSessionStart(opts) {
169
- const projectScope = (await resolveProject()).hash;
197
+ async function buildText() {
198
+ const resolved = await resolveProject();
170
199
  const db = DatabaseManager.open();
171
- let text;
172
200
  try {
173
- text = formatContext(new SessionContextBuilder(db).getSessionContext(projectScope));
201
+ return formatContext(new SessionContextBuilder(db).getSessionContext(resolved.hash));
174
202
  } finally {
175
203
  db.close();
176
204
  }
177
- if (!text) process.exit(0);
178
- const harness = opts.harness;
179
- outputAdditionalContext(text, harness, "SessionStart");
205
+ }
206
+ async function handleEvent(harness, eventName) {
207
+ const text = await buildText().catch((err) => {
208
+ const msg = err instanceof Error ? err.message : String(err);
209
+ process.stderr.write(`membank inject: ${msg}\n`);
210
+ return null;
211
+ });
212
+ if (text === null) process.exit(0);
213
+ outputAdditionalContext(text, harness, eventName);
180
214
  }
181
215
  async function injectCommand(opts) {
182
- if (opts.event !== void 0 && opts.event !== "session-start") process.exit(0);
183
- await handleSessionStart(opts);
216
+ const harnessResult = InjectionHarnessSchema.safeParse(opts.harness);
217
+ const harness = harnessResult.success ? harnessResult.data : void 0;
218
+ if (opts.event === "session-start" || opts.event === void 0) {
219
+ await handleEvent(harness, "SessionStart");
220
+ return;
221
+ }
222
+ if (opts.event === "user-prompt-submit") {
223
+ await handleEvent(harness, "UserPromptSubmit");
224
+ return;
225
+ }
226
+ process.exit(0);
184
227
  }
185
228
  //#endregion
186
229
  //#region src/commands/list.ts
@@ -188,7 +231,7 @@ async function listCommand(options, formatter) {
188
231
  const db = DatabaseManager.open();
189
232
  try {
190
233
  const memories = new MemoryRepository(db, new EmbeddingService(), new ProjectRepository(db)).list({
191
- type: options.type,
234
+ type: options.type !== void 0 ? MemoryTypeSchema$1.parse(options.type) : void 0,
192
235
  pinned: options.pinned
193
236
  });
194
237
  formatter.outputMemories(memories);
@@ -255,12 +298,13 @@ async function queryCommand(queryText, options, formatter) {
255
298
  try {
256
299
  const embedding = new EmbeddingService();
257
300
  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;
301
+ const limit = options.limit !== void 0 ? LimitSchema.parse(options.limit) : 10;
259
302
  const spinner = formatter.isJson ? null : ora("Searching memories…").start();
260
303
  const results = await engine.query({
261
304
  query: queryText,
262
- type: options.type,
263
- limit
305
+ type: options.type !== void 0 ? MemoryTypeSchema$1.parse(options.type) : void 0,
306
+ limit,
307
+ includePinned: options.includePinned
264
308
  });
265
309
  spinner?.succeed(`${results.length} result${results.length === 1 ? "" : "s"} found`);
266
310
  formatter.outputQueryResults(results);
@@ -473,6 +517,21 @@ async function execFileNoThrow(cmd, args) {
473
517
  }
474
518
  }
475
519
  //#endregion
520
+ //#region src/utils/json.ts
521
+ function readJson(path) {
522
+ try {
523
+ return MutableJsonObjectSchema.parse(JSON.parse(readFileSync(path, "utf8")));
524
+ } catch {
525
+ return {};
526
+ }
527
+ }
528
+ function writeJsonAtomic(path, data) {
529
+ mkdirSync(dirname(path), { recursive: true });
530
+ const tmp = join(mkdtempSync(join(tmpdir(), "membank-")), "cfg.json");
531
+ writeFileSync(tmp, JSON.stringify(data, null, 2));
532
+ renameSync(tmp, path);
533
+ }
534
+ //#endregion
476
535
  //#region src/setup/harness-config-writer.ts
477
536
  var CommandError = class extends Error {
478
537
  command;
@@ -490,21 +549,8 @@ const defaultPathResolver$1 = {
490
549
  },
491
550
  cwd: () => process.cwd()
492
551
  };
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
552
  function hasKey(container, key) {
507
- return container !== null && typeof container === "object" && key in container;
553
+ return container !== null && typeof container === "object" && Object.hasOwn(container, key);
508
554
  }
509
555
  function assertCliFound(result, cli, command) {
510
556
  if (result.exitCode === 127) throw new CommandError(`${cli} CLI not found — install ${cli} first`, command);
@@ -524,7 +570,7 @@ const writers$1 = {
524
570
  };
525
571
  },
526
572
  async write(resolver, run, { overwrite = false } = {}) {
527
- const configured = hasKey(readJson$1(join(resolver.home(), ".claude.json")).mcpServers, "membank");
573
+ const configured = hasKey(readJson(join(resolver.home(), ".claude.json")).mcpServers, "membank");
528
574
  if (configured && !overwrite) return { status: "already-configured" };
529
575
  if (configured) {
530
576
  const removeArgs = [
@@ -564,12 +610,12 @@ const writers$1 = {
564
610
  },
565
611
  async write(resolver, _run, { overwrite = false } = {}) {
566
612
  const cfgPath = join(resolver.home(), ".copilot", "mcp-config.json");
567
- const cfg = readJson$1(cfgPath);
613
+ const cfg = readJson(cfgPath);
568
614
  if (hasKey(cfg.mcpServers, "membank") && !overwrite) return { status: "already-configured" };
569
- writeJsonAtomic$1(cfgPath, {
615
+ writeJsonAtomic(cfgPath, {
570
616
  ...cfg,
571
617
  mcpServers: {
572
- ...cfg.mcpServers,
618
+ ...MaybeJsonObjectSchema.parse(cfg.mcpServers),
573
619
  membank: {
574
620
  command: "npx",
575
621
  args: [
@@ -630,12 +676,12 @@ const writers$1 = {
630
676
  },
631
677
  async write(resolver, _run, { overwrite = false } = {}) {
632
678
  const cfgPath = join(resolver.home(), ".config", "opencode", "opencode.json");
633
- const cfg = readJson$1(cfgPath);
679
+ const cfg = readJson(cfgPath);
634
680
  if (hasKey(cfg.mcp, "membank") && !overwrite) return { status: "already-configured" };
635
- writeJsonAtomic$1(cfgPath, {
681
+ writeJsonAtomic(cfgPath, {
636
682
  ...cfg,
637
683
  mcp: {
638
- ...cfg.mcp,
684
+ ...MaybeJsonObjectSchema.parse(cfg.mcp),
639
685
  membank: {
640
686
  type: "local",
641
687
  command: [
@@ -651,7 +697,7 @@ const writers$1 = {
651
697
  }
652
698
  }
653
699
  };
654
- const SUPPORTED_HARNESSES = Object.keys(writers$1);
700
+ const SUPPORTED_HARNESSES = SETUP_HARNESS_VALUES;
655
701
  var HarnessConfigWriter = class {
656
702
  #resolver;
657
703
  #run;
@@ -677,23 +723,10 @@ const defaultPathResolver = { home: () => {
677
723
  if (!h) throw new Error("Cannot determine home directory");
678
724
  return h;
679
725
  } };
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
726
  function getHooksArray(group) {
694
727
  if (typeof group !== "object" || group === null) return [];
695
- const h = group.hooks;
696
- return Array.isArray(h) ? h : [];
728
+ if (!("hooks" in group)) return [];
729
+ return Array.isArray(group.hooks) ? group.hooks : [];
697
730
  }
698
731
  function findMembankHookCommand(hooks, pattern) {
699
732
  for (const h of hooks) {
@@ -733,23 +766,29 @@ const writers = {
733
766
  "claude-code": {
734
767
  inspect(resolver) {
735
768
  const cfgPath = join(resolver.home(), ".claude", "settings.json");
736
- const hooks = readJson(cfgPath).hooks ?? {};
769
+ const cfg = readJson(cfgPath);
770
+ const hooks = MaybeJsonObjectSchema.parse(cfg.hooks) ?? {};
771
+ const sessionStartInner = (Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []).flatMap(getHooksArray);
772
+ const userPromptSubmitInner = (Array.isArray(hooks.UserPromptSubmit) ? hooks.UserPromptSubmit : []).flatMap(getHooksArray);
737
773
  return {
738
774
  status: "ready",
739
775
  configPath: cfgPath,
740
776
  hooks: [{
741
777
  event: "SessionStart",
742
778
  command: "npx -y @membank/cli inject --harness claude-code",
743
- existingCommand: extractInjectCommand((Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []).flatMap(getHooksArray)) || null
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
744
784
  }]
745
785
  };
746
786
  },
747
787
  write(resolver, events) {
748
788
  const cfgPath = join(resolver.home(), ".claude", "settings.json");
749
789
  const cfg = readJson(cfgPath);
750
- const hooks = cfg.hooks ?? {};
790
+ const hooks = MaybeJsonObjectSchema.parse(cfg.hooks) ?? {};
751
791
  const newHooks = { ...hooks };
752
- pruneNestedEvent(newHooks, "UserPromptSubmit");
753
792
  pruneNestedEvent(newHooks, "PostToolUseFailure");
754
793
  if (events.includes("SessionStart")) newHooks.SessionStart = [...filterOutMembank(Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []), {
755
794
  matcher: "",
@@ -758,6 +797,13 @@ const writers = {
758
797
  command: "npx -y @membank/cli inject --harness claude-code"
759
798
  }]
760
799
  }];
800
+ if (events.includes("UserPromptSubmit")) newHooks.UserPromptSubmit = [...filterOutMembank(Array.isArray(hooks.UserPromptSubmit) ? hooks.UserPromptSubmit : []), {
801
+ matcher: "",
802
+ hooks: [{
803
+ type: "command",
804
+ command: "npx -y @membank/cli inject --harness claude-code --event user-prompt-submit"
805
+ }]
806
+ }];
761
807
  writeJsonAtomic(cfgPath, {
762
808
  ...cfg,
763
809
  hooks: newHooks
@@ -768,31 +814,42 @@ const writers = {
768
814
  "copilot-cli": {
769
815
  inspect(resolver) {
770
816
  const cfgPath = join(resolver.home(), ".copilot", "settings.json");
771
- const hooks = readJson(cfgPath).hooks ?? {};
817
+ const cfg = readJson(cfgPath);
818
+ const hooks = MaybeJsonObjectSchema.parse(cfg.hooks) ?? {};
819
+ const sessionStart = Array.isArray(hooks.sessionStart) ? hooks.sessionStart : [];
820
+ const userPromptSubmitted = Array.isArray(hooks.userPromptSubmitted) ? hooks.userPromptSubmitted : [];
772
821
  return {
773
822
  status: "ready",
774
823
  configPath: cfgPath,
775
824
  hooks: [{
776
825
  event: "sessionStart",
777
826
  command: "npx -y @membank/cli inject --harness copilot-cli",
778
- existingCommand: extractInjectCommand(Array.isArray(hooks.sessionStart) ? hooks.sessionStart : []) || null
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
779
832
  }]
780
833
  };
781
834
  },
782
835
  write(resolver, events) {
783
836
  const cfgPath = join(resolver.home(), ".copilot", "settings.json");
784
837
  const cfg = readJson(cfgPath);
785
- const hooks = cfg.hooks ?? {};
838
+ const hooks = MaybeJsonObjectSchema.parse(cfg.hooks) ?? {};
786
839
  const newHooks = { ...hooks };
787
- pruneFlatEvent(newHooks, "userPromptSubmitted");
788
840
  pruneFlatEvent(newHooks, "postToolUseFailure");
789
841
  if (events.includes("sessionStart")) newHooks.sessionStart = [...filterOutMembankFlat(Array.isArray(hooks.sessionStart) ? hooks.sessionStart : []), {
790
842
  type: "command",
791
843
  bash: "npx -y @membank/cli inject --harness copilot-cli",
792
844
  timeoutSec: 30
793
845
  }];
846
+ if (events.includes("userPromptSubmitted")) newHooks.userPromptSubmitted = [...filterOutMembankFlat(Array.isArray(hooks.userPromptSubmitted) ? hooks.userPromptSubmitted : []), {
847
+ type: "command",
848
+ bash: "npx -y @membank/cli inject --harness copilot-cli --event user-prompt-submit",
849
+ timeoutSec: 30
850
+ }];
794
851
  writeJsonAtomic(cfgPath, {
795
- version: cfg.version ?? 1,
852
+ version: OptionalNumberSchema.parse(cfg.version) ?? 1,
796
853
  ...cfg,
797
854
  hooks: newHooks
798
855
  });
@@ -802,23 +859,29 @@ const writers = {
802
859
  codex: {
803
860
  inspect(resolver) {
804
861
  const cfgPath = join(resolver.home(), ".codex", "hooks.json");
805
- const hooks = readJson(cfgPath).hooks ?? {};
862
+ const cfg = readJson(cfgPath);
863
+ const hooks = MaybeJsonObjectSchema.parse(cfg.hooks) ?? {};
864
+ const sessionStartInner = (Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []).flatMap(getHooksArray);
865
+ const userPromptSubmitInner = (Array.isArray(hooks.UserPromptSubmit) ? hooks.UserPromptSubmit : []).flatMap(getHooksArray);
806
866
  return {
807
867
  status: "ready",
808
868
  configPath: cfgPath,
809
869
  hooks: [{
810
870
  event: "SessionStart",
811
871
  command: "npx -y @membank/cli inject --harness codex",
812
- existingCommand: extractInjectCommand((Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []).flatMap(getHooksArray)) || null
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
813
877
  }]
814
878
  };
815
879
  },
816
880
  write(resolver, events) {
817
881
  const cfgPath = join(resolver.home(), ".codex", "hooks.json");
818
882
  const cfg = readJson(cfgPath);
819
- const hooks = cfg.hooks ?? {};
883
+ const hooks = MaybeJsonObjectSchema.parse(cfg.hooks) ?? {};
820
884
  const newHooks = { ...hooks };
821
- pruneNestedEvent(newHooks, "UserPromptSubmit");
822
885
  pruneNestedEvent(newHooks, "PostToolUse");
823
886
  if (events.includes("SessionStart")) newHooks.SessionStart = [...filterOutMembank(Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []), {
824
887
  matcher: "",
@@ -828,6 +891,14 @@ const writers = {
828
891
  timeout: 30
829
892
  }]
830
893
  }];
894
+ if (events.includes("UserPromptSubmit")) newHooks.UserPromptSubmit = [...filterOutMembank(Array.isArray(hooks.UserPromptSubmit) ? hooks.UserPromptSubmit : []), {
895
+ matcher: "",
896
+ hooks: [{
897
+ type: "command",
898
+ command: "npx -y @membank/cli inject --harness codex --event user-prompt-submit",
899
+ timeout: 30
900
+ }]
901
+ }];
831
902
  writeJsonAtomic(cfgPath, {
832
903
  ...cfg,
833
904
  hooks: newHooks
@@ -1034,7 +1105,7 @@ var SetupOrchestrator = class {
1034
1105
  const out = json ? () => {} : this.#out;
1035
1106
  let detected;
1036
1107
  if (harness !== void 0) detected = [{
1037
- name: harness,
1108
+ name: SetupHarnessSchema.parse(harness),
1038
1109
  configPath: ""
1039
1110
  }];
1040
1111
  else detected = this.#detector();
@@ -1241,7 +1312,7 @@ var SetupOrchestrator = class {
1241
1312
  if (process.argv.includes("--mcp")) await startServer();
1242
1313
  const program = new Command();
1243
1314
  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) => {
1315
+ 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
1316
  const globalOpts = program.opts();
1246
1317
  const formatter = Formatter.create(globalOpts.json === true);
1247
1318
  try {
@@ -1356,7 +1427,7 @@ program.command("setup").description("detect installed harnesses and write MCP c
1356
1427
  const formatter = Formatter.create(globalOpts.json === true);
1357
1428
  const interactive = !formatter.isJson && !autoYes && cmdOptions.harness === void 0;
1358
1429
  if (cmdOptions.harness !== void 0) {
1359
- if (!SUPPORTED_HARNESSES.some((h) => h === cmdOptions.harness)) {
1430
+ if (!SetupHarnessSchema.safeParse(cmdOptions.harness).success) {
1360
1431
  formatter.error(`Unknown harness: "${cmdOptions.harness}". Supported: ${SUPPORTED_HARNESSES.join(", ")}`);
1361
1432
  process.exit(1);
1362
1433
  }
@@ -1419,14 +1490,15 @@ program.command("setup").description("detect installed harnesses and write MCP c
1419
1490
  }
1420
1491
  });
1421
1492
  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") {
1493
+ const modeResult = MigrateModeSchema.safeParse(mode);
1494
+ if (!modeResult.success) {
1423
1495
  process.stderr.write(`Error: mode must be "list" or "run"\n`);
1424
1496
  process.exit(1);
1425
1497
  }
1426
1498
  const globalOpts = program.opts();
1427
1499
  const formatter = Formatter.create(globalOpts.json === true);
1428
1500
  try {
1429
- await migrateCommand(mode, name, formatter);
1501
+ await migrateCommand(modeResult.data, name, formatter);
1430
1502
  } catch (err) {
1431
1503
  formatter.error(err instanceof Error ? err.message : String(err));
1432
1504
  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.8.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/core": "0.7.0",
25
+ "@membank/mcp": "0.9.0",
26
+ "@membank/dashboard": "0.4.1"
26
27
  },
27
28
  "devDependencies": {
28
29
  "@types/node": "^25.6.0",