@membank/cli 0.0.2 → 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 +367 -117
  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";
@@ -7,6 +7,8 @@ import { dirname, join } from "node:path";
7
7
  import * as readline from "node:readline";
8
8
  import { createInterface } from "node:readline";
9
9
  import { homedir, tmpdir } from "node:os";
10
+ import { execFile } from "node:child_process";
11
+ import { promisify } from "node:util";
10
12
  import { EventEmitter } from "node:events";
11
13
  import { pipeline } from "@huggingface/transformers";
12
14
  //#region src/commands/add.ts
@@ -70,22 +72,16 @@ function exportCommand(db, formatter, opts) {
70
72
  }
71
73
  //#endregion
72
74
  //#region src/commands/import.ts
73
- const MEMORY_TYPES = new Set([
74
- "correction",
75
- "preference",
76
- "decision",
77
- "learning",
78
- "fact"
79
- ]);
75
+ const MEMORY_TYPES = new Set(MEMORY_TYPE_VALUES);
80
76
  function isValidRecord(r) {
81
77
  if (typeof r !== "object" || r === null) return false;
82
78
  const rec = r;
83
- return typeof rec["id"] === "string" && rec["id"].length > 0 && typeof rec["content"] === "string" && typeof rec["type"] === "string" && MEMORY_TYPES.has(rec["type"]) && typeof rec["scope"] === "string" && rec["scope"].length > 0;
79
+ return typeof rec.id === "string" && rec.id.length > 0 && typeof rec.content === "string" && typeof rec.type === "string" && MEMORY_TYPES.has(rec.type) && typeof rec.scope === "string" && rec.scope.length > 0;
84
80
  }
85
81
  function isExportFile(parsed) {
86
82
  if (typeof parsed !== "object" || parsed === null) return false;
87
83
  const obj = parsed;
88
- return obj["version"] === 1 && Array.isArray(obj["memories"]);
84
+ return obj.version === 1 && Array.isArray(obj.memories);
89
85
  }
