@membank/cli 0.3.0 → 0.4.1

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 (3) hide show
  1. package/README.md +13 -5
  2. package/dist/index.mjs +143 -233
  3. package/package.json +4 -4
package/README.md CHANGED
@@ -33,7 +33,7 @@ Options:
33
33
  --json Machine-readable output
34
34
  ```
35
35
 
36
- Supported harnesses: `claude-code`, `copilot`, `codex`, `opencode`
36
+ Supported harnesses: `claude-code`, `copilot`, `codex`, `opencode` (see `membank setup` for harness-specific setup instructions)
37
37
 
38
38
  ## Commands
39
39
 
@@ -125,6 +125,16 @@ Output session context formatted for a harness. Called automatically by session
125
125
  membank inject --harness claude-code --scope <project-scope>
126
126
  ```
127
127
 
128
+ ### `membank dashboard`
129
+
130
+ Start the web dashboard for browsing and managing memories.
131
+
132
+ ```bash
133
+ membank dashboard
134
+ ```
135
+
136
+ Opens http://localhost:3847 by default. Features: full-text search, filtering by type/scope/pin status, edit memory metadata, view dedup reviews, and storage statistics.
137
+
128
138
  ## Global flags
129
139
 
130
140
  ```
@@ -143,11 +153,9 @@ Starts the stdio MCP server. This is what harnesses connect to — `setup` write
143
153
 
144
154
  ## Session hooks
145
155
 
146
- `setup` installs two hooks:
147
-
148
- **Session start** — calls `membank inject` to prepend pinned memories into the LLM context at the beginning of every session.
156
+ `setup` installs a hook for Claude Code that injects memories at the start of each session:
149
157
 
150
- **Session stop (Claude Code only)** — prompts the LLM to review the session and call `save_memory` for any notable corrections, preferences, or decisions.
158
+ **SessionStart** — calls `membank inject` to prepend pinned memories into the LLM context at the beginning of every session.
151
159
 
152
160
  ## Requirements
153
161
 
package/dist/index.mjs CHANGED
@@ -134,7 +134,7 @@ async function importCommand(filePath, db, formatter, prompt) {
134
134
  }
135
135
  //#endregion
136
136
  //#region src/commands/inject.ts
137
- const MEMORY_GUIDANCE = "[Memory Guidance]: query_memory before answering on topics where past preferences, corrections, or decisions may apply; save_memory when user corrects you, states a preference, makes a decision, or shares something worth retaining across sessions; update_memory to refine an existing memory (query first to find it) or to set pinned=true/false; delete_memory when a memory is wrong or no longer relevant; pin high-value memories that should always appear at session start";
137
+ const MEMORY_GUIDANCE = "[Memory Guidance]: Persistent memory is available via query_memory, save_memory, update_memory, delete_memory. Skipping save_memory when the user gives a correction or preference means they have to repeat themselves next session that is the failure mode to avoid. Skipping query_memory on topics that touch prior decisions means contradicting yourself. Default to saving (type: correction|preference|decision|learning|fact) when in doubt; rely on dedup to handle redundancy. Pin anything that should appear at every session start.";
138
138
  function formatContext(ctx) {
139
139
  const lines = [];
140
140
  const statParts = Object.entries(ctx.stats).filter(([, count]) => count > 0).map(([type, count]) => `${count} ${type}${count !== 1 ? "s" : ""}`);
@@ -160,51 +160,6 @@ function outputAdditionalContext(text, harness, eventName) {
160
160
  }
161
161
  process.stdout.write(`${text}\n`);
162
162
  }
163
- async function readStdin() {
164
- if (process.stdin.isTTY) return "";
165
- return new Promise((resolve) => {
166
- const chunks = [];
167
- const timeout = setTimeout(() => resolve(""), 1e3);
168
- process.stdin.on("data", (chunk) => chunks.push(chunk));
169
- process.stdin.on("end", () => {
170
- clearTimeout(timeout);
171
- resolve(Buffer.concat(chunks).toString("utf8"));
172
- });
173
- process.stdin.on("error", () => {
174
- clearTimeout(timeout);
175
- resolve("");
176
- });
177
- });
178
- }
179
- const FEEDBACK_PATTERNS = [
180
- /\bdon'?t\b/i,
181
- /\bstop\b/i,
182
- /\bnever\b/i,
183
- /\balways\b/i,
184
- /\bremember\b/i,
185
- /\bprefer\b/i,
186
- /\bi (like|want|hate|dislike)\b/i,
187
- /\bfrom now on\b/i,
188
- /\bkeep in mind\b/i,
189
- /\bnote that\b/i,
190
- /\bstop doing\b/i,
191
- /\bstop using\b/i,
192
- /\bthat'?s wrong\b/i,
193
- /\bno,?\s+(actually|that'?s)\b/i,
194
- /\bplease (don'?t|stop|always|never)\b/i
195
- ];
196
- function looksLikeFeedback(prompt) {
197
- return FEEDBACK_PATTERNS.some((p) => p.test(prompt));
198
- }
199
- function isToolFailure(data) {
200
- if (data.hook_event_name === "PostToolUseFailure") return true;
201
- if (typeof data.error_message === "string" && data.error_message.length > 0) return true;
202
- const response = data.tool_result ?? data.tool_response;
203
- if (typeof response === "object" && response !== null) {
204
- if (response.is_error === true) return true;
205
- }
206
- return false;
207
- }
208
163
  async function handleSessionStart(opts) {
209
164
  const projectScope = opts.scope ?? await resolveScope();
210
165
  const db = DatabaseManager.open();
@@ -218,41 +173,8 @@ async function handleSessionStart(opts) {
218
173
  const harness = opts.harness;
219
174
  outputAdditionalContext(text, harness, "SessionStart");
220
175
  }
221
- async function handleUserPrompt(harness) {
222
- const raw = await readStdin();
223
- if (!raw) process.exit(0);
224
- let data;
225
- try {
226
- data = JSON.parse(raw);
227
- } catch {
228
- process.exit(0);
229
- }
230
- if (!looksLikeFeedback(typeof data.prompt === "string" ? data.prompt : "")) process.exit(0);
231
- outputAdditionalContext("User prompt may contain a correction, preference, or decision worth saving. After responding, evaluate: should this be saved as a memory? If yes, call save_memory with the appropriate type (correction/preference/decision/learning) and scope (global or project).", harness, "UserPromptSubmit");
232
- }
233
- async function handleToolFailure(harness) {
234
- const raw = await readStdin();
235
- if (!raw) process.exit(0);
236
- let data;
237
- try {
238
- data = JSON.parse(raw);
239
- } catch {
240
- process.exit(0);
241
- }
242
- if (!isToolFailure(data)) process.exit(0);
243
- outputAdditionalContext(`Tool "${typeof data.tool_name === "string" ? data.tool_name : "unknown"}" failed. If this reveals a non-obvious constraint, environment issue, or repeatable failure pattern, call save_memory with type "learning" to prevent repeating it.`, harness, "PostToolUseFailure");
244
- }
245
176
  async function injectCommand(opts) {
246
- const harness = opts.harness;
247
- const event = opts.event ?? "session-start";
248
- if (event === "user-prompt") {
249
- await handleUserPrompt(harness);
250
- return;
251
- }
252
- if (event === "tool-failure") {
253
- await handleToolFailure(harness);
254
- return;
255
- }
177
+ if (opts.event !== void 0 && opts.event !== "session-start") process.exit(0);
256
178
  await handleSessionStart(opts);
257
179
  }
258
180
  //#endregion
@@ -491,6 +413,7 @@ function assertCliFound(result, cli) {
491
413
  }
492
414
  const MEMBANK_NPX_ARGS = [
493
415
  "npx",
416
+ "-y",
494
417
  "@membank/cli@latest",
495
418
  "--mcp"
496
419
  ];
@@ -532,7 +455,11 @@ const writers$1 = {
532
455
  ...cfg.mcpServers,
533
456
  membank: {
534
457
  command: "npx",
535
- args: ["@membank/cli@latest", "--mcp"]
458
+ args: [
459
+ "-y",
460
+ "@membank/cli@latest",
461
+ "--mcp"
462
+ ]
536
463
  }
537
464
  }
538
465
  });
@@ -575,6 +502,7 @@ const writers$1 = {
575
502
  type: "local",
576
503
  command: [
577
504
  "npx",
505
+ "-y",
578
506
  "@membank/cli@latest",
579
507
  "--mcp"
580
508
  ]
@@ -632,10 +560,10 @@ function findMembankHookCommand(hooks, pattern) {
632
560
  return "";
633
561
  }
634
562
  function containsMembankInject(hooks) {
635
- return findMembankHookCommand(hooks, "@membank/cli inject") !== "";
563
+ return findMembankHookCommand(hooks, "@membank/cli") !== "";
636
564
  }
637
565
  function extractInjectCommand(hooks) {
638
- return findMembankHookCommand(hooks, "@membank/cli inject");
566
+ return findMembankHookCommand(hooks, "@membank/cli");
639
567
  }
640
568
  function filterOutMembank(groups) {
641
569
  return groups.filter((g) => !containsMembankInject(getHooksArray(g)));
@@ -643,154 +571,139 @@ function filterOutMembank(groups) {
643
571
  function filterOutMembankFlat(hooks) {
644
572
  return hooks.filter((h) => !containsMembankInject([h]));
645
573
  }
574
+ function pruneNestedEvent(hooks, eventKey) {
575
+ const existing = hooks[eventKey];
576
+ if (!Array.isArray(existing)) return;
577
+ const cleaned = filterOutMembank(existing);
578
+ if (cleaned.length === 0) delete hooks[eventKey];
579
+ else hooks[eventKey] = cleaned;
580
+ }
581
+ function pruneFlatEvent(hooks, eventKey) {
582
+ const existing = hooks[eventKey];
583
+ if (!Array.isArray(existing)) return;
584
+ const cleaned = filterOutMembankFlat(existing);
585
+ if (cleaned.length === 0) delete hooks[eventKey];
586
+ else hooks[eventKey] = cleaned;
587
+ }
646
588
  const writers = {
647
589
  "claude-code": {
648
- replacement: "npx @membank/cli inject --harness claude-code",
649
- write(resolver, overwrite = false) {
590
+ inspect(resolver) {
591
+ const hooks = readJson(join(resolver.home(), ".claude", "settings.json")).hooks ?? {};
592
+ return {
593
+ status: "ready",
594
+ hooks: [{
595
+ event: "SessionStart",
596
+ command: "npx -y @membank/cli@latest inject --harness claude-code",
597
+ existingCommand: extractInjectCommand((Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []).flatMap(getHooksArray)) || null
598
+ }]
599
+ };
600
+ },
601
+ write(resolver, events) {
650
602
  const cfgPath = join(resolver.home(), ".claude", "settings.json");
651
603
  const cfg = readJson(cfgPath);
652
- const hooks = cfg.hooks;
653
- const existingSessionStart = Array.isArray(hooks?.SessionStart) ? hooks.SessionStart : [];
654
- const innerHooks = existingSessionStart.flatMap(getHooksArray);
655
- if (!overwrite && containsMembankInject(innerHooks)) return {
656
- status: "already-configured",
657
- existing: extractInjectCommand(innerHooks),
658
- replacement: this.replacement
659
- };
660
- const filteredSessionStart = overwrite ? filterOutMembank(existingSessionStart) : existingSessionStart;
661
- const existingUserPrompt = Array.isArray(hooks?.UserPromptSubmit) ? hooks.UserPromptSubmit : [];
662
- const existingToolFailure = Array.isArray(hooks?.PostToolUseFailure) ? hooks.PostToolUseFailure : [];
604
+ const hooks = cfg.hooks ?? {};
605
+ const newHooks = { ...hooks };
606
+ pruneNestedEvent(newHooks, "UserPromptSubmit");
607
+ pruneNestedEvent(newHooks, "PostToolUseFailure");
608
+ if (events.includes("SessionStart")) newHooks.SessionStart = [...filterOutMembank(Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []), {
609
+ matcher: "",
610
+ hooks: [{
611
+ type: "command",
612
+ command: "npx -y @membank/cli@latest inject --harness claude-code"
613
+ }]
614
+ }];
663
615
  writeJsonAtomic(cfgPath, {
664
616
  ...cfg,
665
- hooks: {
666
- ...hooks ?? {},
667
- SessionStart: [...filteredSessionStart, {
668
- matcher: "",
669
- hooks: [{
670
- type: "command",
671
- command: "npx @membank/cli inject --harness claude-code"
672
- }]
673
- }],
674
- UserPromptSubmit: [...filterOutMembank(existingUserPrompt), {
675
- matcher: "",
676
- hooks: [{
677
- type: "command",
678
- command: "npx @membank/cli inject --event user-prompt --harness claude-code"
679
- }]
680
- }],
681
- PostToolUseFailure: [...filterOutMembank(existingToolFailure), {
682
- matcher: "",
683
- hooks: [{
684
- type: "command",
685
- command: "npx @membank/cli inject --event tool-failure --harness claude-code"
686
- }]
687
- }]
688
- }
617
+ hooks: newHooks
689
618
  });
690
619
  return { status: "written" };
691
620
  }
692
621
  },
693
622
  "copilot-cli": {
694
- replacement: "npx @membank/cli inject --harness copilot-cli",
695
- write(resolver, overwrite = false) {
623
+ inspect(resolver) {
624
+ const hooks = readJson(join(resolver.home(), ".copilot", "settings.json")).hooks ?? {};
625
+ return {
626
+ status: "ready",
627
+ hooks: [{
628
+ event: "sessionStart",
629
+ command: "npx -y @membank/cli@latest inject --harness copilot-cli",
630
+ existingCommand: extractInjectCommand(Array.isArray(hooks.sessionStart) ? hooks.sessionStart : []) || null
631
+ }]
632
+ };
633
+ },
634
+ write(resolver, events) {
696
635
  const cfgPath = join(resolver.home(), ".copilot", "settings.json");
697
636
  const cfg = readJson(cfgPath);
698
- const hooks = cfg.hooks;
699
- const existingSessionStart = Array.isArray(hooks?.sessionStart) ? hooks.sessionStart : [];
700
- if (!overwrite && containsMembankInject(existingSessionStart)) return {
701
- status: "already-configured",
702
- existing: extractInjectCommand(existingSessionStart),
703
- replacement: this.replacement
704
- };
705
- const filteredSessionStart = overwrite ? filterOutMembankFlat(existingSessionStart) : existingSessionStart;
706
- const existingUserPrompt = Array.isArray(hooks?.userPromptSubmitted) ? hooks.userPromptSubmitted : [];
707
- const existingToolFailure = Array.isArray(hooks?.postToolUseFailure) ? hooks.postToolUseFailure : [];
637
+ const hooks = cfg.hooks ?? {};
638
+ const newHooks = { ...hooks };
639
+ pruneFlatEvent(newHooks, "userPromptSubmitted");
640
+ pruneFlatEvent(newHooks, "postToolUseFailure");
641
+ if (events.includes("sessionStart")) newHooks.sessionStart = [...filterOutMembankFlat(Array.isArray(hooks.sessionStart) ? hooks.sessionStart : []), {
642
+ type: "command",
643
+ bash: "npx -y @membank/cli@latest inject --harness copilot-cli",
644
+ timeoutSec: 30
645
+ }];
708
646
  writeJsonAtomic(cfgPath, {
709
647
  version: cfg.version ?? 1,
710
648
  ...cfg,
711
- hooks: {
712
- ...hooks ?? {},
713
- sessionStart: [...filteredSessionStart, {
714
- type: "command",
715
- bash: "npx @membank/cli inject --harness copilot-cli",
716
- timeoutSec: 30
717
- }],
718
- userPromptSubmitted: [...filterOutMembankFlat(existingUserPrompt), {
719
- type: "command",
720
- bash: "npx @membank/cli inject --event user-prompt --harness copilot-cli",
721
- timeoutSec: 30
722
- }],
723
- postToolUseFailure: [...filterOutMembankFlat(existingToolFailure), {
724
- type: "command",
725
- bash: "npx @membank/cli inject --event tool-failure --harness copilot-cli",
726
- timeoutSec: 30
727
- }]
728
- }
649
+ hooks: newHooks
729
650
  });
730
651
  return { status: "written" };
731
652
  }
732
653
  },
733
654
  codex: {
734
- replacement: "npx @membank/cli inject --harness codex",
735
- write(resolver, overwrite = false) {
655
+ inspect(resolver) {
656
+ const hooks = readJson(join(resolver.home(), ".codex", "hooks.json")).hooks ?? {};
657
+ return {
658
+ status: "ready",
659
+ hooks: [{
660
+ event: "SessionStart",
661
+ command: "npx -y @membank/cli@latest inject --harness codex",
662
+ existingCommand: extractInjectCommand((Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []).flatMap(getHooksArray)) || null
663
+ }]
664
+ };
665
+ },
666
+ write(resolver, events) {
736
667
  const cfgPath = join(resolver.home(), ".codex", "hooks.json");
737
668
  const cfg = readJson(cfgPath);
738
- const hooks = cfg.hooks;
739
- const existingSessionStart = Array.isArray(hooks?.SessionStart) ? hooks.SessionStart : [];
740
- const innerHooks = existingSessionStart.flatMap(getHooksArray);
741
- if (!overwrite && containsMembankInject(innerHooks)) return {
742
- status: "already-configured",
743
- existing: extractInjectCommand(innerHooks),
744
- replacement: this.replacement
745
- };
746
- const filteredSessionStart = overwrite ? filterOutMembank(existingSessionStart) : existingSessionStart;
747
- const existingUserPrompt = Array.isArray(hooks?.UserPromptSubmit) ? hooks.UserPromptSubmit : [];
748
- const existingToolFailure = Array.isArray(hooks?.PostToolUse) ? hooks.PostToolUse : [];
669
+ const hooks = cfg.hooks ?? {};
670
+ const newHooks = { ...hooks };
671
+ pruneNestedEvent(newHooks, "UserPromptSubmit");
672
+ pruneNestedEvent(newHooks, "PostToolUse");
673
+ if (events.includes("SessionStart")) newHooks.SessionStart = [...filterOutMembank(Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []), {
674
+ matcher: "",
675
+ hooks: [{
676
+ type: "command",
677
+ command: "npx -y @membank/cli@latest inject --harness codex",
678
+ timeout: 30
679
+ }]
680
+ }];
749
681
  writeJsonAtomic(cfgPath, {
750
682
  ...cfg,
751
- hooks: {
752
- ...hooks ?? {},
753
- SessionStart: [...filteredSessionStart, {
754
- matcher: "",
755
- hooks: [{
756
- type: "command",
757
- command: "npx @membank/cli inject --harness codex",
758
- timeout: 30
759
- }]
760
- }],
761
- UserPromptSubmit: [...filterOutMembank(existingUserPrompt), {
762
- matcher: "",
763
- hooks: [{
764
- type: "command",
765
- command: "npx @membank/cli inject --event user-prompt --harness codex",
766
- timeout: 30
767
- }]
768
- }],
769
- PostToolUse: [...filterOutMembank(existingToolFailure), {
770
- matcher: "",
771
- hooks: [{
772
- type: "command",
773
- command: "npx @membank/cli inject --event tool-failure --harness codex",
774
- timeout: 30
775
- }]
776
- }]
777
- }
683
+ hooks: newHooks
778
684
  });
779
685
  return { status: "written" };
780
686
  }
781
687
  },
782
688
  opencode: {
783
- replacement: "npx @membank/cli inject",
784
- write(resolver, overwrite = false) {
689
+ inspect(resolver) {
785
690
  const pluginPath = join(resolver.home(), ".config", "opencode", "plugins", "membank.js");
786
- if (!overwrite && existsSync(pluginPath)) {
787
- const existing = readFileSync(pluginPath, "utf8");
788
- if (existing.includes("@membank/cli inject")) return {
789
- status: "already-configured",
790
- existing: existing.trim(),
791
- replacement: newOpencodePlugin()
792
- };
691
+ let existingCommand = null;
692
+ if (existsSync(pluginPath)) {
693
+ if (readFileSync(pluginPath, "utf8").includes("@membank/cli")) existingCommand = pluginPath;
793
694
  }
695
+ return {
696
+ status: "ready",
697
+ hooks: [{
698
+ event: "plugin",
699
+ command: pluginPath,
700
+ existingCommand
701
+ }]
702
+ };
703
+ },
704
+ write(resolver, events) {
705
+ if (events.length === 0) return { status: "written" };
706
+ const pluginPath = join(resolver.home(), ".config", "opencode", "plugins", "membank.js");
794
707
  mkdirSync(dirname(pluginPath), { recursive: true });
795
708
  writeFileSync(pluginPath, `${newOpencodePlugin()}\n`, "utf8");
796
709
  return { status: "written" };
@@ -802,19 +715,7 @@ function newOpencodePlugin() {
802
715
  "export default {",
803
716
  " hooks: {",
804
717
  " \"session.start\": async ({ $ }) => {",
805
- " return await $`npx @membank/cli inject`.text();",
806
- " },",
807
- " \"chat.message\": async ({ $, message }) => {",
808
- " const input = JSON.stringify({ prompt: message?.content ?? \"\" });",
809
- " return await $`npx @membank/cli inject --event user-prompt`.stdin(input).text();",
810
- " },",
811
- " \"tool.execute.after\": async ({ $, result }) => {",
812
- " if (!result?.exitCode && !result?.error) return;",
813
- " const payload = JSON.stringify({",
814
- " tool_name: result.tool ?? \"unknown\",",
815
- " error_message: result.error ?? (\"exit code \" + result.exitCode),",
816
- " });",
817
- " return await $`npx @membank/cli inject --event tool-failure`.stdin(payload).text();",
718
+ " return await $`npx -y @membank/cli@latest inject`.text();",
818
719
  " },",
819
720
  " },",
820
721
  "};"
@@ -825,10 +726,15 @@ var InjectionHookWriter = class {
825
726
  constructor(resolver = defaultPathResolver) {
826
727
  this.#resolver = resolver;
827
728
  }
828
- write(harness, overwrite) {
729
+ inspect(harness) {
730
+ const writer = writers[harness];
731
+ if (!writer) return { status: "not-supported" };
732
+ return writer.inspect(this.#resolver);
733
+ }
734
+ write(harness, events) {
829
735
  const writer = writers[harness];
830
736
  if (!writer) return { status: "not-supported" };
831
- return writer.write(this.#resolver, overwrite);
737
+ return writer.write(this.#resolver, events);
832
738
  }
833
739
  };
834
740
  //#endregion
@@ -1062,8 +968,7 @@ var SetupOrchestrator = class {
1062
968
  out("");
1063
969
  const injectionHooksConfigured = [];
1064
970
  if (this.#hookWriter) {
1065
- const w = this.#hookWriter;
1066
- injectionHooksConfigured.push(...await this.#runHookLoop(detected, "injection hook", (h, ow) => w.write(h, ow), yes, out));
971
+ injectionHooksConfigured.push(...await this.#runHookSetup(detected, yes, out));
1067
972
  out("");
1068
973
  }
1069
974
  let modelDownloaded = false;
@@ -1086,28 +991,33 @@ var SetupOrchestrator = class {
1086
991
  }
1087
992
  return results;
1088
993
  }
1089
- async #runHookLoop(detected, label, write, yes, out) {
994
+ async #runHookSetup(detected, yes, out) {
1090
995
  const configured = [];
996
+ const w = this.#hookWriter;
1091
997
  for (const h of detected) try {
1092
- const result = write(h.name);
1093
- if (result.status === "not-supported") continue;
1094
- if (result.status === "written") {
1095
- out(` ✓ ${h.name}: ${label} written`);
1096
- configured.push(h.name);
998
+ const inspected = w.inspect(h.name);
999
+ if (inspected.status === "not-supported") continue;
1000
+ const toWrite = [];
1001
+ for (const hook of inspected.hooks) if (hook.existingCommand === null) {
1002
+ out(` ${h.name}: ${hook.event} injection hook`);
1003
+ out(` Command: ${hook.command}`);
1004
+ if (yes || await this.#prompter(` Configure ${hook.event} injection hook for ${h.name}?`)) toWrite.push(hook.event);
1097
1005
  } else {
1098
- out(` ⚠ ${h.name}: ${label} already configured`);
1099
- out(` Current: ${result.existing}`);
1100
- out(` New: ${result.replacement}`);
1101
- if (yes || await this.#prompter(` Replace ${label} for ${h.name}?`)) {
1102
- if (write(h.name, true).status === "written") {
1103
- out(` ✓ ${h.name}: ${label} replaced`);
1104
- configured.push(h.name);
1105
- }
1106
- }
1006
+ out(` ⚠ ${h.name}: ${hook.event} injection hook already configured`);
1007
+ out(` Current: ${hook.existingCommand}`);
1008
+ if (hook.existingCommand !== hook.command) out(` New: ${hook.command}`);
1009
+ if (yes || await this.#prompter(` Replace ${hook.event} injection hook for ${h.name}?`)) toWrite.push(hook.event);
1010
+ }
1011
+ if (toWrite.length > 0) {
1012
+ w.write(h.name, toWrite);
1013
+ const skippedCount = inspected.hooks.length - toWrite.length;
1014
+ const label = skippedCount > 0 ? `${toWrite.length} injection hook(s) written, ${skippedCount} skipped` : `${toWrite.length} injection hook(s) written`;
1015
+ out(` ✓ ${h.name}: ${label}`);
1016
+ configured.push(h.name);
1107
1017
  }
1108
1018
  } catch (err) {
1109
1019
  const msg = err instanceof Error ? err.message : String(err);
1110
- out(` ✗ ${h.name} ${label}: ${msg}`);
1020
+ out(` ✗ ${h.name} injection hooks: ${msg}`);
1111
1021
  }
1112
1022
  return configured;
1113
1023
  }
@@ -1241,7 +1151,7 @@ program.command("import <file>").description("import memories from a JSON export
1241
1151
  db.close();
1242
1152
  }
1243
1153
  });
1244
- program.command("inject").description("output session context for harness injection (used by setup hooks)").option("--harness <name>", "format output for a specific harness (claude-code|copilot-cli|codex|opencode)").option("--scope <scope>", "project scope override (default: auto-detect from git remote)").option("--event <event>", "hook event type (session-start|user-prompt|tool-failure)", "session-start").action(async (cmdOptions) => {
1154
+ program.command("inject").description("output session context for harness injection (used by setup hooks)").option("--harness <name>", "format output for a specific harness (claude-code|copilot-cli|codex|opencode)").option("--scope <scope>", "project scope override (default: auto-detect from git remote)").option("--event <event>", "hook event type (only session-start is supported; other values no-op for legacy hook compatibility)", "session-start").action(async (cmdOptions) => {
1245
1155
  try {
1246
1156
  await injectCommand(cmdOptions);
1247
1157
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@membank/cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,9 +17,9 @@
17
17
  "@huggingface/transformers": "^4.2.0",
18
18
  "commander": "^14.0.3",
19
19
  "ora": "^9.4.0",
20
- "@membank/dashboard": "0.1.0",
21
- "@membank/mcp": "0.3.0",
22
- "@membank/core": "0.3.0"
20
+ "@membank/core": "0.4.1",
21
+ "@membank/dashboard": "0.2.1",
22
+ "@membank/mcp": "0.4.1"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@types/node": "^25.6.0",