@membank/cli 0.0.3 → 0.0.4

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 +242 -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,41 @@ 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
+ function formatContext(ctx) {
132
+ const lines = [];
133
+ const statParts = Object.entries(ctx.stats).filter(([, count]) => count > 0).map(([type, count]) => `${count} ${type}${count !== 1 ? "s" : ""}`);
134
+ if (statParts.length > 0) lines.push(`[Memory Stats]: ${statParts.join(", ")}`);
135
+ const formatMemory = (m) => `"${m.content}" (${m.type})`;
136
+ for (const m of ctx.pinnedGlobal) lines.push(`[Pinned Global]: ${formatMemory(m)}`);
137
+ for (const m of ctx.pinnedProject) lines.push(`[Pinned Project]: ${formatMemory(m)}`);
138
+ return lines.join("\n");
139
+ }
140
+ async function injectCommand(opts) {
141
+ const projectScope = opts.scope ?? await resolveScope();
142
+ const db = DatabaseManager.open();
143
+ let text;
144
+ try {
145
+ text = formatContext(new SessionContextBuilder(db).getSessionContext(projectScope));
146
+ } finally {
147
+ db.close();
148
+ }
149
+ if (!text) process.exit(0);
150
+ const harness = opts.harness;
151
+ if (harness === "claude-code") {
152
+ process.stdout.write(JSON.stringify({ hookSpecificOutput: {
153
+ hookEventName: "SessionStart",
154
+ additionalContext: text
155
+ } }));
156
+ return;
157
+ }
158
+ if (harness === "copilot-cli") {
159
+ process.stdout.write(JSON.stringify({ additionalContext: text }));
160
+ return;
161
+ }
162
+ process.stdout.write(`${text}\n`);
163
+ }
164
+ //#endregion
136
165
  //#region src/commands/list.ts
