@membank/cli 0.0.3 → 0.1.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 +245 -95
  2. package/package.json +3 -3
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { DatabaseManager, EmbeddingService, MemoryRepository, QueryEngine } from "@membank/core";
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
5
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
@@ -72,13 +72,7 @@ function exportCommand(db, formatter, opts) {
72
72
  }
73
73
  //#endregion
74
74
  //#region src/commands/import.ts
75
- const MEMORY_TYPES = new Set([
76
- "correction",
77
- "preference",
78
- "decision",
79
- "learning",
80
- "fact"
81
- ]);
75
+ const MEMORY_TYPES = new Set(MEMORY_TYPE_VALUES);
82
76
  function isValidRecord(r) {
83
77
  if (typeof r !== "object" || r === null) return false;
84
78
  const rec = r;
@@ -133,6 +127,44 @@ async function importCommand(filePath, db, formatter, prompt) {
133
127
  else process.stdout.write(`Imported ${count} memories.\n`);
134
128
  }
135
129
  //#endregion
130
+ //#region src/commands/inject.ts
131
+ 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";
132
+ function formatContext(ctx) {
133
+ const lines = [];
134
+ const statParts = Object.entries(ctx.stats).filter(([, count]) => count > 0).map(([type, count]) => `${count} ${type}${count !== 1 ? "s" : ""}`);
135
+ if (statParts.length > 0) lines.push(`[Memory Stats]: ${statParts.join(", ")}`);
136
+ else lines.push("[Memory Stats]: no memories saved yet");
137
+ const formatMemory = (m) => `"${m.content}" (${m.type})`;
138
+ for (const m of ctx.pinnedGlobal) lines.push(`[Pinned Global]: ${formatMemory(m)}`);
139
+ for (const m of ctx.pinnedProject) lines.push(`[Pinned Project]: ${formatMemory(m)}`);
140
+ lines.push(MEMORY_GUIDANCE);
141
+ return lines.join("\n");
142
+ }
143
+ async function injectCommand(opts) {
144
+ const projectScope = opts.scope ?? await resolveScope();
145
+ const db = DatabaseManager.open();
146
+ let text;
147
+ try {
148
+ text = formatContext(new SessionContextBuilder(db).getSessionContext(projectScope));
149
+ } finally {
150
+ db.close();
151
+ }
152
+ if (!text) process.exit(0);
153
+ const harness = opts.harness;
154
+ if (harness === "claude-code") {
155
+ process.stdout.write(JSON.stringify({ hookSpecificOutput: {
156
+ hookEventName: "SessionStart",
157
+ additionalContext: text
158
+ } }));
159
+ return;
160
+ }
161
+ if (harness === "copilot-cli") {
162
+ process.stdout.write(JSON.stringify({ additionalContext: text }));
163
+ return;
164
+ }
165
+ process.stdout.write(`${text}\n`);
166
+ }
167
+ //#endregion
136
168
  //#region src/commands/list.ts
137
169
  async function listCommand(options, formatter) {
138
170
  const db = DatabaseManager.open();
@@ -216,11 +248,8 @@ var Formatter = class Formatter {
216
248
  constructor(isJson) {
217
249
  this.#isJson = isJson;
218
250
  }
219
- static create() {
220
- return new Formatter(!process.stdout.isTTY);
221
- }
222
- withJson(isJson) {
223
- return new Formatter(isJson);
251
+ static create(forceJson = false) {
252
+ return new Formatter(forceJson || !process.stdout.isTTY);
224
253
  }
225
254
  get isJson() {
226
255
  return this.#isJson;
@@ -230,13 +259,7 @@ var Formatter = class Formatter {
230
259
  process.stdout.write(`${JSON.stringify(memory)}\n`);
231
260
  return;
232
261
  }
233
- const tags = memory.tags.length > 0 ? memory.tags.join(", ") : "(none)";
234
- process.stdout.write(`\n[${memory.type}] ${memory.id}\n`);
235
- process.stdout.write(` Content : ${memory.content}\n`);
236
- process.stdout.write(` Tags : ${tags}\n`);
237
- process.stdout.write(` Scope : ${memory.scope}\n`);
238
- process.stdout.write(` Pinned : ${memory.pinned}\n`);
239
- process.stdout.write("\n");
262
+ this.#writeMemoryBlock(memory, ` Pinned : ${memory.pinned}\n`);
240
263
  }
241
264
  outputMemories(memories) {
242
265
  if (this.#isJson) {
@@ -247,13 +270,7 @@ var Formatter = class Formatter {
247
270
  process.stdout.write("No memories found.\n");
248
271
  return;
249
272
  }
250
- for (const memory of memories) {
251
- const tags = memory.tags.length > 0 ? memory.tags.join(", ") : "(none)";
252
- process.stdout.write(`\n[${memory.type}] ${memory.id}\n`);
253
- process.stdout.write(` Content : ${memory.content}\n`);
254
- process.stdout.write(` Tags : ${tags}\n`);
255
- process.stdout.write(` Scope : ${memory.scope}\n`);
256
- }
273
+ for (const memory of memories) this.#writeMemoryBlock(memory);
257
274
  process.stdout.write("\n");
258
275
  }
259
276
  outputStats(stats) {
@@ -280,20 +297,22 @@ var Formatter = class Formatter {
280
297
  process.stdout.write("No memories found.\n");
281
298
  return;
282
299
  }
283
- for (const result of results) {
284
- const tags = result.tags.length > 0 ? result.tags.join(", ") : "(none)";
285
- process.stdout.write(`\n[${result.type}] ${result.id}\n`);
286
- process.stdout.write(` Content : ${result.content}\n`);
287
- process.stdout.write(` Tags : ${tags}\n`);
288
- process.stdout.write(` Scope : ${result.scope}\n`);
289
- process.stdout.write(` Score : ${result.score.toFixed(4)}\n`);
290
- }
300
+ for (const result of results) this.#writeMemoryBlock(result, ` Score : ${result.score.toFixed(4)}\n`);
291
301
  process.stdout.write("\n");
292
302
  }
293
303
  error(msg) {
294
304
  if (this.#isJson) process.stderr.write(`${JSON.stringify({ error: msg })}\n`);
295
305
  else process.stderr.write(`Error: ${msg}\n`);
296
306
  }
307
+ #writeMemoryBlock(memory, extra) {
308
+ const tags = memory.tags.length > 0 ? memory.tags.join(", ") : "(none)";
309
+ process.stdout.write(`\n[${memory.type}] ${memory.id}\n`);
310
+ process.stdout.write(` Content : ${memory.content}\n`);
311
+ process.stdout.write(` Tags : ${tags}\n`);
312
+ process.stdout.write(` Scope : ${memory.scope}\n`);
313
+ if (extra !== void 0) process.stdout.write(extra);
314
+ process.stdout.write("\n");
315
+ }
297
316
  };
298
317
  //#endregion
299
318
  //#region src/prompt-helper.ts
@@ -352,7 +371,7 @@ async function execFileNoThrow(cmd, args) {
352
371
  }
353
372
  //#endregion
354
373
  //#region src/setup/harness-config-writer.ts
355
- const defaultPathResolver = {
374
+ const defaultPathResolver$1 = {
356
375
  home: () => {
357
376
  const h = process.env.HOME ?? process.env.USERPROFILE;
358
377
  if (!h) throw new Error("Cannot determine home directory");
@@ -360,14 +379,14 @@ const defaultPathResolver = {
360
379
  },
361
380
  cwd: () => process.cwd()
362
381
  };
363
- function readJson(path) {
382
+ function readJson$1(path) {
364
383
  try {
365
384
  return JSON.parse(readFileSync(path, "utf8"));
366
385
  } catch {
367
386
  return {};
368
387
  }
369
388
  }
370
- function writeJsonAtomic(path, data) {
389
+ function writeJsonAtomic$1(path, data) {
371
390
  mkdirSync(dirname(path), { recursive: true });
372
391
  const tmp = join(mkdtempSync(join(tmpdir(), "membank-")), "cfg.json");
373
392
  writeFileSync(tmp, JSON.stringify(data, null, 2));
@@ -384,9 +403,9 @@ const MEMBANK_NPX_ARGS = [
384
403
  "@membank/cli@latest",
385
404
  "--mcp"
386
405
  ];
387
- const writers = {
406
+ const writers$1 = {
388
407
  "claude-code": { async write(resolver, run, { overwrite = false } = {}) {
389
- const configured = hasKey(readJson(join(resolver.home(), ".claude.json")).mcpServers, "membank");
408
+ const configured = hasKey(readJson$1(join(resolver.home(), ".claude.json")).mcpServers, "membank");
390
409
  if (configured && !overwrite) return { status: "already-configured" };
391
410
  if (configured) {
392
411
  const remove = await run("claude", [
@@ -412,37 +431,20 @@ const writers = {
412
431
  if (add.exitCode !== 0) throw new Error(`claude mcp add failed: ${add.stderr || add.stdout}`);
413
432
  return { status: "written" };
414
433
  } },
415
- vscode: { async write(resolver, run, { overwrite = false } = {}) {
416
- const cfgPath = join(resolver.cwd(), ".vscode", "mcp.json");
417
- const cfg = readJson(cfgPath);
418
- const configured = hasKey(cfg.servers, "membank");
419
- if (configured && !overwrite) return { status: "already-configured" };
420
- if (configured) {
421
- writeJsonAtomic(cfgPath, {
422
- ...cfg,
423
- servers: {
424
- ...cfg.servers,
425
- membank: {
426
- command: "npx",
427
- args: ["@membank/cli@latest", "--mcp"]
428
- }
434
+ copilot: { async write(resolver, _run, { overwrite = false } = {}) {
435
+ const cfgPath = join(resolver.home(), ".copilot", "mcp-config.json");
436
+ const cfg = readJson$1(cfgPath);
437
+ if (hasKey(cfg.mcpServers, "membank") && !overwrite) return { status: "already-configured" };
438
+ writeJsonAtomic$1(cfgPath, {
439
+ ...cfg,
440
+ mcpServers: {
441
+ ...cfg.mcpServers,
442
+ membank: {
443
+ command: "npx",
444
+ args: ["@membank/cli@latest", "--mcp"]
429
445
  }
430
- });
431
- return { status: "written" };
432
- }
433
- const payload = JSON.stringify({
434
- name: "membank",
435
- command: "npx",
436
- args: ["@membank/cli@latest", "--mcp"]
446
+ }
437
447
  });
438
- const result = await run("code", [
439
- "--folder-uri",
440
- resolver.cwd(),
441
- "--add-mcp",
442
- payload
443
- ]);
444
- assertCliFound(result, "code");
445
- if (result.exitCode !== 0) throw new Error(`code --add-mcp failed: ${result.stderr || result.stdout}`);
446
448
  return { status: "written" };
447
449
  } },
448
450
  codex: { async write(_resolver, run, { overwrite = false } = {}) {
@@ -472,9 +474,9 @@ const writers = {
472
474
  } },
473
475
  opencode: { async write(resolver, _run, { overwrite = false } = {}) {
474
476
  const cfgPath = join(resolver.home(), ".config", "opencode", "opencode.json");
475
- const cfg = readJson(cfgPath);
477
+ const cfg = readJson$1(cfgPath);
476
478
  if (hasKey(cfg.mcp, "membank") && !overwrite) return { status: "already-configured" };
477
- writeJsonAtomic(cfgPath, {
479
+ writeJsonAtomic$1(cfgPath, {
478
480
  ...cfg,
479
481
  mcp: {
480
482
  ...cfg.mcp,
@@ -491,21 +493,142 @@ const writers = {
491
493
  return { status: "written" };
492
494
  } }
493
495
  };
494
- const SUPPORTED_HARNESSES = Object.keys(writers);
496
+ const SUPPORTED_HARNESSES = Object.keys(writers$1);
495
497
  var HarnessConfigWriter = class {
496
498
  #resolver;
497
499
  #run;
498
- constructor(resolver = defaultPathResolver, run = execFileNoThrow) {
500
+ constructor(resolver = defaultPathResolver$1, run = execFileNoThrow) {
499
501
  this.#resolver = resolver;
500
502
  this.#run = run;
501
503
  }
502
504
  async write(harness, { overwrite = false } = {}) {
503
- const writer = writers[harness];
505
+ const writer = writers$1[harness];
504
506
  if (!writer) throw new Error(`Unknown harness: ${harness}`);
505
507
  return writer.write(this.#resolver, this.#run, { overwrite });
506
508
  }
507
509
  };
508
510
  //#endregion
511
+ //#region src/setup/injection-hook-writer.ts
512
+ const defaultPathResolver = { home: () => {
513
+ const h = process.env.HOME ?? process.env.USERPROFILE;
514
+ if (!h) throw new Error("Cannot determine home directory");
515
+ return h;
516
+ } };
517
+ function readJson(path) {
518
+ try {
519
+ return JSON.parse(readFileSync(path, "utf8"));
520
+ } catch {
521
+ return {};
522
+ }
523
+ }
524
+ function writeJsonAtomic(path, data) {
525
+ mkdirSync(dirname(path), { recursive: true });
526
+ const tmp = join(mkdtempSync(join(tmpdir(), "membank-hook-")), "cfg.json");
527
+ writeFileSync(tmp, JSON.stringify(data, null, 2));
528
+ renameSync(tmp, path);
529
+ }
530
+ 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")));
533
+ }
534
+ 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"
551
+ }]
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: [{
592
+ type: "command",
593
+ command: "npx @membank/cli inject --harness codex",
594
+ timeout: 30
595
+ }]
596
+ }]
597
+ }
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" };
605
+ }
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
+ } }
619
+ };
620
+ var InjectionHookWriter = class {
621
+ #resolver;
622
+ constructor(resolver = defaultPathResolver) {
623
+ this.#resolver = resolver;
624
+ }
625
+ write(harness) {
626
+ const writer = writers[harness];
627
+ if (!writer) return { status: "not-supported" };
628
+ return writer.write(this.#resolver);
629
+ }
630
+ };
631
+ //#endregion
509
632
  //#region src/setup/model-downloader.ts
510
633
  const MODEL_NAME = "Xenova/bge-small-en-v1.5";
511
634
  var ModelDownloadError = class extends Error {
@@ -571,13 +694,9 @@ var ModelDownloader = class extends EventEmitter {
571
694
  };
572
695
  //#endregion
573
696
  //#region src/setup/harness-detector.ts
574
- const defaultResolver = {
575
- homeDir: homedir,
576
- cwd: () => process.cwd()
577
- };
697
+ const defaultResolver = { homeDir: homedir };
578
698
  function harnessConfigs(resolver) {
579
699
  const home = resolver.homeDir();
580
- const cwd = resolver.cwd();
581
700
  return [
582
701
  {
583
702
  name: "claude-code",
@@ -585,8 +704,8 @@ function harnessConfigs(resolver) {
585
704
  fallbackPaths: [join(home, ".claude", "settings.json")]
586
705
  },
587
706
  {
588
- name: "vscode",
589
- configPath: join(cwd, ".vscode", "mcp.json")
707
+ name: "copilot",
708
+ configPath: join(home, ".copilot", "mcp-config.json")
590
709
  },
591
710
  {
592
711
  name: "codex",
@@ -628,6 +747,7 @@ function defaultPrompter(question) {
628
747
  var SetupOrchestrator = class {
629
748
  #detector;
630
749
  #writer;
750
+ #hookWriter;
631
751
  #prompter;
632
752
  #modelDownloader;
633
753
  #out;
@@ -635,6 +755,7 @@ var SetupOrchestrator = class {
635
755
  constructor(deps) {
636
756
  this.#detector = deps.detector ?? (() => detectHarnesses());
637
757
  this.#writer = deps.writer;
758
+ this.#hookWriter = deps.hookWriter;
638
759
  this.#prompter = deps.prompter ?? defaultPrompter;
639
760
  this.#modelDownloader = deps.modelDownloader;
640
761
  this.#out = deps.out ?? ((msg) => process.stdout.write(`${msg}\n`));
@@ -656,6 +777,7 @@ var SetupOrchestrator = class {
656
777
  if (json) this.#out(JSON.stringify({
657
778
  detectedHarnesses: [],
658
779
  configuredHarnesses: [],
780
+ injectionHooksConfigured: [],
659
781
  modelDownloaded: false
660
782
  }));
661
783
  return [];
@@ -667,7 +789,10 @@ var SetupOrchestrator = class {
667
789
  }
668
790
  if (dryRun) {
669
791
  out("Planned changes (dry-run — no files written):");
670
- for (const h of detected) out(` ⚠ ${h.name}: would write MCP config`);
792
+ for (const h of detected) {
793
+ out(` ⚠ ${h.name}: would write MCP config`);
794
+ if (this.#hookWriter) out(` ⚠ ${h.name}: would write injection hook config`);
795
+ }
671
796
  out("");
672
797
  out(" ⚠ Model download: skipped (dry-run)");
673
798
  return detected.map((h) => ({
@@ -732,6 +857,21 @@ var SetupOrchestrator = class {
732
857
  });
733
858
  }
734
859
  out("");
860
+ const injectionHooksConfigured = [];
861
+ 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
+ }
873
+ out("");
874
+ }
735
875
  let modelDownloaded = false;
736
876
  if (this.#modelDownloader) modelDownloaded = !(await this.#runModelDownload(this.#modelDownloader, out)).skipped;
737
877
  else out("Model download step: see DRA-52");
@@ -742,6 +882,7 @@ var SetupOrchestrator = class {
742
882
  const output = {
743
883
  detectedHarnesses: detected.map((h) => h.name),
744
884
  configuredHarnesses: results.filter((r) => r.status === "written").map((r) => r.harness),
885
+ injectionHooksConfigured,
745
886
  modelDownloaded
746
887
  };
747
888
  this.#out(JSON.stringify(output));
@@ -782,7 +923,7 @@ const program = new Command();
782
923
  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)");
783
924
  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) => {
784
925
  const globalOpts = program.opts();
785
- const formatter = Formatter.create().withJson(globalOpts.json === true || !process.stdout.isTTY);
926
+ const formatter = Formatter.create(globalOpts.json === true);
786
927
  try {
787
928
  await queryCommand(queryText, cmdOptions, formatter);
788
929
  } catch (err) {
@@ -792,7 +933,7 @@ program.command("query <queryText>").description("search memories by semantic si
792
933
  });
793
934
  program.command("list").description("list memories with optional filters").option("--type <type>", "filter by memory type (correction|preference|decision|learning|fact)").option("--pinned", "return only pinned memories").action(async (cmdOptions) => {
794
935
  const globalOpts = program.opts();
795
- const formatter = Formatter.create().withJson(globalOpts.json === true || !process.stdout.isTTY);
936
+ const formatter = Formatter.create(globalOpts.json === true);
796
937
  try {
797
938
  await listCommand(cmdOptions, formatter);
798
939
  } catch (err) {
@@ -802,7 +943,7 @@ program.command("list").description("list memories with optional filters").optio
802
943
  });
803
944
  program.command("stats").description("show memory counts by type, total, and needs_review").action(async () => {
804
945
  const globalOpts = program.opts();
805
- const formatter = Formatter.create().withJson(globalOpts.json === true || !process.stdout.isTTY);
946
+ const formatter = Formatter.create(globalOpts.json === true);
806
947
  try {
807
948
  await statsCommand(formatter);
808
949
  } catch (err) {
@@ -812,7 +953,7 @@ program.command("stats").description("show memory counts by type, total, and nee
812
953
  });
813
954
  program.command("delete <id>").description("delete a memory by ID").action(async (id) => {
814
955
  const globalOpts = program.opts();
815
- const formatter = Formatter.create().withJson(globalOpts.json === true || !process.stdout.isTTY);
956
+ const formatter = Formatter.create(globalOpts.json === true);
816
957
  const prompt = new PromptHelper(globalOpts.yes === true || !process.stdout.isTTY);
817
958
  const db = DatabaseManager.open();
818
959
  try {
@@ -826,7 +967,7 @@ program.command("delete <id>").description("delete a memory by ID").action(async
826
967
  });
827
968
  program.command("add <content>").description("save a new memory").requiredOption("--type <type>", "memory type (correction|preference|decision|learning|fact)").option("--tags <tags>", "comma-separated tags").option("--scope <scope>", "scope (global or project identifier)").action(async (content, cmdOptions) => {
828
969
  const globalOpts = program.opts();
829
- const formatter = Formatter.create().withJson(globalOpts.json === true || !process.stdout.isTTY);
970
+ const formatter = Formatter.create(globalOpts.json === true);
830
971
  try {
831
972
  await addCommand(content, cmdOptions, formatter);
832
973
  } catch (err) {
@@ -836,7 +977,7 @@ program.command("add <content>").description("save a new memory").requiredOption
836
977
  });
837
978
  program.command("pin <id>").description("pin a memory by ID").action((id) => {
838
979
  const globalOpts = program.opts();
839
- const formatter = Formatter.create().withJson(globalOpts.json === true || !process.stdout.isTTY);
980
+ const formatter = Formatter.create(globalOpts.json === true);
840
981
  try {
841
982
  pinCommand(id, formatter);
842
983
  } catch (err) {
@@ -846,7 +987,7 @@ program.command("pin <id>").description("pin a memory by ID").action((id) => {
846
987
  });
847
988
  program.command("unpin <id>").description("unpin a memory by ID").action((id) => {
848
989
  const globalOpts = program.opts();
849
- const formatter = Formatter.create().withJson(globalOpts.json === true || !process.stdout.isTTY);
990
+ const formatter = Formatter.create(globalOpts.json === true);
850
991
  try {
851
992
  unpinCommand(id, formatter);
852
993
  } catch (err) {
@@ -856,7 +997,7 @@ program.command("unpin <id>").description("unpin a memory by ID").action((id) =>
856
997
  });
857
998
  program.command("export").description("export all memories to a JSON file").option("--output <path>", "output file path (default: membank-export-<timestamp>.json in cwd)").action((cmdOptions) => {
858
999
  const globalOpts = program.opts();
859
- const formatter = Formatter.create().withJson(globalOpts.json === true || !process.stdout.isTTY);
1000
+ const formatter = Formatter.create(globalOpts.json === true);
860
1001
  const db = DatabaseManager.open();
861
1002
  try {
862
1003
  exportCommand(db, formatter, cmdOptions);
@@ -869,7 +1010,7 @@ program.command("export").description("export all memories to a JSON file").opti
869
1010
  });
870
1011
  program.command("import <file>").description("import memories from a JSON export file").action(async (file) => {
871
1012
  const globalOpts = program.opts();
872
- const formatter = Formatter.create().withJson(globalOpts.json === true || !process.stdout.isTTY);
1013
+ const formatter = Formatter.create(globalOpts.json === true);
873
1014
  const prompt = new PromptHelper(globalOpts.yes === true || !process.stdout.isTTY);
874
1015
  const db = DatabaseManager.open();
875
1016
  try {
@@ -881,21 +1022,30 @@ program.command("import <file>").description("import memories from a JSON export
881
1022
  db.close();
882
1023
  }
883
1024
  });
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) => {
1026
+ try {
1027
+ await injectCommand(cmdOptions);
1028
+ } catch (err) {
1029
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
1030
+ process.exit(2);
1031
+ }
1032
+ });
884
1033
  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) => {
885
1034
  const globalOpts = program.opts();
886
1035
  const autoYes = cmdOptions.yes === true || globalOpts.yes === true;
887
- const useJson = globalOpts.json === true || !process.stdout.isTTY;
888
- const formatter = Formatter.create().withJson(useJson);
1036
+ const formatter = Formatter.create(globalOpts.json === true);
889
1037
  if (cmdOptions.harness !== void 0) {
890
- if (!SUPPORTED_HARNESSES.includes(cmdOptions.harness)) {
1038
+ if (!SUPPORTED_HARNESSES.some((h) => h === cmdOptions.harness)) {
891
1039
  formatter.error(`Unknown harness: "${cmdOptions.harness}". Supported: ${SUPPORTED_HARNESSES.join(", ")}`);
892
1040
  process.exit(1);
893
1041
  }
894
1042
  }
895
1043
  const writer = new HarnessConfigWriter();
1044
+ const hookWriter = new InjectionHookWriter();
896
1045
  const promptHelper = new PromptHelper(autoYes);
897
1046
  const orchestrator = new SetupOrchestrator({
898
1047
  writer,
1048
+ hookWriter,
899
1049
  prompter: (question) => promptHelper.confirm(question),
900
1050
  modelDownloader: new ModelDownloader()
901
1051
  });
@@ -904,7 +1054,7 @@ program.command("setup").description("detect installed harnesses and write MCP c
904
1054
  yes: autoYes,
905
1055
  dryRun: cmdOptions.dryRun,
906
1056
  harness: cmdOptions.harness,
907
- json: useJson
1057
+ json: formatter.isJson
908
1058
  })).some((r) => r.status === "error")) process.exit(1);
909
1059
  } catch (err) {
910
1060
  formatter.error(err instanceof Error ? err.message : String(err));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@membank/cli",
3
- "version": "0.0.3",
3
+ "version": "0.1.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,8 +17,8 @@
17
17
  "@huggingface/transformers": "^4.2.0",
18
18
  "commander": "^14.0.3",
19
19
  "ora": "^9.4.0",
20
- "@membank/core": "0.0.3",
21
- "@membank/mcp": "0.0.3"
20
+ "@membank/core": "0.1.0",
21
+ "@membank/mcp": "0.1.0"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/node": "^25.6.0",