@membank/cli 0.1.1 → 0.3.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 (3) hide show
  1. package/README.md +154 -0
  2. package/dist/index.mjs +331 -104
  3. package/package.json +4 -3
package/README.md ADDED
@@ -0,0 +1,154 @@
1
+ # @membank/cli
2
+
3
+ CLI and npx entrypoint for membank. Manages memories from the terminal and starts the MCP server for LLM harnesses.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @membank/cli
9
+ ```
10
+
11
+ Or use without installing:
12
+
13
+ ```bash
14
+ npx @membank/cli <command>
15
+ ```
16
+
17
+ ## Setup
18
+
19
+ Run once to configure your LLM harness:
20
+
21
+ ```bash
22
+ membank setup
23
+ ```
24
+
25
+ This auto-detects installed harnesses (Claude Code, GitHub Copilot CLI, Codex, OpenCode), writes MCP server config, installs session hooks, and downloads the embedding model (~33 MB).
26
+
27
+ Options:
28
+
29
+ ```
30
+ --harness <name> Target a specific harness instead of auto-detecting
31
+ --yes Skip confirmation prompts
32
+ --dry-run Preview changes without writing files
33
+ --json Machine-readable output
34
+ ```
35
+
36
+ Supported harnesses: `claude-code`, `copilot`, `codex`, `opencode`
37
+
38
+ ## Commands
39
+
40
+ ### `membank query <text>`
41
+
42
+ Semantic search over stored memories.
43
+
44
+ ```bash
45
+ membank query "how to run pnpm in one package"
46
+ membank query "auth decisions" --type decision --limit 5
47
+ ```
48
+
49
+ Options: `--type <type>`, `--limit <n>` (default 10)
50
+
51
+ ### `membank add <content>`
52
+
53
+ Save a new memory.
54
+
55
+ ```bash
56
+ membank add "Use --filter flag for scoped pnpm commands" --type preference --tags "pnpm,monorepo"
57
+ ```
58
+
59
+ Required: `--type <type>`
60
+ Options: `--tags <a,b,c>`, `--scope <scope>`
61
+
62
+ ### `membank list`
63
+
64
+ List stored memories.
65
+
66
+ ```bash
67
+ membank list
68
+ membank list --type correction
69
+ membank list --pinned
70
+ ```
71
+
72
+ Options: `--type <type>`, `--pinned`
73
+
74
+ ### `membank stats`
75
+
76
+ Show memory counts by type.
77
+
78
+ ```bash
79
+ membank stats
80
+ ```
81
+
82
+ ### `membank pin <id>` / `membank unpin <id>`
83
+
84
+ Pin a memory so it's always injected at session start.
85
+
86
+ ```bash
87
+ membank pin abc123
88
+ membank unpin abc123
89
+ ```
90
+
91
+ ### `membank delete <id>`
92
+
93
+ Delete a memory. Prompts for confirmation unless `--yes` is passed.
94
+
95
+ ```bash
96
+ membank delete abc123
97
+ membank delete abc123 --yes
98
+ ```
99
+
100
+ ### `membank export`
101
+
102
+ Export all memories to a JSON file.
103
+
104
+ ```bash
105
+ membank export
106
+ membank export --output my-backup.json
107
+ ```
108
+
109
+ Default filename: `membank-export-<timestamp>.json`
110
+
111
+ ### `membank import <file>`
112
+
113
+ Import memories from an export file.
114
+
115
+ ```bash
116
+ membank import membank-export-2025-01-01.json
117
+ membank import membank-export-2025-01-01.json --yes
118
+ ```
119
+
120
+ ### `membank inject`
121
+
122
+ Output session context formatted for a harness. Called automatically by session hooks — you don't normally run this directly.
123
+
124
+ ```bash
125
+ membank inject --harness claude-code --scope <project-scope>
126
+ ```
127
+
128
+ ## Global flags
129
+
130
+ ```
131
+ --json Output machine-readable JSON
132
+ --yes, -y Skip confirmation prompts
133
+ --mcp Start MCP stdio server (used by harness config)
134
+ ```
135
+
136
+ ## MCP server mode
137
+
138
+ ```bash
139
+ membank --mcp
140
+ ```
141
+
142
+ Starts the stdio MCP server. This is what harnesses connect to — `setup` writes this command into harness configs automatically.
143
+
144
+ ## Session hooks
145
+
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.
149
+
150
+ **Session stop (Claude Code only)** — prompts the LLM to review the session and call `save_memory` for any notable corrections, preferences, or decisions.
151
+
152
+ ## Requirements
153
+
154
+ - Node.js >=24
package/dist/index.mjs CHANGED
@@ -2,6 +2,7 @@
2
2
  import { DatabaseManager, EmbeddingService, MEMORY_TYPE_VALUES, MemoryRepository, QueryEngine, SessionContextBuilder, resolveScope } from "@membank/core";
3
3
  import { startServer } from "@membank/mcp";
4
4
  import { Command } from "commander";
5
+ import { startDashboard } from "@membank/dashboard";
5
6
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
6
7
  import { dirname, join } from "node:path";
7
8
  import * as readline from "node:readline";
@@ -30,6 +31,11 @@ async function addCommand(content, options, formatter, db, embeddingService) {
30
31
  }
31
32
  }
32
33
  //#endregion
34
+ //#region src/commands/dashboard.ts
35
+ async function dashboardCommand(opts) {
36
+ await startDashboard({ port: opts.port !== void 0 ? parseInt(opts.port, 10) : void 0 });
37
+ }
38
+ //#endregion
33
39
  //#region src/commands/delete.ts
34
40
  async function deleteCommand(id, db, formatter, prompt) {
35
41
  if (db.db.prepare(`SELECT id FROM memories WHERE id = ?`).get(id) === void 0) {
@@ -140,7 +146,66 @@ function formatContext(ctx) {
140
146
  lines.push(MEMORY_GUIDANCE);
141
147
  return lines.join("\n");
142
148
  }
143
- async function injectCommand(opts) {
149
+ function outputAdditionalContext(text, harness, eventName) {
150
+ if (harness === "claude-code") {
151
+ process.stdout.write(JSON.stringify({ hookSpecificOutput: {
152
+ hookEventName: eventName,
153
+ additionalContext: text
154
+ } }));
155
+ return;
156
+ }
157
+ if (harness === "copilot-cli") {
158
+ process.stdout.write(JSON.stringify({ additionalContext: text }));
159
+ return;
160
+ }
161
+ process.stdout.write(`${text}\n`);
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
+ async function handleSessionStart(opts) {
144
209
  const projectScope = opts.scope ?? await resolveScope();
145
210
  const db = DatabaseManager.open();
146
211
  let text;
@@ -151,18 +216,44 @@ async function injectCommand(opts) {
151
216
  }
152
217
  if (!text) process.exit(0);
153
218
  const harness = opts.harness;
154
- if (harness === "claude-code") {
155
- process.stdout.write(JSON.stringify({ hookSpecificOutput: {
156
- hookEventName: "SessionStart",
157
- additionalContext: text
158
- } }));
219
+ outputAdditionalContext(text, harness, "SessionStart");
220
+ }
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
+ 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);
159
250
  return;
160
251
  }
161
- if (harness === "copilot-cli") {
162
- process.stdout.write(JSON.stringify({ additionalContext: text }));
252
+ if (event === "tool-failure") {
253
+ await handleToolFailure(harness);
163
254
  return;
164
255
  }
165
- process.stdout.write(`${text}\n`);
256
+ await handleSessionStart(opts);
166
257
  }
167
258
  //#endregion
168
259
  //#region src/commands/list.ts
@@ -527,105 +618,217 @@ function writeJsonAtomic(path, data) {
527
618
  writeFileSync(tmp, JSON.stringify(data, null, 2));
528
619
  renameSync(tmp, path);
529
620
  }
621
+ function getHooksArray(group) {
622
+ if (typeof group !== "object" || group === null) return [];
623
+ const h = group.hooks;
624
+ return Array.isArray(h) ? h : [];
625
+ }
626
+ function findMembankHookCommand(hooks, pattern) {
627
+ for (const h of hooks) {
628
+ if (typeof h !== "object" || h === null) continue;
629
+ if ("command" in h && typeof h.command === "string" && h.command.includes(pattern)) return h.command;
630
+ if ("bash" in h && typeof h.bash === "string" && h.bash.includes(pattern)) return h.bash;
631
+ }
632
+ return "";
633
+ }
530
634
  function containsMembankInject(hooks) {
531
- if (!Array.isArray(hooks)) return false;
532
- return hooks.some((h) => typeof h === "object" && h !== null && ("command" in h && typeof h.command === "string" && h.command.includes("@membank/cli inject") || "bash" in h && typeof h.bash === "string" && h.bash.includes("@membank/cli inject")));
635
+ return findMembankHookCommand(hooks, "@membank/cli inject") !== "";
636
+ }
637
+ function extractInjectCommand(hooks) {
638
+ return findMembankHookCommand(hooks, "@membank/cli inject");
639
+ }
640
+ function filterOutMembank(groups) {
641
+ return groups.filter((g) => !containsMembankInject(getHooksArray(g)));
642
+ }
643
+ function filterOutMembankFlat(hooks) {
644
+ return hooks.filter((h) => !containsMembankInject([h]));
533
645
  }
534
646
  const writers = {
535
- "claude-code": { write(resolver) {
536
- const cfgPath = join(resolver.home(), ".claude", "settings.json");
537
- const cfg = readJson(cfgPath);
538
- const hooks = cfg.hooks;
539
- const sessionStart = hooks?.SessionStart;
540
- if (Array.isArray(sessionStart) && containsMembankInject(sessionStart.flatMap((g) => Array.isArray(g.hooks) ? g.hooks : []))) return { status: "already-configured" };
541
- const existingSessionStart = Array.isArray(sessionStart) ? sessionStart : [];
542
- writeJsonAtomic(cfgPath, {
543
- ...cfg,
544
- hooks: {
545
- ...hooks ?? {},
546
- SessionStart: [...existingSessionStart, {
547
- matcher: "",
548
- hooks: [{
549
- type: "command",
550
- command: "npx @membank/cli inject --harness claude-code"
647
+ "claude-code": {
648
+ replacement: "npx @membank/cli inject --harness claude-code",
649
+ write(resolver, overwrite = false) {
650
+ const cfgPath = join(resolver.home(), ".claude", "settings.json");
651
+ 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 : [];
663
+ writeJsonAtomic(cfgPath, {
664
+ ...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
+ }]
551
687
  }]
552
- }]
553
- }
554
- });
555
- return { status: "written" };
556
- } },
557
- "copilot-cli": { write(resolver) {
558
- const cfgPath = join(resolver.home(), ".copilot", "settings.json");
559
- const cfg = readJson(cfgPath);
560
- const hooks = cfg.hooks;
561
- const sessionStart = hooks?.sessionStart;
562
- if (Array.isArray(sessionStart) && containsMembankInject(sessionStart)) return { status: "already-configured" };
563
- const existingSessionStart = Array.isArray(sessionStart) ? sessionStart : [];
564
- writeJsonAtomic(cfgPath, {
565
- version: cfg.version ?? 1,
566
- ...cfg,
567
- hooks: {
568
- ...hooks ?? {},
569
- sessionStart: [...existingSessionStart, {
570
- type: "command",
571
- bash: "npx @membank/cli inject --harness copilot-cli",
572
- timeoutSec: 30
573
- }]
574
- }
575
- });
576
- return { status: "written" };
577
- } },
578
- codex: { write(resolver) {
579
- const cfgPath = join(resolver.home(), ".codex", "hooks.json");
580
- const cfg = readJson(cfgPath);
581
- const hooks = cfg.hooks;
582
- const sessionStart = hooks?.SessionStart;
583
- if (Array.isArray(sessionStart) && containsMembankInject(sessionStart.flatMap((g) => Array.isArray(g.hooks) ? g.hooks : []))) return { status: "already-configured" };
584
- const existingSessionStart = Array.isArray(sessionStart) ? sessionStart : [];
585
- writeJsonAtomic(cfgPath, {
586
- ...cfg,
587
- hooks: {
588
- ...hooks ?? {},
589
- SessionStart: [...existingSessionStart, {
590
- matcher: "",
591
- hooks: [{
688
+ }
689
+ });
690
+ return { status: "written" };
691
+ }
692
+ },
693
+ "copilot-cli": {
694
+ replacement: "npx @membank/cli inject --harness copilot-cli",
695
+ write(resolver, overwrite = false) {
696
+ const cfgPath = join(resolver.home(), ".copilot", "settings.json");
697
+ 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 : [];
708
+ writeJsonAtomic(cfgPath, {
709
+ version: cfg.version ?? 1,
710
+ ...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), {
592
719
  type: "command",
593
- command: "npx @membank/cli inject --harness codex",
594
- timeout: 30
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
+ }
729
+ });
730
+ return { status: "written" };
731
+ }
732
+ },
733
+ codex: {
734
+ replacement: "npx @membank/cli inject --harness codex",
735
+ write(resolver, overwrite = false) {
736
+ const cfgPath = join(resolver.home(), ".codex", "hooks.json");
737
+ 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 : [];
749
+ writeJsonAtomic(cfgPath, {
750
+ ...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
+ }]
595
776
  }]
596
- }]
777
+ }
778
+ });
779
+ return { status: "written" };
780
+ }
781
+ },
782
+ opencode: {
783
+ replacement: "npx @membank/cli inject",
784
+ write(resolver, overwrite = false) {
785
+ 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
+ };
597
793
  }
598
- });
599
- return { status: "written" };
600
- } },
601
- opencode: { write(resolver) {
602
- const pluginPath = join(resolver.home(), ".config", "opencode", "plugins", "membank.js");
603
- if (existsSync(pluginPath)) {
604
- if (readFileSync(pluginPath, "utf8").includes("@membank/cli inject")) return { status: "already-configured" };
794
+ mkdirSync(dirname(pluginPath), { recursive: true });
795
+ writeFileSync(pluginPath, `${newOpencodePlugin()}\n`, "utf8");
796
+ return { status: "written" };
605
797
  }
606
- mkdirSync(dirname(pluginPath), { recursive: true });
607
- writeFileSync(pluginPath, [
608
- "export default {",
609
- " hooks: {",
610
- " \"session.start\": async ({ $ }) => {",
611
- " return await $`npx @membank/cli inject`.text();",
612
- " },",
613
- " },",
614
- "};",
615
- ""
616
- ].join("\n"), "utf8");
617
- return { status: "written" };
618
- } }
798
+ }
619
799
  };
800
+ function newOpencodePlugin() {
801
+ return [
802
+ "export default {",
803
+ " hooks: {",
804
+ " \"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();",
818
+ " },",
819
+ " },",
820
+ "};"
821
+ ].join("\n");
822
+ }
620
823
  var InjectionHookWriter = class {
621
824
  #resolver;
622
825
  constructor(resolver = defaultPathResolver) {
623
826
  this.#resolver = resolver;
624
827
  }
625
- write(harness) {
828
+ write(harness, overwrite) {
626
829
  const writer = writers[harness];
627
830
  if (!writer) return { status: "not-supported" };
628
- return writer.write(this.#resolver);
831
+ return writer.write(this.#resolver, overwrite);
629
832
  }
630
833
  };
631
834
  //#endregion
@@ -859,17 +1062,8 @@ var SetupOrchestrator = class {
859
1062
  out("");
860
1063
  const injectionHooksConfigured = [];
861
1064
  if (this.#hookWriter) {
862
- for (const h of detected) try {
863
- const hookResult = this.#hookWriter.write(h.name);
864
- if (hookResult.status === "not-supported") continue;
865
- if (hookResult.status === "written") {
866
- out(` ✓ ${h.name}: injection hook written`);
867
- injectionHooksConfigured.push(h.name);
868
- } else out(` ⚠ ${h.name}: injection hook already configured`);
869
- } catch (err) {
870
- const msg = err instanceof Error ? err.message : String(err);
871
- out(` ✗ ${h.name} injection hook: ${msg}`);
872
- }
1065
+ const w = this.#hookWriter;
1066
+ injectionHooksConfigured.push(...await this.#runHookLoop(detected, "injection hook", (h, ow) => w.write(h, ow), yes, out));
873
1067
  out("");
874
1068
  }
875
1069
  let modelDownloaded = false;
@@ -892,6 +1086,31 @@ var SetupOrchestrator = class {
892
1086
  }
893
1087
  return results;
894
1088
  }
1089
+ async #runHookLoop(detected, label, write, yes, out) {
1090
+ const configured = [];
1091
+ 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);
1097
+ } 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
+ }
1107
+ }
1108
+ } catch (err) {
1109
+ const msg = err instanceof Error ? err.message : String(err);
1110
+ out(` ✗ ${h.name} ${label}: ${msg}`);
1111
+ }
1112
+ return configured;
1113
+ }
895
1114
  async #runModelDownload(downloader, out) {
896
1115
  out("Downloading embedding model (bge-small-en-v1.5, ~33 MB)...");
897
1116
  downloader.on?.("progress", (p) => {
@@ -1022,7 +1241,7 @@ program.command("import <file>").description("import memories from a JSON export
1022
1241
  db.close();
1023
1242
  }
1024
1243
  });
1025
- 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)").action(async (cmdOptions) => {
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) => {
1026
1245
  try {
1027
1246
  await injectCommand(cmdOptions);
1028
1247
  } catch (err) {
@@ -1061,6 +1280,14 @@ program.command("setup").description("detect installed harnesses and write MCP c
1061
1280
  process.exit(2);
1062
1281
  }
1063
1282
  });
1283
+ program.command("dashboard").description("open the memory management dashboard in the browser").option("--port <port>", "port to listen on (default: 3847, fallback to random)").action(async (cmdOptions) => {
1284
+ try {
1285
+ await dashboardCommand(cmdOptions);
1286
+ } catch (err) {
1287
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
1288
+ process.exit(2);
1289
+ }
1290
+ });
1064
1291
  program.on("command:*", () => {
1065
1292
  program.outputHelp();
1066
1293
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@membank/cli",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,8 +17,9 @@
17
17
  "@huggingface/transformers": "^4.2.0",
18
18
  "commander": "^14.0.3",
19
19
  "ora": "^9.4.0",
20
- "@membank/core": "0.1.1",
21
- "@membank/mcp": "0.1.1"
20
+ "@membank/dashboard": "0.1.0",
21
+ "@membank/mcp": "0.3.0",
22
+ "@membank/core": "0.3.0"
22
23
  },
23
24
  "devDependencies": {
24
25
  "@types/node": "^25.6.0",