90
86
  async function importCommand(filePath, db, formatter, prompt) {
91
87
  let raw;
@@ -131,6 +127,41 @@ async function importCommand(filePath, db, formatter, prompt) {
131
127
  else process.stdout.write(`Imported ${count} memories.\n`);
132
128
  }
133
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
134
165
  //#region src/commands/list.ts
135
166
  async function listCommand(options, formatter) {
136
167
  const db = DatabaseManager.open();
@@ -214,11 +245,8 @@ var Formatter = class Formatter {
214
245
  constructor(isJson) {
215
246
  this.#isJson = isJson;
216
247
  }
217
- static create() {
218
- return new Formatter(!process.stdout.isTTY);
219
- }
220
- withJson(isJson) {
221
- return new Formatter(isJson);
248
+ static create(forceJson = false) {
249
+ return new Formatter(forceJson || !process.stdout.isTTY);
222
250
  }
223
251
  get isJson() {
224
252
  return this.#isJson;
@@ -228,13 +256,7 @@ var Formatter = class Formatter {
228
256
  process.stdout.write(`${JSON.stringify(memory)}\n`);
229
257
  return;
230
258
  }
231
- const tags = memory.tags.length > 0 ? memory.tags.join(", ") : "(none)";
232
- process.stdout.write(`\n[${memory.type}] ${memory.id}\n`);
233
- process.stdout.write(` Content : ${memory.content}\n`);
234
- process.stdout.write(` Tags : ${tags}\n`);
235
- process.stdout.write(` Scope : ${memory.scope}\n`);
236
- process.stdout.write(` Pinned : ${memory.pinned}\n`);
237
- process.stdout.write("\n");
259
+ this.#writeMemoryBlock(memory, ` Pinned : ${memory.pinned}\n`);
238
260
  }
239
261
  outputMemories(memories) {
240
262
  if (this.#isJson) {
@@ -245,13 +267,7 @@ var Formatter = class Formatter {
245
267
  process.stdout.write("No memories found.\n");
246
268
  return;
247
269
  }
248
- for (const memory of memories) {
249
- const tags = memory.tags.length > 0 ? memory.tags.join(", ") : "(none)";
250
- process.stdout.write(`\n[${memory.type}] ${memory.id}\n`);
251
- process.stdout.write(` Content : ${memory.content}\n`);
252
- process.stdout.write(` Tags : ${tags}\n`);
253
- process.stdout.write(` Scope : ${memory.scope}\n`);
254
- }
270
+ for (const memory of memories) this.#writeMemoryBlock(memory);
255
271
  process.stdout.write("\n");
256
272
  }
257
273
  outputStats(stats) {
@@ -278,20 +294,22 @@ var Formatter = class Formatter {
278
294
  process.stdout.write("No memories found.\n");
279
295
  return;
280
296
  }
281
- for (const result of results) {
282
- const tags = result.tags.length > 0 ? result.tags.join(", ") : "(none)";
283
- process.stdout.write(`\n[${result.type}] ${result.id}\n`);
284
- process.stdout.write(` Content : ${result.content}\n`);
285
- process.stdout.write(` Tags : ${tags}\n`);
286
- process.stdout.write(` Scope : ${result.scope}\n`);
287
- process.stdout.write(` Score : ${result.score.toFixed(4)}\n`);
288
- }
297
+ for (const result of results) this.#writeMemoryBlock(result, ` Score : ${result.score.toFixed(4)}\n`);
289
298
  process.stdout.write("\n");
290
299
  }
291
300
  error(msg) {
292
301
  if (this.#isJson) process.stderr.write(`${JSON.stringify({ error: msg })}\n`);
293
302
  else process.stderr.write(`Error: ${msg}\n`);
294
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
+ }
295
313
  };
296
314
  //#endregion
297
315
  //#region src/prompt-helper.ts
@@ -314,27 +332,58 @@ var PromptHelper = class {
314
332
  }
315
333
  };
316
334
  //#endregion
335
+ //#region src/utils/execFileNoThrow.ts
336
+ const execFileAsync = promisify(execFile);
337
+ function resolveCmd(cmd) {
338
+ return process.platform === "win32" ? `${cmd}.cmd` : cmd;
339
+ }
340
+ async function execFileNoThrow(cmd, args) {
341
+ try {
342
+ const { stdout, stderr } = await execFileAsync(resolveCmd(cmd), args, { encoding: "utf8" });
343
+ return {
344
+ stdout: stdout ?? "",
345
+ stderr: stderr ?? "",
346
+ exitCode: 0
347
+ };
348
+ } catch (err) {
349
+ if (err !== null && typeof err === "object" && "code" in err) {
350
+ const e = err;
351
+ if (e.code === "ENOENT") return {
352
+ stdout: "",
353
+ stderr: `Command not found: ${cmd}`,
354
+ exitCode: 127
355
+ };
356
+ return {
357
+ stdout: typeof e.stdout === "string" ? e.stdout : "",
358
+ stderr: typeof e.stderr === "string" ? e.stderr : "",
359
+ exitCode: typeof e.code === "number" ? e.code : 1
360
+ };
361
+ }
362
+ return {
363
+ stdout: "",
364
+ stderr: err instanceof Error ? err.message : String(err),
365
+ exitCode: 1
366
+ };
367
+ }
368
+ }
369
+ //#endregion
317
370
  //#region src/setup/harness-config-writer.ts
318
- const defaultPathResolver = {
371
+ const defaultPathResolver$1 = {
319
372
  home: () => {
320
- const h = process.env["HOME"] ?? process.env["USERPROFILE"];
373
+ const h = process.env.HOME ?? process.env.USERPROFILE;
321
374
  if (!h) throw new Error("Cannot determine home directory");
322
375
  return h;
323
376
  },
324
377
  cwd: () => process.cwd()
325
378
  };
326
- const MEMBANK_ENTRY = {
327
- command: "npx",
328
- args: ["@membank/cli", "--mcp"]
329
- };
330
- function readJson(path) {
379
+ function readJson$1(path) {
331
380
  try {
332
381
  return JSON.parse(readFileSync(path, "utf8"));
333
382
  } catch {
334
383
  return {};
335
384
  }
336
385
  }
337
- function writeJsonAtomic(path, data) {
386
+ function writeJsonAtomic$1(path, data) {
338
387
  mkdirSync(dirname(path), { recursive: true });
339
388
  const tmp = join(mkdtempSync(join(tmpdir(), "membank-")), "cfg.json");
340
389
  writeFileSync(tmp, JSON.stringify(data, null, 2));
@@ -343,66 +392,237 @@ function writeJsonAtomic(path, data) {
343
392
  function hasKey(container, key) {
344
393
  return container !== null && typeof container === "object" && key in container;
345
394
  }
346
- const writers = {
347
- "claude-code": {
348
- configPath: (r) => join(r.home(), ".claude", "settings.json"),
349
- isConfigured: (cfg) => hasKey(cfg["mcpServers"], "membank"),
350
- merge: (cfg) => ({
395
+ function assertCliFound(result, cli) {
396
+ if (result.exitCode === 127) throw new Error(`${cli} CLI not found — install ${cli} first`);
397
+ }
398
+ const MEMBANK_NPX_ARGS = [
399
+ "npx",
400
+ "@membank/cli@latest",
401
+ "--mcp"
402
+ ];
403
+ const writers$1 = {
404
+ "claude-code": { async write(resolver, run, { overwrite = false } = {}) {
405
+ const configured = hasKey(readJson$1(join(resolver.home(), ".claude.json")).mcpServers, "membank");
406
+ if (configured && !overwrite) return { status: "already-configured" };
407
+ if (configured) {
408
+ const remove = await run("claude", [
409
+ "mcp",
410
+ "remove",
411
+ "--scope",
412
+ "user",
413
+ "membank"
414
+ ]);
415
+ assertCliFound(remove, "claude");
416
+ if (remove.exitCode !== 0) throw new Error(`claude mcp remove failed: ${remove.stderr}`);
417
+ }
418
+ const add = await run("claude", [
419
+ "mcp",
420
+ "add",
421
+ "--scope",
422
+ "user",
423
+ "membank",
424
+ "--",
425
+ ...MEMBANK_NPX_ARGS
426
+ ]);
427
+ assertCliFound(add, "claude");
428
+ if (add.exitCode !== 0) throw new Error(`claude mcp add failed: ${add.stderr || add.stdout}`);
429
+ return { status: "written" };
430
+ } },
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, {
351
436
  ...cfg,
352
437
  mcpServers: {
353
- ...cfg["mcpServers"],
354
- membank: MEMBANK_ENTRY
438
+ ...cfg.mcpServers,
439
+ membank: {
440
+ command: "npx",
441
+ args: ["@membank/cli@latest", "--mcp"]
442
+ }
355
443
  }
356
- })
357
- },
358
- vscode: {
359
- configPath: (r) => join(r.cwd(), ".vscode", "mcp.json"),
360
- isConfigured: (cfg) => hasKey(cfg["servers"], "membank"),
361
- merge: (cfg) => ({
444
+ });
445
+ return { status: "written" };
446
+ } },
447
+ codex: { async write(_resolver, run, { overwrite = false } = {}) {
448
+ const list = await run("codex", ["mcp", "list"]);
449
+ assertCliFound(list, "codex");
450
+ const configured = list.exitCode === 0 && list.stdout.includes("membank");
451
+ if (configured && !overwrite) return { status: "already-configured" };
452
+ if (configured) {
453
+ const remove = await run("codex", [
454
+ "mcp",
455
+ "remove",
456
+ "membank"
457
+ ]);
458
+ assertCliFound(remove, "codex");
459
+ if (remove.exitCode !== 0) throw new Error(`codex mcp remove failed: ${remove.stderr}`);
460
+ }
461
+ const add = await run("codex", [
462
+ "mcp",
463
+ "add",
464
+ "membank",
465
+ "--",
466
+ ...MEMBANK_NPX_ARGS
467
+ ]);
468
+ assertCliFound(add, "codex");
469
+ if (add.exitCode !== 0) throw new Error(`codex mcp add failed: ${add.stderr || add.stdout}`);
470
+ return { status: "written" };
471
+ } },
472
+ opencode: { async write(resolver, _run, { overwrite = false } = {}) {
473
+ const cfgPath = join(resolver.home(), ".config", "opencode", "opencode.json");
474
+ const cfg = readJson$1(cfgPath);
475
+ if (hasKey(cfg.mcp, "membank") && !overwrite) return { status: "already-configured" };
476
+ writeJsonAtomic$1(cfgPath, {
362
477
  ...cfg,
363
- servers: {
364
- ...cfg["servers"],
365
- membank: MEMBANK_ENTRY
478
+ mcp: {
479
+ ...cfg.mcp,
480
+ membank: {
481
+ type: "local",
482
+ command: [
483
+ "npx",
484
+ "@membank/cli@latest",
485
+ "--mcp"
486
+ ]
487
+ }
366
488
  }
367
- })
368
- },
369
- codex: {
370
- configPath: (r) => join(r.home(), ".codex", "config.json"),
371
- isConfigured: (cfg) => hasKey(cfg["mcpServers"], "membank"),
372
- merge: (cfg) => ({
489
+ });
490
+ return { status: "written" };
491
+ } }
492
+ };
493
+ const SUPPORTED_HARNESSES = Object.keys(writers$1);
494
+ var HarnessConfigWriter = class {
495
+ #resolver;
496
+ #run;
497
+ constructor(resolver = defaultPathResolver$1, run = execFileNoThrow) {
498
+ this.#resolver = resolver;
499
+ this.#run = run;
500
+ }
501
+ async write(harness, { overwrite = false } = {}) {
502
+ const writer = writers$1[harness];
503
+ if (!writer) throw new Error(`Unknown harness: ${harness}`);
504
+ return writer.write(this.#resolver, this.#run, { overwrite });
505
+ }
506
+ };
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, {
373
540
  ...cfg,
374
- mcpServers: {
375
- ...cfg["mcpServers"],
376
- membank: MEMBANK_ENTRY
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
+ }]
377
550
  }
378
- })
379
- },
380
- opencode: {
381
- configPath: (r) => join(r.home(), ".config", "opencode", "config.json"),
382
- isConfigured: (cfg) => hasKey(cfg["mcp"], "membank"),
383
- merge: (cfg) => ({
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,
384
563
  ...cfg,
385
- mcp: {
386
- ...cfg["mcp"],
387
- membank: MEMBANK_ENTRY
564
+ hooks: {
565
+ ...hooks ?? {},
566
+ sessionStart: [...existingSessionStart, {
567
+ type: "command",
568
+ bash: "npx @membank/cli inject --harness copilot-cli",
569
+ timeoutSec: 30
570
+ }]
388
571
  }
389
- })
390
- }
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
+ } }
391
616
  };
392
- const SUPPORTED_HARNESSES = Object.keys(writers);
393
- var HarnessConfigWriter = class {
617
+ var InjectionHookWriter = class {
394
618
  #resolver;
395
619
  constructor(resolver = defaultPathResolver) {
396
620
  this.#resolver = resolver;
397
621
  }
398
- write(harness, { overwrite = false } = {}) {
622
+ write(harness) {
399
623
  const writer = writers[harness];
400
- if (!writer) throw new Error(`Unknown harness: ${harness}`);
401
- const path = writer.configPath(this.#resolver);
402
- const existing = readJson(path);
403
- if (!overwrite && writer.isConfigured(existing)) return { status: "already-configured" };
404
- writeJsonAtomic(path, writer.merge(existing));
405
- return { status: "written" };
624
+ if (!writer) return { status: "not-supported" };
625
+ return writer.write(this.#resolver);
406
626
  }
407
627
  };
408
628
  //#endregion
@@ -471,34 +691,33 @@ var ModelDownloader = class extends EventEmitter {
471
691
  };
472
692
  //#endregion
473
693
  //#region src/setup/harness-detector.ts
474
- const defaultResolver = {
475
- homeDir: homedir,
476
- cwd: () => process.cwd()
477
- };
694
+ const defaultResolver = { homeDir: homedir };
478
695
  function harnessConfigs(resolver) {
479
696
  const home = resolver.homeDir();
480
- const cwd = resolver.cwd();
481
697
  return [
482
698
  {
483
699
  name: "claude-code",
484
- configPath: join(home, ".claude", "settings.json")
700
+ configPath: join(home, ".claude.json"),
701
+ fallbackPaths: [join(home, ".claude", "settings.json")]
485
702
  },
486
703
  {
487
- name: "vscode",
488
- configPath: join(cwd, ".vscode", "mcp.json")
704
+ name: "copilot",
705
+ configPath: join(home, ".copilot", "mcp-config.json")
489
706
  },
490
707
  {
491
708
  name: "codex",
492
- configPath: join(home, ".codex", "config.json")
709
+ configPath: join(home, ".codex", "config.toml"),
710
+ fallbackPaths: [join(home, ".codex", "config.json")]
493
711
  },
494
712
  {
495
713
  name: "opencode",
496
- configPath: join(home, ".config", "opencode", "config.json")
714
+ configPath: join(home, ".config", "opencode", "opencode.json"),
715
+ fallbackPaths: [join(home, ".config", "opencode", "config.json")]
497
716
  }
498
717
  ];
499
718
  }
500
719
  function detectHarnesses(resolver = defaultResolver) {
501
- return harnessConfigs(resolver).filter((h) => existsSync(h.configPath)).map((h) => ({
720
+ return harnessConfigs(resolver).filter((h) => existsSync(h.configPath) || (h.fallbackPaths?.some((p) => existsSync(p)) ?? false)).map((h) => ({
502
721
  name: h.name,
503
722
  configPath: h.configPath
504
723
  }));
@@ -525,6 +744,7 @@ function defaultPrompter(question) {
525
744
  var SetupOrchestrator = class {
526
745
  #detector;
527
746
  #writer;
747
+ #hookWriter;
528
748
  #prompter;
529
749
  #modelDownloader;
530
750
  #out;
@@ -532,6 +752,7 @@ var SetupOrchestrator = class {
532
752
  constructor(deps) {
533
753
  this.#detector = deps.detector ?? (() => detectHarnesses());
534
754
  this.#writer = deps.writer;
755
+ this.#hookWriter = deps.hookWriter;
535
756
  this.#prompter = deps.prompter ?? defaultPrompter;
536
757
  this.#modelDownloader = deps.modelDownloader;
537
758
  this.#out = deps.out ?? ((msg) => process.stdout.write(`${msg}\n`));
@@ -553,6 +774,7 @@ var SetupOrchestrator = class {
553
774
  if (json) this.#out(JSON.stringify({
554
775
  detectedHarnesses: [],
555
776
  configuredHarnesses: [],
777
+ injectionHooksConfigured: [],
556
778
  modelDownloaded: false
557
779
  }));
558
780
  return [];
@@ -564,7 +786,10 @@ var SetupOrchestrator = class {
564
786
  }
565
787
  if (dryRun) {
566
788
  out("Planned changes (dry-run — no files written):");
567
- 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
+ }
568
793
  out("");
569
794
  out(" ⚠ Model download: skipped (dry-run)");
570
795
  return detected.map((h) => ({
@@ -582,7 +807,7 @@ var SetupOrchestrator = class {
582
807
  for (const h of detected) {
583
808
  let writeResult;
584
809
  try {
585
- writeResult = this.#writer.write(h.name);
810
+ writeResult = await this.#writer.write(h.name);
586
811
  } catch (err) {
587
812
  const msg = err instanceof Error ? err.message : String(err);
588
813
  out(` ✗ ${h.name}: ${msg}`);
@@ -605,7 +830,7 @@ var SetupOrchestrator = class {
605
830
  continue;
606
831
  }
607
832
  try {
608
- this.#writer.write(h.name, { overwrite: true });
833
+ await this.#writer.write(h.name, { overwrite: true });
609
834
  out(` ✓ ${h.name}: written (overwritten)`);
610
835
  results.push({
611
836
  harness: h.name,
@@ -629,6 +854,21 @@ var SetupOrchestrator = class {
629
854
  });
630
855
  }
631
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
+ }
632
872
  let modelDownloaded = false;
633
873
  if (this.#modelDownloader) modelDownloaded = !(await this.#runModelDownload(this.#modelDownloader, out)).skipped;
634
874
  else out("Model download step: see DRA-52");
@@ -639,6 +879,7 @@ var SetupOrchestrator = class {
639
879
  const output = {
640
880
  detectedHarnesses: detected.map((h) => h.name),
641
881
  configuredHarnesses: results.filter((r) => r.status === "written").map((r) => r.harness),
882
+ injectionHooksConfigured,
642
883
  modelDownloaded
643
884
  };
644
885
  this.#out(JSON.stringify(output));
@@ -679,7 +920,7 @@ const program = new Command();
679
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)");
680
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) => {
681
922
  const globalOpts = program.opts();
682
- const formatter = Formatter.create().withJson(globalOpts.json === true || !process.stdout.isTTY);
923
+ const formatter = Formatter.create(globalOpts.json === true);
683
924
  try {
684
925
  await queryCommand(queryText, cmdOptions, formatter);
685
926
  } catch (err) {
@@ -689,7 +930,7 @@ program.command("query <queryText>").description("search memories by semantic si
689
930
  });
690
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) => {
691
932
  const globalOpts = program.opts();
692
- const formatter = Formatter.create().withJson(globalOpts.json === true || !process.stdout.isTTY);
933
+ const formatter = Formatter.create(globalOpts.json === true);
693
934
  try {
694
935
  await listCommand(cmdOptions, formatter);
695
936
  } catch (err) {
@@ -699,7 +940,7 @@ program.command("list").description("list memories with optional filters").optio
699
940
  });
700
941
  program.command("stats").description("show memory counts by type, total, and needs_review").action(async () => {
701
942
  const globalOpts = program.opts();
702
- const formatter = Formatter.create().withJson(globalOpts.json === true || !process.stdout.isTTY);
943
+ const formatter = Formatter.create(globalOpts.json === true);
703
944
  try {
704
945
  await statsCommand(formatter);
705
946
  } catch (err) {
@@ -709,7 +950,7 @@ program.command("stats").description("show memory counts by type, total, and nee
709
950
  });
710
951
  program.command("delete <id>").description("delete a memory by ID").action(async (id) => {
711
952
  const globalOpts = program.opts();
712
- const formatter = Formatter.create().withJson(globalOpts.json === true || !process.stdout.isTTY);
953
+ const formatter = Formatter.create(globalOpts.json === true);
713
954
  const prompt = new PromptHelper(globalOpts.yes === true || !process.stdout.isTTY);
714
955
  const db = DatabaseManager.open();
715
956
  try {
@@ -723,7 +964,7 @@ program.command("delete <id>").description("delete a memory by ID").action(async
723
964
  });
724
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) => {
725
966
  const globalOpts = program.opts();
726
- const formatter = Formatter.create().withJson(globalOpts.json === true || !process.stdout.isTTY);
967
+ const formatter = Formatter.create(globalOpts.json === true);
727
968
  try {
728
969
  await addCommand(content, cmdOptions, formatter);
729
970
  } catch (err) {
@@ -733,7 +974,7 @@ program.command("add <content>").description("save a new memory").requiredOption
733
974
  });
734
975
  program.command("pin <id>").description("pin a memory by ID").action((id) => {
735
976
  const globalOpts = program.opts();
736
- const formatter = Formatter.create().withJson(globalOpts.json === true || !process.stdout.isTTY);
977
+ const formatter = Formatter.create(globalOpts.json === true);
737
978
  try {
738
979
  pinCommand(id, formatter);
739
980
  } catch (err) {
@@ -743,7 +984,7 @@ program.command("pin <id>").description("pin a memory by ID").action((id) => {
743
984
  });
744
985
  program.command("unpin <id>").description("unpin a memory by ID").action((id) => {
745
986
  const globalOpts = program.opts();
746
- const formatter = Formatter.create().withJson(globalOpts.json === true || !process.stdout.isTTY);
987
+ const formatter = Formatter.create(globalOpts.json === true);
747
988
  try {
748
989
  unpinCommand(id, formatter);
749
990
  } catch (err) {
@@ -753,7 +994,7 @@ program.command("unpin <id>").description("unpin a memory by ID").action((id) =>
753
994
  });
754
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) => {
755
996
  const globalOpts = program.opts();
756
- const formatter = Formatter.create().withJson(globalOpts.json === true || !process.stdout.isTTY);
997
+ const formatter = Formatter.create(globalOpts.json === true);
757
998
  const db = DatabaseManager.open();
758
999
  try {
759
1000
  exportCommand(db, formatter, cmdOptions);
@@ -766,7 +1007,7 @@ program.command("export").description("export all memories to a JSON file").opti
766
1007
  });
767
1008
  program.command("import <file>").description("import memories from a JSON export file").action(async (file) => {
768
1009
  const globalOpts = program.opts();
769
- const formatter = Formatter.create().withJson(globalOpts.json === true || !process.stdout.isTTY);
1010
+ const formatter = Formatter.create(globalOpts.json === true);
770
1011
  const prompt = new PromptHelper(globalOpts.yes === true || !process.stdout.isTTY);
771
1012
  const db = DatabaseManager.open();
772
1013
  try {
@@ -778,21 +1019,30 @@ program.command("import <file>").description("import memories from a JSON export
778
1019
  db.close();
779
1020
  }
780
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
+ });
781
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) => {
782
1031
  const globalOpts = program.opts();
783
1032
  const autoYes = cmdOptions.yes === true || globalOpts.yes === true;
784
- const useJson = globalOpts.json === true || !process.stdout.isTTY;
785
- const formatter = Formatter.create().withJson(useJson);
1033
+ const formatter = Formatter.create(globalOpts.json === true);
786
1034
  if (cmdOptions.harness !== void 0) {
787
- if (!SUPPORTED_HARNESSES.includes(cmdOptions.harness)) {
1035
+ if (!SUPPORTED_HARNESSES.some((h) => h === cmdOptions.harness)) {
788
1036
  formatter.error(`Unknown harness: "${cmdOptions.harness}". Supported: ${SUPPORTED_HARNESSES.join(", ")}`);
789
1037
  process.exit(1);
790
1038
  }
791
1039
  }
792
1040
  const writer = new HarnessConfigWriter();
1041
+ const hookWriter = new InjectionHookWriter();
793
1042
  const promptHelper = new PromptHelper(autoYes);
794
1043
  const orchestrator = new SetupOrchestrator({
795
1044
  writer,
1045
+ hookWriter,
796
1046
  prompter: (question) => promptHelper.confirm(question),
797
1047
  modelDownloader: new ModelDownloader()
798
1048
  });
@@ -801,7 +1051,7 @@ program.command("setup").description("detect installed harnesses and write MCP c
801
1051
  yes: autoYes,
802
1052
  dryRun: cmdOptions.dryRun,
803
1053
  harness: cmdOptions.harness,
804
- json: useJson
1054
+ json: formatter.isJson
805
1055
  })).some((r) => r.status === "error")) process.exit(1);
806
1056
  } catch (err) {
807
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.2",
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.2",
21
- "@membank/mcp": "0.0.2"
20
+ "@membank/mcp": "0.0.4",
21
+ "@membank/core": "0.0.4"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/node": "^25.6.0",