@membank/cli 0.2.0 → 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 (2) hide show
  1. package/dist/index.mjs +356 -331
  2. package/package.json +4 -3
package/dist/index.mjs CHANGED
@@ -2,11 +2,12 @@
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
- import { homedir, tmpdir } from "node:os";
8
8
  import * as readline from "node:readline";
9
9
  import { createInterface } from "node:readline";
10
+ import { homedir, tmpdir } from "node:os";
10
11
  import { execFile } from "node:child_process";
11
12
  import { promisify } from "node:util";
12
13
  import { EventEmitter } from "node:events";
@@ -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
@@ -225,303 +316,6 @@ async function statsCommand(formatter) {
225
316
  }
226
317
  }
227
318
  //#endregion
228
- //#region src/setup/injection-hook-writer.ts
229
- const defaultPathResolver$1 = { home: () => {
230
- const h = process.env.HOME ?? process.env.USERPROFILE;
231
- if (!h) throw new Error("Cannot determine home directory");
232
- return h;
233
- } };
234
- function readJson$1(path) {
235
- try {
236
- return JSON.parse(readFileSync(path, "utf8"));
237
- } catch {
238
- return {};
239
- }
240
- }
241
- function writeJsonAtomic$1(path, data) {
242
- mkdirSync(dirname(path), { recursive: true });
243
- const tmp = join(mkdtempSync(join(tmpdir(), "membank-hook-")), "cfg.json");
244
- writeFileSync(tmp, JSON.stringify(data, null, 2));
245
- renameSync(tmp, path);
246
- }
247
- function getHooksArray(group) {
248
- if (typeof group !== "object" || group === null) return [];
249
- const h = group.hooks;
250
- return Array.isArray(h) ? h : [];
251
- }
252
- function findMembankHookCommand(hooks, pattern) {
253
- for (const h of hooks) {
254
- if (typeof h !== "object" || h === null) continue;
255
- if ("command" in h && typeof h.command === "string" && h.command.includes(pattern)) return h.command;
256
- if ("bash" in h && typeof h.bash === "string" && h.bash.includes(pattern)) return h.bash;
257
- }
258
- return "";
259
- }
260
- function containsMembankInject(hooks) {
261
- return findMembankHookCommand(hooks, "@membank/cli inject") !== "";
262
- }
263
- function extractInjectCommand(hooks) {
264
- return findMembankHookCommand(hooks, "@membank/cli inject");
265
- }
266
- const writers$1 = {
267
- "claude-code": {
268
- replacement: "npx @membank/cli inject --harness claude-code",
269
- write(resolver, overwrite = false) {
270
- const cfgPath = join(resolver.home(), ".claude", "settings.json");
271
- const cfg = readJson$1(cfgPath);
272
- const hooks = cfg.hooks;
273
- const existingGroups = Array.isArray(hooks?.SessionStart) ? hooks.SessionStart : [];
274
- const innerHooks = existingGroups.flatMap(getHooksArray);
275
- if (!overwrite && containsMembankInject(innerHooks)) return {
276
- status: "already-configured",
277
- existing: extractInjectCommand(innerHooks),
278
- replacement: this.replacement
279
- };
280
- const filteredGroups = overwrite ? existingGroups.filter((g) => !containsMembankInject(getHooksArray(g))) : existingGroups;
281
- writeJsonAtomic$1(cfgPath, {
282
- ...cfg,
283
- hooks: {
284
- ...hooks ?? {},
285
- SessionStart: [...filteredGroups, {
286
- matcher: "",
287
- hooks: [{
288
- type: "command",
289
- command: this.replacement
290
- }]
291
- }]
292
- }
293
- });
294
- return { status: "written" };
295
- }
296
- },
297
- "copilot-cli": {
298
- replacement: "npx @membank/cli inject --harness copilot-cli",
299
- write(resolver, overwrite = false) {
300
- const cfgPath = join(resolver.home(), ".copilot", "settings.json");
301
- const cfg = readJson$1(cfgPath);
302
- const hooks = cfg.hooks;
303
- const existingHooks = Array.isArray(hooks?.sessionStart) ? hooks.sessionStart : [];
304
- if (!overwrite && containsMembankInject(existingHooks)) return {
305
- status: "already-configured",
306
- existing: extractInjectCommand(existingHooks),
307
- replacement: this.replacement
308
- };
309
- const filteredHooks = overwrite ? existingHooks.filter((h) => !containsMembankInject([h])) : existingHooks;
310
- writeJsonAtomic$1(cfgPath, {
311
- version: cfg.version ?? 1,
312
- ...cfg,
313
- hooks: {
314
- ...hooks ?? {},
315
- sessionStart: [...filteredHooks, {
316
- type: "command",
317
- bash: this.replacement,
318
- timeoutSec: 30
319
- }]
320
- }
321
- });
322
- return { status: "written" };
323
- }
324
- },
325
- codex: {
326
- replacement: "npx @membank/cli inject --harness codex",
327
- write(resolver, overwrite = false) {
328
- const cfgPath = join(resolver.home(), ".codex", "hooks.json");
329
- const cfg = readJson$1(cfgPath);
330
- const hooks = cfg.hooks;
331
- const existingGroups = Array.isArray(hooks?.SessionStart) ? hooks.SessionStart : [];
332
- const innerHooks = existingGroups.flatMap(getHooksArray);
333
- if (!overwrite && containsMembankInject(innerHooks)) return {
334
- status: "already-configured",
335
- existing: extractInjectCommand(innerHooks),
336
- replacement: this.replacement
337
- };
338
- const filteredGroups = overwrite ? existingGroups.filter((g) => !containsMembankInject(getHooksArray(g))) : existingGroups;
339
- writeJsonAtomic$1(cfgPath, {
340
- ...cfg,
341
- hooks: {
342
- ...hooks ?? {},
343
- SessionStart: [...filteredGroups, {
344
- matcher: "",
345
- hooks: [{
346
- type: "command",
347
- command: this.replacement,
348
- timeout: 30
349
- }]
350
- }]
351
- }
352
- });
353
- return { status: "written" };
354
- }
355
- },
356
- opencode: {
357
- replacement: "npx @membank/cli inject",
358
- write(resolver, overwrite = false) {
359
- const pluginPath = join(resolver.home(), ".config", "opencode", "plugins", "membank.js");
360
- if (!overwrite && existsSync(pluginPath)) {
361
- const existing = readFileSync(pluginPath, "utf8");
362
- if (existing.includes("@membank/cli inject")) return {
363
- status: "already-configured",
364
- existing: existing.trim(),
365
- replacement: newOpencodePlugin()
366
- };
367
- }
368
- mkdirSync(dirname(pluginPath), { recursive: true });
369
- writeFileSync(pluginPath, `${newOpencodePlugin()}\n`, "utf8");
370
- return { status: "written" };
371
- }
372
- }
373
- };
374
- function newOpencodePlugin(includeIdle = false) {
375
- return [
376
- "export default {",
377
- " hooks: {",
378
- " \"session.start\": async ({ $ }) => {",
379
- " return await $`npx @membank/cli inject`.text();",
380
- " },",
381
- ...includeIdle ? [
382
- " \"session.idle\": async ({ $ }) => {",
383
- " return await $`npx @membank/cli stop-hook --harness opencode`.text();",
384
- " },"
385
- ] : [],
386
- " },",
387
- "};"
388
- ].join("\n");
389
- }
390
- const STOP_HOOK_PROMPT = "Review this session and consider whether any user preferences, corrections, decisions, or learnings are worth saving for future sessions. If so, use the save_memory MCP tool to store them. Be selective — only save what would genuinely help in a future conversation. Skip ephemeral task details.";
391
- function containsMembankStopHookCmd(hooks) {
392
- return findMembankHookCommand(hooks, "@membank/cli stop-hook") !== "";
393
- }
394
- function extractStopHookCmd(hooks) {
395
- return findMembankHookCommand(hooks, "@membank/cli stop-hook");
396
- }
397
- function containsMembankStopPrompt(stopGroups) {
398
- return stopGroups.some((g) => getHooksArray(g).some((h) => typeof h === "object" && h !== null && "type" in h && h.type === "prompt" && "prompt" in h && typeof h.prompt === "string" && h.prompt.includes("save_memory")));
399
- }
400
- function extractStopPrompt(stopGroups) {
401
- for (const g of stopGroups) for (const h of getHooksArray(g)) if (typeof h === "object" && h !== null && "type" in h && h.type === "prompt" && "prompt" in h && typeof h.prompt === "string" && h.prompt.includes("save_memory")) return h.prompt;
402
- return "";
403
- }
404
- const stopHookWriters = {
405
- "claude-code": { write(resolver, overwrite = false) {
406
- const cfgPath = join(resolver.home(), ".claude", "settings.json");
407
- const cfg = readJson$1(cfgPath);
408
- const hooks = cfg.hooks;
409
- const existingStop = Array.isArray(hooks?.Stop) ? hooks.Stop : [];
410
- if (!overwrite && containsMembankStopPrompt(existingStop)) return {
411
- status: "already-configured",
412
- existing: extractStopPrompt(existingStop),
413
- replacement: STOP_HOOK_PROMPT
414
- };
415
- const filteredStop = overwrite ? existingStop.filter((g) => !containsMembankStopPrompt([g])) : existingStop;
416
- writeJsonAtomic$1(cfgPath, {
417
- ...cfg,
418
- hooks: {
419
- ...hooks ?? {},
420
- Stop: [...filteredStop, { hooks: [{
421
- type: "prompt",
422
- prompt: STOP_HOOK_PROMPT
423
- }] }]
424
- }
425
- });
426
- return { status: "written" };
427
- } },
428
- "copilot-cli": { write(resolver, overwrite = false) {
429
- const cfgPath = join(resolver.home(), ".copilot", "settings.json");
430
- const cfg = readJson$1(cfgPath);
431
- const replacement = "npx @membank/cli stop-hook --harness copilot-cli";
432
- const hooks = cfg.hooks;
433
- const existingStop = Array.isArray(hooks?.stop) ? hooks.stop : [];
434
- if (!overwrite && containsMembankStopHookCmd(existingStop)) return {
435
- status: "already-configured",
436
- existing: extractStopHookCmd(existingStop),
437
- replacement
438
- };
439
- const filteredStop = overwrite ? existingStop.filter((h) => !containsMembankStopHookCmd([h])) : existingStop;
440
- writeJsonAtomic$1(cfgPath, {
441
- version: cfg.version ?? 1,
442
- ...cfg,
443
- hooks: {
444
- ...hooks ?? {},
445
- stop: [...filteredStop, {
446
- type: "command",
447
- bash: replacement,
448
- timeoutSec: 30
449
- }]
450
- }
451
- });
452
- return { status: "written" };
453
- } },
454
- codex: { write(resolver, overwrite = false) {
455
- const cfgPath = join(resolver.home(), ".codex", "hooks.json");
456
- const cfg = readJson$1(cfgPath);
457
- const replacement = "npx @membank/cli stop-hook --harness codex";
458
- const hooks = cfg.hooks;
459
- const existingGroups = Array.isArray(hooks?.Stop) ? hooks.Stop : [];
460
- const innerHooks = existingGroups.flatMap(getHooksArray);
461
- if (!overwrite && containsMembankStopHookCmd(innerHooks)) return {
462
- status: "already-configured",
463
- existing: extractStopHookCmd(innerHooks),
464
- replacement
465
- };
466
- const filteredGroups = overwrite ? existingGroups.filter((g) => !containsMembankStopHookCmd(getHooksArray(g))) : existingGroups;
467
- writeJsonAtomic$1(cfgPath, {
468
- ...cfg,
469
- hooks: {
470
- ...hooks ?? {},
471
- Stop: [...filteredGroups, {
472
- matcher: "",
473
- hooks: [{
474
- type: "command",
475
- command: replacement,
476
- timeout: 30
477
- }]
478
- }]
479
- }
480
- });
481
- return { status: "written" };
482
- } },
483
- opencode: { write(resolver, overwrite = false) {
484
- const pluginPath = join(resolver.home(), ".config", "opencode", "plugins", "membank.js");
485
- if (!overwrite && existsSync(pluginPath)) {
486
- const existing = readFileSync(pluginPath, "utf8");
487
- if (existing.includes("@membank/cli stop-hook")) return {
488
- status: "already-configured",
489
- existing: existing.trim(),
490
- replacement: newOpencodePlugin(true)
491
- };
492
- }
493
- mkdirSync(dirname(pluginPath), { recursive: true });
494
- writeFileSync(pluginPath, `${newOpencodePlugin(true)}\n`, "utf8");
495
- return { status: "written" };
496
- } }
497
- };
498
- var InjectionHookWriter = class {
499
- #resolver;
500
- constructor(resolver = defaultPathResolver$1) {
501
- this.#resolver = resolver;
502
- }
503
- write(harness, overwrite) {
504
- const writer = writers$1[harness];
505
- if (!writer) return { status: "not-supported" };
506
- return writer.write(this.#resolver, overwrite);
507
- }
508
- writeStopHook(harness, overwrite) {
509
- const writer = stopHookWriters[harness];
510
- if (!writer) return { status: "not-supported" };
511
- return writer.write(this.#resolver, overwrite);
512
- }
513
- };
514
- //#endregion
515
- //#region src/commands/stop-hook.ts
516
- function stopHookCommand(opts) {
517
- const { harness } = opts;
518
- if (harness === "copilot-cli" || harness === "codex") {
519
- process.stdout.write(JSON.stringify({ systemMessage: STOP_HOOK_PROMPT }));
520
- return;
521
- }
522
- process.stdout.write(`${STOP_HOOK_PROMPT}\n`);
523
- }
524
- //#endregion
525
319
  //#region src/commands/unpin.ts
526
320
  function unpinCommand(id, formatter, db) {
527
321
  const ownDb = db === void 0;
@@ -668,7 +462,7 @@ async function execFileNoThrow(cmd, args) {
668
462
  }
669
463
  //#endregion
670
464
  //#region src/setup/harness-config-writer.ts
671
- const defaultPathResolver = {
465
+ const defaultPathResolver$1 = {
672
466
  home: () => {
673
467
  const h = process.env.HOME ?? process.env.USERPROFILE;
674
468
  if (!h) throw new Error("Cannot determine home directory");
@@ -676,14 +470,14 @@ const defaultPathResolver = {
676
470
  },
677
471
  cwd: () => process.cwd()
678
472
  };
679
- function readJson(path) {
473
+ function readJson$1(path) {
680
474
  try {
681
475
  return JSON.parse(readFileSync(path, "utf8"));
682
476
  } catch {
683
477
  return {};
684
478
  }
685
479
  }
686
- function writeJsonAtomic(path, data) {
480
+ function writeJsonAtomic$1(path, data) {
687
481
  mkdirSync(dirname(path), { recursive: true });
688
482
  const tmp = join(mkdtempSync(join(tmpdir(), "membank-")), "cfg.json");
689
483
  writeFileSync(tmp, JSON.stringify(data, null, 2));
@@ -700,9 +494,9 @@ const MEMBANK_NPX_ARGS = [
700
494
  "@membank/cli@latest",
701
495
  "--mcp"
702
496
  ];
703
- const writers = {
497
+ const writers$1 = {
704
498
  "claude-code": { async write(resolver, run, { overwrite = false } = {}) {
705
- const configured = hasKey(readJson(join(resolver.home(), ".claude.json")).mcpServers, "membank");
499
+ const configured = hasKey(readJson$1(join(resolver.home(), ".claude.json")).mcpServers, "membank");
706
500
  if (configured && !overwrite) return { status: "already-configured" };
707
501
  if (configured) {
708
502
  const remove = await run("claude", [
@@ -730,9 +524,9 @@ const writers = {
730
524
  } },
731
525
  copilot: { async write(resolver, _run, { overwrite = false } = {}) {
732
526
  const cfgPath = join(resolver.home(), ".copilot", "mcp-config.json");
733
- const cfg = readJson(cfgPath);
527
+ const cfg = readJson$1(cfgPath);
734
528
  if (hasKey(cfg.mcpServers, "membank") && !overwrite) return { status: "already-configured" };
735
- writeJsonAtomic(cfgPath, {
529
+ writeJsonAtomic$1(cfgPath, {
736
530
  ...cfg,
737
531
  mcpServers: {
738
532
  ...cfg.mcpServers,
@@ -771,9 +565,9 @@ const writers = {
771
565
  } },
772
566
  opencode: { async write(resolver, _run, { overwrite = false } = {}) {
773
567
  const cfgPath = join(resolver.home(), ".config", "opencode", "opencode.json");
774
- const cfg = readJson(cfgPath);
568
+ const cfg = readJson$1(cfgPath);
775
569
  if (hasKey(cfg.mcp, "membank") && !overwrite) return { status: "already-configured" };
776
- writeJsonAtomic(cfgPath, {
570
+ writeJsonAtomic$1(cfgPath, {
777
571
  ...cfg,
778
572
  mcp: {
779
573
  ...cfg.mcp,
@@ -790,21 +584,254 @@ const writers = {
790
584
  return { status: "written" };
791
585
  } }
792
586
  };
793
- const SUPPORTED_HARNESSES = Object.keys(writers);
587
+ const SUPPORTED_HARNESSES = Object.keys(writers$1);
794
588
  var HarnessConfigWriter = class {
795
589
  #resolver;
796
590
  #run;
797
- constructor(resolver = defaultPathResolver, run = execFileNoThrow) {
591
+ constructor(resolver = defaultPathResolver$1, run = execFileNoThrow) {
798
592
  this.#resolver = resolver;
799
593
  this.#run = run;
800
594
  }
801
595
  async write(harness, { overwrite = false } = {}) {
802
- const writer = writers[harness];
596
+ const writer = writers$1[harness];
803
597
  if (!writer) throw new Error(`Unknown harness: ${harness}`);
804
598
  return writer.write(this.#resolver, this.#run, { overwrite });
805
599
  }
806
600
  };
807
601
  //#endregion
602
+ //#region src/setup/injection-hook-writer.ts
603
+ const defaultPathResolver = { home: () => {
604
+ const h = process.env.HOME ?? process.env.USERPROFILE;
605
+ if (!h) throw new Error("Cannot determine home directory");
606
+ return h;
607
+ } };
608
+ function readJson(path) {
609
+ try {
610
+ return JSON.parse(readFileSync(path, "utf8"));
611
+ } catch {
612
+ return {};
613
+ }
614
+ }
615
+ function writeJsonAtomic(path, data) {
616
+ mkdirSync(dirname(path), { recursive: true });
617
+ const tmp = join(mkdtempSync(join(tmpdir(), "membank-hook-")), "cfg.json");
618
+ writeFileSync(tmp, JSON.stringify(data, null, 2));
619
+ renameSync(tmp, path);
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
+ }
634
+ function containsMembankInject(hooks) {
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]));
645
+ }
646
+ const writers = {
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
+ }]
687
+ }]
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), {
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
+ }
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
+ }]
776
+ }]
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
+ };
793
+ }
794
+ mkdirSync(dirname(pluginPath), { recursive: true });
795
+ writeFileSync(pluginPath, `${newOpencodePlugin()}\n`, "utf8");
796
+ return { status: "written" };
797
+ }
798
+ }
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
+ }
823
+ var InjectionHookWriter = class {
824
+ #resolver;
825
+ constructor(resolver = defaultPathResolver) {
826
+ this.#resolver = resolver;
827
+ }
828
+ write(harness, overwrite) {
829
+ const writer = writers[harness];
830
+ if (!writer) return { status: "not-supported" };
831
+ return writer.write(this.#resolver, overwrite);
832
+ }
833
+ };
834
+ //#endregion
808
835
  //#region src/setup/model-downloader.ts
809
836
  const MODEL_NAME = "Xenova/bge-small-en-v1.5";
810
837
  var ModelDownloadError = class extends Error {
@@ -954,7 +981,6 @@ var SetupOrchestrator = class {
954
981
  detectedHarnesses: [],
955
982
  configuredHarnesses: [],
956
983
  injectionHooksConfigured: [],
957
- stopHooksConfigured: [],
958
984
  modelDownloaded: false
959
985
  }));
960
986
  return [];
@@ -968,10 +994,7 @@ var SetupOrchestrator = class {
968
994
  out("Planned changes (dry-run — no files written):");
969
995
  for (const h of detected) {
970
996
  out(` ⚠ ${h.name}: would write MCP config`);
971
- if (this.#hookWriter) {
972
- out(` ⚠ ${h.name}: would write injection hook config`);
973
- out(` ⚠ ${h.name}: would write stop hook config`);
974
- }
997
+ if (this.#hookWriter) out(` ⚠ ${h.name}: would write injection hook config`);
975
998
  }
976
999
  out("");
977
1000
  out(" ⚠ Model download: skipped (dry-run)");
@@ -1038,11 +1061,9 @@ var SetupOrchestrator = class {
1038
1061
  }
1039
1062
  out("");
1040
1063
  const injectionHooksConfigured = [];
1041
- const stopHooksConfigured = [];
1042
1064
  if (this.#hookWriter) {
1043
1065
  const w = this.#hookWriter;
1044
1066
  injectionHooksConfigured.push(...await this.#runHookLoop(detected, "injection hook", (h, ow) => w.write(h, ow), yes, out));
1045
- stopHooksConfigured.push(...await this.#runHookLoop(detected, "stop hook", (h, ow) => w.writeStopHook(h, ow), yes, out));
1046
1067
  out("");
1047
1068
  }
1048
1069
  let modelDownloaded = false;
@@ -1056,7 +1077,6 @@ var SetupOrchestrator = class {
1056
1077
  detectedHarnesses: detected.map((h) => h.name),
1057
1078
  configuredHarnesses: results.filter((r) => r.status === "written").map((r) => r.harness),
1058
1079
  injectionHooksConfigured,
1059
- stopHooksConfigured,
1060
1080
  modelDownloaded
1061
1081
  };
1062
1082
  this.#out(JSON.stringify(output));
@@ -1221,7 +1241,7 @@ program.command("import <file>").description("import memories from a JSON export
1221
1241
  db.close();
1222
1242
  }
1223
1243
  });
1224
- 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) => {
1225
1245
  try {
1226
1246
  await injectCommand(cmdOptions);
1227
1247
  } catch (err) {
@@ -1229,9 +1249,6 @@ program.command("inject").description("output session context for harness inject
1229
1249
  process.exit(2);
1230
1250
  }
1231
1251
  });
1232
- program.command("stop-hook").description("output session-end prompt for harness stop hooks (called by hooks, not users)").option("--harness <name>", "harness name (copilot-cli, codex, opencode)").action((cmdOptions) => {
1233
- stopHookCommand(cmdOptions);
1234
- });
1235
1252
  program.command("setup").description("detect installed harnesses and write MCP config for each").option("--yes", "skip all confirmation prompts").option("--dry-run", "print planned changes without writing any file").option("--harness <name>", "target only the named harness (skip detection)").action(async (cmdOptions) => {
1236
1253
  const globalOpts = program.opts();
1237
1254
  const autoYes = cmdOptions.yes === true || globalOpts.yes === true;
@@ -1263,6 +1280,14 @@ program.command("setup").description("detect installed harnesses and write MCP c
1263
1280
  process.exit(2);
1264
1281
  }
1265
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
+ });
1266
1291
  program.on("command:*", () => {
1267
1292
  program.outputHelp();
1268
1293
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@membank/cli",
3
- "version": "0.2.0",
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.2.0",
21
- "@membank/mcp": "0.2.0"
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",