137
166
  async function listCommand(options, formatter) {
138
167
  const db = DatabaseManager.open();
@@ -216,11 +245,8 @@ var Formatter = class Formatter {
216
245
  constructor(isJson) {
217
246
  this.#isJson = isJson;
218
247
  }
219
- static create() {
220
- return new Formatter(!process.stdout.isTTY);
221
- }
222
- withJson(isJson) {
223
- return new Formatter(isJson);
248
+ static create(forceJson = false) {
249
+ return new Formatter(forceJson || !process.stdout.isTTY);
224
250
  }
225
251
  get isJson() {
226
252
  return this.#isJson;
@@ -230,13 +256,7 @@ var Formatter = class Formatter {
230
256
  process.stdout.write(`${JSON.stringify(memory)}\n`);
231
257
  return;
232
258
  }
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");
259
+ this.#writeMemoryBlock(memory, ` Pinned : ${memory.pinned}\n`);
240
260
  }
241
261
  outputMemories(memories) {
242
262
  if (this.#isJson) {
@@ -247,13 +267,7 @@ var Formatter = class Formatter {
247
267
  process.stdout.write("No memories found.\n");
248
268
  return;
249
269
  }
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
- }
270
+ for (const memory of memories) this.#writeMemoryBlock(memory);
257
271
  process.stdout.write("\n");
258
272
  }
259
273
  outputStats(stats) {
@@ -280,20 +294,22 @@ var Formatter = class Formatter {
280
294
  process.stdout.write("No memories found.\n");
281
295
  return;
282
296
  }
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
- }
297
+ for (const result of results) this.#writeMemoryBlock(result, ` Score : ${result.score.toFixed(4)}\n`);
291
298
  process.stdout.write("\n");
292
299
  }
293
300
  error(msg) {
294
301
  if (this.#isJson) process.stderr.write(`${JSON.stringify({ error: msg })}\n`);
295
302
  else process.stderr.write(`Error: ${msg}\n`);
296
303
  }
304
+ #writeMemoryBlock(memory, extra) {
305
+ const tags = memory.tags.length > 0 ? memory.tags.join(", ") : "(none)";
306
+ process.stdout.write(`\n[${memory.type}] ${memory.id}\n`);
307
+ process.stdout.write(` Content : ${memory.content}\n`);
308
+ process.stdout.write(` Tags : ${tags}\n`);
309
+ process.stdout.write(` Scope : ${memory.scope}\n`);
310
+ if (extra !== void 0) process.stdout.write(extra);
311
+ process.stdout.write("\n");
312
+ }
297
313
  };
298
314
  //#endregion
299
315
  //#region src/prompt-helper.ts
@@ -352,7 +368,7 @@ async function execFileNoThrow(cmd, args) {
352
368
  }
353
369
  //#endregion
354
370
  //#region src/setup/harness-config-writer.ts
355
- const defaultPathResolver = {
371
+ const defaultPathResolver$1 = {
356
372
  home: () => {
357
373
  const h = process.env.HOME ?? process.env.USERPROFILE;
358
374
  if (!h) throw new Error("Cannot determine home directory");
@@ -360,14 +376,14 @@ const defaultPathResolver = {
360
376
  },
361
377
  cwd: () => process.cwd()
362
378
  };
363
- function readJson(path) {
379
+ function readJson$1(path) {
364
380
  try {
365
381
  return JSON.parse(readFileSync(path, "utf8"));
366
382
  } catch {
367
383
  return {};
368
384
  }
369
385
  }
370
- function writeJsonAtomic(path, data) {
386
+ function writeJsonAtomic$1(path, data) {
371
387
  mkdirSync(dirname(path), { recursive: true });
372
388
  const tmp = join(mkdtempSync(join(tmpdir(), "membank-")), "cfg.json");
373
389
  writeFileSync(tmp, JSON.stringify(data, null, 2));
@@ -384,9 +400,9 @@ const MEMBANK_NPX_ARGS = [
384
400
  "@membank/cli@latest",
385
401
  "--mcp"
386
402
  ];
387
- const writers = {
403
+ const writers$1 = {
388
404
  "claude-code": { async write(resolver, run, { overwrite = false } = {}) {
389
- const configured = hasKey(readJson(join(resolver.home(), ".claude.json")).mcpServers, "membank");
405
+ const configured = hasKey(readJson$1(join(resolver.home(), ".claude.json")).mcpServers, "membank");
390
406
  if (configured && !overwrite) return { status: "already-configured" };
391
407
  if (configured) {
392
408
  const remove = await run("claude", [
@@ -412,37 +428,20 @@ const writers = {
412
428
  if (add.exitCode !== 0) throw new Error(`claude mcp add failed: ${add.stderr || add.stdout}`);
413
429
  return { status: "written" };
414
430
  } },
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
- }
431
+ copilot: { async write(resolver, _run, { overwrite = false } = {}) {
432
+ const cfgPath = join(resolver.home(), ".copilot", "mcp-config.json");
433
+ const cfg = readJson$1(cfgPath);
434
+ if (hasKey(cfg.mcpServers, "membank") && !overwrite) return { status: "already-configured" };
435
+ writeJsonAtomic$1(cfgPath, {
436
+ ...cfg,
437
+ mcpServers: {
438
+ ...cfg.mcpServers,
439
+ membank: {
440
+ command: "npx",
441
+ args: ["@membank/cli@latest", "--mcp"]
429
442
  }
430
- });
431
- return { status: "written" };
432
- }
433
- const payload = JSON.stringify({
434
- name: "membank",
435
- command: "npx",
436
- args: ["@membank/cli@latest", "--mcp"]
443
+ }
437
444
  });
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
445
  return { status: "written" };
447
446
  } },
448
447
  codex: { async write(_resolver, run, { overwrite = false } = {}) {
@@ -472,9 +471,9 @@ const writers = {
472
471
  } },
473
472
  opencode: { async write(resolver, _run, { overwrite = false } = {}) {
474
473
  const cfgPath = join(resolver.home(), ".config", "opencode", "opencode.json");
475
- const cfg = readJson(cfgPath);
474
+ const cfg = readJson$1(cfgPath);
476
475
  if (hasKey(cfg.mcp, "membank") && !overwrite) return { status: "already-configured" };
477
- writeJsonAtomic(cfgPath, {
476
+ writeJsonAtomic$1(cfgPath, {
478
477
  ...cfg,
479
478
  mcp: {
480
479
  ...cfg.mcp,
@@ -491,21 +490,142 @@ const writers = {
491
490
  return { status: "written" };
492
491
  } }
493
492
  };
494
- const SUPPORTED_HARNESSES = Object.keys(writers);
493
+ const SUPPORTED_HARNESSES = Object.keys(writers$1);
495
494
  var HarnessConfigWriter = class {
496
495
  #resolver;
497
496
  #run;
498
- constructor(resolver = defaultPathResolver, run = execFileNoThrow) {
497
+ constructor(resolver = defaultPathResolver$1, run = execFileNoThrow) {
499
498
  this.#resolver = resolver;
500
499
  this.#run = run;
501
500
  }
502
501
  async write(harness, { overwrite = false } = {}) {
503
- const writer = writers[harness];
502
+ const writer = writers$1[harness];
504
503
  if (!writer) throw new Error(`Unknown harness: ${harness}`);
505
504
  return writer.write(this.#resolver, this.#run, { overwrite });
506
505
  }
507
506
  };
508
507
  //#endregion
508
+ //#region src/setup/injection-hook-writer.ts
509
+ const defaultPathResolver = { home: () => {
510
+ const h = process.env.HOME ?? process.env.USERPROFILE;
511
+ if (!h) throw new Error("Cannot determine home directory");
512
+ return h;
513
+ } };
514
+ function readJson(path) {
515
+ try {
516
+ return JSON.parse(readFileSync(path, "utf8"));
517
+ } catch {
518
+ return {};
519
+ }
520
+ }
521
+ function writeJsonAtomic(path, data) {
522
+ mkdirSync(dirname(path), { recursive: true });
523
+ const tmp = join(mkdtempSync(join(tmpdir(), "membank-hook-")), "cfg.json");
524
+ writeFileSync(tmp, JSON.stringify(data, null, 2));
525
+ renameSync(tmp, path);
526
+ }
527
+ function containsMembankInject(hooks) {
528
+ if (!Array.isArray(hooks)) return false;
529
+ 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")));
530
+ }
531
+ const writers = {
532
+ "claude-code": { write(resolver) {
533
+ const cfgPath = join(resolver.home(), ".claude", "settings.json");
534
+ const cfg = readJson(cfgPath);
535
+ const hooks = cfg.hooks;
536
+ const sessionStart = hooks?.SessionStart;
537
+ if (Array.isArray(sessionStart) && containsMembankInject(sessionStart.flatMap((g) => Array.isArray(g.hooks) ? g.hooks : []))) return { status: "already-configured" };
538
+ const existingSessionStart = Array.isArray(sessionStart) ? sessionStart : [];
539
+ writeJsonAtomic(cfgPath, {
540
+ ...cfg,
541
+ hooks: {
542
+ ...hooks ?? {},
543
+ SessionStart: [...existingSessionStart, {
544
+ matcher: "",
545
+ hooks: [{
546
+ type: "command",
547
+ command: "npx @membank/cli inject --harness claude-code"
548
+ }]
549
+ }]
550
+ }
551
+ });
552
+ return { status: "written" };
553
+ } },
554
+ "copilot-cli": { write(resolver) {
555
+ const cfgPath = join(resolver.home(), ".copilot", "settings.json");
556
+ const cfg = readJson(cfgPath);
557
+ const hooks = cfg.hooks;
558
+ const sessionStart = hooks?.sessionStart;
559
+ if (Array.isArray(sessionStart) && containsMembankInject(sessionStart)) return { status: "already-configured" };
560
+ const existingSessionStart = Array.isArray(sessionStart) ? sessionStart : [];
561
+ writeJsonAtomic(cfgPath, {
562
+ version: cfg.version ?? 1,
563
+ ...cfg,
564
+ hooks: {
565
+ ...hooks ?? {},
566
+ sessionStart: [...existingSessionStart, {
567
+ type: "command",
568
+ bash: "npx @membank/cli inject --harness copilot-cli",
569
+ timeoutSec: 30
570
+ }]
571
+ }
572
+ });
573
+ return { status: "written" };
574
+ } },
575
+ codex: { write(resolver) {
576
+ const cfgPath = join(resolver.home(), ".codex", "hooks.json");
577
+ const cfg = readJson(cfgPath);
578
+ const hooks = cfg.hooks;
579
+ const sessionStart = hooks?.SessionStart;
580
+ if (Array.isArray(sessionStart) && containsMembankInject(sessionStart.flatMap((g) => Array.isArray(g.hooks) ? g.hooks : []))) return { status: "already-configured" };
581
+ const existingSessionStart = Array.isArray(sessionStart) ? sessionStart : [];
582
+ writeJsonAtomic(cfgPath, {
583
+ ...cfg,
584
+ hooks: {
585
+ ...hooks ?? {},
586
+ SessionStart: [...existingSessionStart, {
587
+ matcher: "",
588
+ hooks: [{
589
+ type: "command",
590
+ command: "npx @membank/cli inject --harness codex",
591
+ timeout: 30
592
+ }]
593
+ }]
594
+ }
595
+ });
596
+ return { status: "written" };
597
+ } },
598
+ opencode: { write(resolver) {
599
+ const pluginPath = join(resolver.home(), ".config", "opencode", "plugins", "membank.js");
600
+ if (existsSync(pluginPath)) {
601
+ if (readFileSync(pluginPath, "utf8").includes("@membank/cli inject")) return { status: "already-configured" };
602
+ }
603
+ mkdirSync(dirname(pluginPath), { recursive: true });
604
+ writeFileSync(pluginPath, [
605
+ "export default {",
606
+ " hooks: {",
607
+ " \"session.start\": async ({ $ }) => {",
608
+ " return await $`npx @membank/cli inject`.text();",
609
+ " },",
610
+ " },",
611
+ "};",
612
+ ""
613
+ ].join("\n"), "utf8");
614
+ return { status: "written" };
615
+ } }
616
+ };
617
+ var InjectionHookWriter = class {
618
+ #resolver;
619
+ constructor(resolver = defaultPathResolver) {
620
+ this.#resolver = resolver;
621
+ }
622
+ write(harness) {
623
+ const writer = writers[harness];
624
+ if (!writer) return { status: "not-supported" };
625
+ return writer.write(this.#resolver);
626
+ }
627
+ };
628
+ //#endregion
509
629
  //#region src/setup/model-downloader.ts
510
630
  const MODEL_NAME = "Xenova/bge-small-en-v1.5";
511
631
  var ModelDownloadError = class extends Error {
@@ -571,13 +691,9 @@ var ModelDownloader = class extends EventEmitter {
571
691
  };
572
692
  //#endregion
573
693
  //#region src/setup/harness-detector.ts
574
- const defaultResolver = {
575
- homeDir: homedir,
576
- cwd: () => process.cwd()
577
- };
694
+ const defaultResolver = { homeDir: homedir };
578
695
  function harnessConfigs(resolver) {
579
696
  const home = resolver.homeDir();
580
- const cwd = resolver.cwd();
581
697
  return [
582
698
  {
583
699
  name: "claude-code",
@@ -585,8 +701,8 @@ function harnessConfigs(resolver) {
585
701
  fallbackPaths: [join(home, ".claude", "settings.json")]
586
702
  },
587
703
  {
588
- name: "vscode",
589
- configPath: join(cwd, ".vscode", "mcp.json")
704
+ name: "copilot",
705
+ configPath: join(home, ".copilot", "mcp-config.json")
590
706
  },
591
707
  {
592
708
  name: "codex",
@@ -628,6 +744,7 @@ function defaultPrompter(question) {
628
744
  var SetupOrchestrator = class {
629
745
  #detector;
630
746
  #writer;
747
+ #hookWriter;
631
748
  #prompter;
632
749
  #modelDownloader;
633
750
  #out;
@@ -635,6 +752,7 @@ var SetupOrchestrator = class {
635
752
  constructor(deps) {
636
753
  this.#detector = deps.detector ?? (() => detectHarnesses());
637
754
  this.#writer = deps.writer;
755
+ this.#hookWriter = deps.hookWriter;
638
756
  this.#prompter = deps.prompter ?? defaultPrompter;
639
757
  this.#modelDownloader = deps.modelDownloader;
640
758
  this.#out = deps.out ?? ((msg) => process.stdout.write(`${msg}\n`));
@@ -656,6 +774,7 @@ var SetupOrchestrator = class {
656
774
  if (json) this.#out(JSON.stringify({
657
775
  detectedHarnesses: [],
658
776
  configuredHarnesses: [],
777
+ injectionHooksConfigured: [],
659
778
  modelDownloaded: false
660
779
  }));
661
780
  return [];
@@ -667,7 +786,10 @@ var SetupOrchestrator = class {
667
786
  }
668
787
  if (dryRun) {
669
788
  out("Planned changes (dry-run — no files written):");
670
- for (const h of detected) out(` ⚠ ${h.name}: would write MCP config`);
789
+ for (const h of detected) {
790
+ out(` ⚠ ${h.name}: would write MCP config`);
791
+ if (this.#hookWriter) out(` ⚠ ${h.name}: would write injection hook config`);
792
+ }
671
793
  out("");
672
794
  out(" ⚠ Model download: skipped (dry-run)");
673
795
  return detected.map((h) => ({
@@ -732,6 +854,21 @@ var SetupOrchestrator = class {
732
854
  });
733
855
  }
734
856
  out("");
857
+ const injectionHooksConfigured = [];
858
+ if (this.#hookWriter) {
859
+ for (const h of detected) try {
860
+ const hookResult = this.#hookWriter.write(h.name);
861
+ if (hookResult.status === "not-supported") continue;
862
+ if (hookResult.status === "written") {
863
+ out(` ✓ ${h.name}: injection hook written`);
864
+ injectionHooksConfigured.push(h.name);
865
+ } else out(` ⚠ ${h.name}: injection hook already configured`);
866
+ } catch (err) {
867
+ const msg = err instanceof Error ? err.message : String(err);
868
+ out(` ✗ ${h.name} injection hook: ${msg}`);
869
+ }
870
+ out("");
871
+ }
735
872
  let modelDownloaded = false;
736
873
  if (this.#modelDownloader) modelDownloaded = !(await this.#runModelDownload(this.#modelDownloader, out)).skipped;
737
874
  else out("Model download step: see DRA-52");
@@ -742,6 +879,7 @@ var SetupOrchestrator = class {
742
879
  const output = {
743
880
  detectedHarnesses: detected.map((h) => h.name),
744
881
  configuredHarnesses: results.filter((r) => r.status === "written").map((r) => r.harness),
882
+ injectionHooksConfigured,
745
883
  modelDownloaded
746
884
  };
747
885
  this.#out(JSON.stringify(output));
@@ -782,7 +920,7 @@ const program = new Command();
782
920
  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
921
  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
922
  const globalOpts = program.opts();
785
- const formatter = Formatter.create().withJson(globalOpts.json === true || !process.stdout.isTTY);
923
+ const formatter = Formatter.create(globalOpts.json === true);
786
924
  try {
787
925
  await queryCommand(queryText, cmdOptions, formatter);
788
926
  } catch (err) {
@@ -792,7 +930,7 @@ program.command("query <queryText>").description("search memories by semantic si
792
930
  });
793
931
  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
932
  const globalOpts = program.opts();
795
- const formatter = Formatter.create().withJson(globalOpts.json === true || !process.stdout.isTTY);
933
+ const formatter = Formatter.create(globalOpts.json === true);
796
934
  try {
797
935
  await listCommand(cmdOptions, formatter);
798
936
  } catch (err) {
@@ -802,7 +940,7 @@ program.command("list").description("list memories with optional filters").optio
802
940
  });
803
941
  program.command("stats").description("show memory counts by type, total, and needs_review").action(async () => {
804
942
  const globalOpts = program.opts();
805
- const formatter = Formatter.create().withJson(globalOpts.json === true || !process.stdout.isTTY);
943
+ const formatter = Formatter.create(globalOpts.json === true);
806
944
  try {
807
945
  await statsCommand(formatter);
808
946
  } catch (err) {
@@ -812,7 +950,7 @@ program.command("stats").description("show memory counts by type, total, and nee
812
950
  });
813
951
  program.command("delete <id>").description("delete a memory by ID").action(async (id) => {
814
952
  const globalOpts = program.opts();
815
- const formatter = Formatter.create().withJson(globalOpts.json === true || !process.stdout.isTTY);
953
+ const formatter = Formatter.create(globalOpts.json === true);
816
954
  const prompt = new PromptHelper(globalOpts.yes === true || !process.stdout.isTTY);
817
955
  const db = DatabaseManager.open();
818
956
  try {
@@ -826,7 +964,7 @@ program.command("delete <id>").description("delete a memory by ID").action(async
826
964
  });
827
965
  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
966
  const globalOpts = program.opts();
829
- const formatter = Formatter.create().withJson(globalOpts.json === true || !process.stdout.isTTY);
967
+ const formatter = Formatter.create(globalOpts.json === true);
830
968
  try {
831
969
  await addCommand(content, cmdOptions, formatter);
832
970
  } catch (err) {
@@ -836,7 +974,7 @@ program.command("add <content>").description("save a new memory").requiredOption
836
974
  });
837
975
  program.command("pin <id>").description("pin a memory by ID").action((id) => {
838
976
  const globalOpts = program.opts();
839
- const formatter = Formatter.create().withJson(globalOpts.json === true || !process.stdout.isTTY);
977
+ const formatter = Formatter.create(globalOpts.json === true);
840
978
  try {
841
979
  pinCommand(id, formatter);
842
980
  } catch (err) {
@@ -846,7 +984,7 @@ program.command("pin <id>").description("pin a memory by ID").action((id) => {
846
984
  });
847
985
  program.command("unpin <id>").description("unpin a memory by ID").action((id) => {
848
986
  const globalOpts = program.opts();
849
- const formatter = Formatter.create().withJson(globalOpts.json === true || !process.stdout.isTTY);
987
+ const formatter = Formatter.create(globalOpts.json === true);
850
988
  try {
851
989
  unpinCommand(id, formatter);
852
990
  } catch (err) {
@@ -856,7 +994,7 @@ program.command("unpin <id>").description("unpin a memory by ID").action((id) =>
856
994
  });
857
995
  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
996
  const globalOpts = program.opts();
859
- const formatter = Formatter.create().withJson(globalOpts.json === true || !process.stdout.isTTY);
997
+ const formatter = Formatter.create(globalOpts.json === true);
860
998
  const db = DatabaseManager.open();
861
999
  try {
862
1000
  exportCommand(db, formatter, cmdOptions);
@@ -869,7 +1007,7 @@ program.command("export").description("export all memories to a JSON file").opti
869
1007
  });
870
1008
  program.command("import <file>").description("import memories from a JSON export file").action(async (file) => {
871
1009
  const globalOpts = program.opts();
872
- const formatter = Formatter.create().withJson(globalOpts.json === true || !process.stdout.isTTY);
1010
+ const formatter = Formatter.create(globalOpts.json === true);
873
1011
  const prompt = new PromptHelper(globalOpts.yes === true || !process.stdout.isTTY);
874
1012
  const db = DatabaseManager.open();
875
1013
  try {
@@ -881,21 +1019,30 @@ program.command("import <file>").description("import memories from a JSON export
881
1019
  db.close();
882
1020
  }
883
1021
  });
1022
+ 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) => {
1023
+ try {
1024
+ await injectCommand(cmdOptions);
1025
+ } catch (err) {
1026
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
1027
+ process.exit(2);
1028
+ }
1029
+ });
884
1030
  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
1031
  const globalOpts = program.opts();
886
1032
  const autoYes = cmdOptions.yes === true || globalOpts.yes === true;
887
- const useJson = globalOpts.json === true || !process.stdout.isTTY;
888
- const formatter = Formatter.create().withJson(useJson);
1033
+ const formatter = Formatter.create(globalOpts.json === true);
889
1034
  if (cmdOptions.harness !== void 0) {
890
- if (!SUPPORTED_HARNESSES.includes(cmdOptions.harness)) {
1035
+ if (!SUPPORTED_HARNESSES.some((h) => h === cmdOptions.harness)) {
891
1036
  formatter.error(`Unknown harness: "${cmdOptions.harness}". Supported: ${SUPPORTED_HARNESSES.join(", ")}`);
892
1037
  process.exit(1);
893
1038
  }
894
1039
  }
895
1040
  const writer = new HarnessConfigWriter();
1041
+ const hookWriter = new InjectionHookWriter();
896
1042
  const promptHelper = new PromptHelper(autoYes);
897
1043
  const orchestrator = new SetupOrchestrator({
898
1044
  writer,
1045
+ hookWriter,
899
1046
  prompter: (question) => promptHelper.confirm(question),
900
1047
  modelDownloader: new ModelDownloader()
901
1048
  });
@@ -904,7 +1051,7 @@ program.command("setup").description("detect installed harnesses and write MCP c
904
1051
  yes: autoYes,
905
1052
  dryRun: cmdOptions.dryRun,
906
1053
  harness: cmdOptions.harness,
907
- json: useJson
1054
+ json: formatter.isJson
908
1055
  })).some((r) => r.status === "error")) process.exit(1);
909
1056
  } catch (err) {
910
1057
  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.0.4",
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/mcp": "0.0.4",
21
+ "@membank/core": "0.0.4"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/node": "^25.6.0",