@membank/cli 0.4.1 → 0.5.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 +339 -144
  2. package/package.json +7 -4
package/dist/index.mjs CHANGED
@@ -1,17 +1,20 @@
1
1
  #!/usr/bin/env node
2
+ import { cancel, confirm, intro, isCancel, multiselect, note, outro } from "@clack/prompts";
2
3
  import { DatabaseManager, EmbeddingService, MEMORY_TYPE_VALUES, MemoryRepository, QueryEngine, SessionContextBuilder, resolveScope } from "@membank/core";
3
4
  import { startServer } from "@membank/mcp";
5
+ import chalk from "chalk";
4
6
  import { Command } from "commander";
7
+ import ora from "ora";
5
8
  import { startDashboard } from "@membank/dashboard";
6
9
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
7
10
  import { dirname, join } from "node:path";
8
- import * as readline from "node:readline";
9
- import { createInterface } from "node:readline";
11
+ import Table from "cli-table3";
10
12
  import { homedir, tmpdir } from "node:os";
11
13
  import { execFile } from "node:child_process";
12
14
  import { promisify } from "node:util";
13
15
  import { EventEmitter } from "node:events";
14
16
  import { pipeline } from "@huggingface/transformers";
17
+ import { createInterface } from "node:readline";
15
18
  //#region src/commands/add.ts
16
19
  async function addCommand(content, options, formatter, db, embeddingService) {
17
20
  const ownDb = db === void 0;
@@ -19,12 +22,14 @@ async function addCommand(content, options, formatter, db, embeddingService) {
19
22
  try {
20
23
  const repo = new MemoryRepository(resolvedDb, embeddingService ?? new EmbeddingService());
21
24
  const tags = options.tags !== void 0 ? options.tags.split(",").map((t) => t.trim()) : [];
25
+ const spinner = formatter.isJson ? null : ora("Saving memory…").start();
22
26
  const memory = await repo.save({
23
27
  content,
24
28
  type: options.type,
25
29
  tags,
26
30
  scope: options.scope
27
31
  });
32
+ spinner?.succeed("Memory saved");
28
33
  formatter.outputMemory(memory);
29
34
  } finally {
30
35
  if (ownDb) resolvedDb.close();
@@ -44,7 +49,7 @@ async function deleteCommand(id, db, formatter, prompt) {
44
49
  }
45
50
  if (!await prompt.confirm(`Delete memory ${id}?`)) return;
46
51
  await new MemoryRepository(db, new EmbeddingService()).delete(id);
47
- process.stdout.write(`Deleted memory: ${id}\n`);
52
+ process.stdout.write(`${chalk.green("✓")} Deleted memory: ${chalk.dim(id)}\n`);
48
53
  }
49
54
  //#endregion
50
55
  //#region src/commands/export.ts
@@ -134,7 +139,7 @@ async function importCommand(filePath, db, formatter, prompt) {
134
139
  }
135
140
  //#endregion
136
141
  //#region src/commands/inject.ts
137
- const MEMORY_GUIDANCE = "[Memory Guidance]: Persistent memory is available via query_memory, save_memory, update_memory, delete_memory. Skipping save_memory when the user gives a correction or preference means they have to repeat themselves next session that is the failure mode to avoid. Skipping query_memory on topics that touch prior decisions means contradicting yourself. Default to saving (type: correction|preference|decision|learning|fact) when in doubt; rely on dedup to handle redundancy. Pin anything that should appear at every session start.";
142
+ const MEMORY_GUIDANCE = "[Memory Guidance]: Call save_memory when ANY of these happen: (1) user states a preference or makes a decision; (2) user corrects you; (3) you discover a working fix after a tool error; (4) you learn a non-obvious project fact. Type correction|preference|decision|learning|fact. Call query_memory before answering anything that might touch prior decisions. When unsure, save.";
138
143
  function formatContext(ctx) {
139
144
  const lines = [];
140
145
  const statParts = Object.entries(ctx.stats).filter(([, count]) => count > 0).map(([type, count]) => `${count} ${type}${count !== 1 ? "s" : ""}`);
@@ -202,7 +207,7 @@ function pinCommand(id, formatter, db) {
202
207
  process.exit(2);
203
208
  } else {
204
209
  resolvedDb.db.prepare("UPDATE memories SET pinned = 1 WHERE id = ?").run(id);
205
- process.stdout.write(`Pinned: ${id}\n`);
210
+ process.stdout.write(`${chalk.green("✓")} Pinned: ${chalk.dim(id)}\n`);
206
211
  }
207
212
  } finally {
208
213
  if (ownDb) resolvedDb.close();
@@ -216,11 +221,13 @@ async function queryCommand(queryText, options, formatter) {
216
221
  const embedding = new EmbeddingService();
217
222
  const engine = new QueryEngine(db, embedding, new MemoryRepository(db, embedding));
218
223
  const limit = options.limit !== void 0 ? Number.parseInt(options.limit, 10) : 10;
224
+ const spinner = formatter.isJson ? null : ora("Searching memories…").start();
219
225
  const results = await engine.query({
220
226
  query: queryText,
221
227
  type: options.type,
222
228
  limit
223
229
  });
230
+ spinner?.succeed(`${results.length} result${results.length === 1 ? "" : "s"} found`);
224
231
  formatter.outputQueryResults(results);
225
232
  } finally {
226
233
  db.close();
@@ -248,7 +255,7 @@ function unpinCommand(id, formatter, db) {
248
255
  process.exit(2);
249
256
  } else {
250
257
  resolvedDb.db.prepare("UPDATE memories SET pinned = 0 WHERE id = ?").run(id);
251
- process.stdout.write(`Unpinned: ${id}\n`);
258
+ process.stdout.write(`${chalk.green("✓")} Unpinned: ${chalk.dim(id)}\n`);
252
259
  }
253
260
  } finally {
254
261
  if (ownDb) resolvedDb.close();
@@ -256,6 +263,19 @@ function unpinCommand(id, formatter, db) {
256
263
  }
257
264
  //#endregion
258
265
  //#region src/formatter.ts
266
+ const TYPE_COLORS = {
267
+ correction: chalk.yellow,
268
+ preference: chalk.cyan,
269
+ decision: chalk.blue,
270
+ learning: chalk.green,
271
+ fact: chalk.dim
272
+ };
273
+ function colorType(type) {
274
+ return TYPE_COLORS[type](type);
275
+ }
276
+ function truncate(str, max) {
277
+ return str.length > max ? `${str.slice(0, max - 1)}…` : str;
278
+ }
259
279
  var Formatter = class Formatter {
260
280
  #isJson;
261
281
  constructor(isJson) {
@@ -272,7 +292,12 @@ var Formatter = class Formatter {
272
292
  process.stdout.write(`${JSON.stringify(memory)}\n`);
273
293
  return;
274
294
  }
275
- this.#writeMemoryBlock(memory, ` Pinned : ${memory.pinned}\n`);
295
+ const tags = memory.tags.length > 0 ? memory.tags.join(", ") : "(none)";
296
+ process.stdout.write("\n");
297
+ process.stdout.write(` ${colorType(memory.type)} ${chalk.dim(memory.id)}\n`);
298
+ process.stdout.write(` ${memory.content}\n`);
299
+ process.stdout.write(` ${chalk.dim("Tags:")} ${tags} ${chalk.dim("Scope:")} ${memory.scope}\n`);
300
+ process.stdout.write(`\n ${chalk.dim(`Hint: pin with membank pin ${memory.id}`)}\n\n`);
276
301
  }
277
302
  outputMemories(memories) {
278
303
  if (this.#isJson) {
@@ -280,26 +305,51 @@ var Formatter = class Formatter {
280
305
  return;
281
306
  }
282
307
  if (memories.length === 0) {
283
- process.stdout.write("No memories found.\n");
308
+ process.stdout.write(`${chalk.dim("No memories found.")}\n`);
284
309
  return;
285
310
  }
286
- for (const memory of memories) this.#writeMemoryBlock(memory);
287
- process.stdout.write("\n");
311
+ const table = new Table({
312
+ head: [
313
+ "Type",
314
+ "ID",
315
+ "Content",
316
+ "Pinned"
317
+ ].map((h) => chalk.bold(h)),
318
+ style: {
319
+ head: [],
320
+ border: []
321
+ }
322
+ });
323
+ for (const m of memories) {
324
+ const tags = m.tags.length > 0 ? m.tags.join(", ") : "(none)";
325
+ const meta = `${truncate(m.content, 45)}\n${chalk.dim(`${tags} · ${m.scope}`)}`;
326
+ table.push([
327
+ colorType(m.type),
328
+ chalk.dim(m.id),
329
+ meta,
330
+ m.pinned ? "📌" : ""
331
+ ]);
332
+ }
333
+ process.stdout.write(`\n${table.toString()}\n\n`);
288
334
  }
289
335
  outputStats(stats) {
290
336
  if (this.#isJson) {
291
337
  process.stdout.write(`${JSON.stringify(stats)}\n`);
292
338
  return;
293
339
  }
294
- for (const type of [
340
+ const types = [
295
341
  "correction",
296
342
  "preference",
297
343
  "decision",
298
344
  "learning",
299
345
  "fact"
300
- ]) process.stdout.write(` ${type.padEnd(12)}: ${stats.byType[type]}\n`);
301
- process.stdout.write(`\n total : ${stats.total}\n`);
302
- process.stdout.write(` needs_review: ${stats.needsReview}\n`);
346
+ ];
347
+ process.stdout.write("\n");
348
+ for (const type of types) process.stdout.write(` ${TYPE_COLORS[type](type.padEnd(14))} ${stats.byType[type]}\n`);
349
+ process.stdout.write(`\n ${chalk.dim("─".repeat(24))}\n`);
350
+ process.stdout.write(` ${"total".padEnd(14)} ${stats.total}\n`);
351
+ if (stats.needsReview > 0) process.stdout.write(` ${chalk.yellow("⚠")} ${"needs_review".padEnd(12)} ${stats.needsReview}\n\n`);
352
+ else process.stdout.write(` ${" needs_review".padEnd(14)} ${stats.needsReview}\n\n`);
303
353
  }
304
354
  outputQueryResults(results) {
305
355
  if (this.#isJson) {
@@ -307,24 +357,38 @@ var Formatter = class Formatter {
307
357
  return;
308
358
  }
309
359
  if (results.length === 0) {
310
- process.stdout.write("No memories found.\n");
360
+ process.stdout.write(`${chalk.dim("No memories found.")}\n`);
311
361
  return;
312
362
  }
313
- for (const result of results) this.#writeMemoryBlock(result, ` Score : ${result.score.toFixed(4)}\n`);
314
- process.stdout.write("\n");
363
+ const table = new Table({
364
+ head: [
365
+ "Type",
366
+ "ID",
367
+ "Content",
368
+ "Score"
369
+ ].map((h) => chalk.bold(h)),
370
+ style: {
371
+ head: [],
372
+ border: []
373
+ }
374
+ });
375
+ for (const r of results) {
376
+ const scoreStr = r.score.toFixed(4);
377
+ const score = r.score >= .85 ? chalk.bold(scoreStr) : r.score < .75 ? chalk.dim(scoreStr) : scoreStr;
378
+ const tags = r.tags.length > 0 ? r.tags.join(", ") : "(none)";
379
+ const meta = `${truncate(r.content, 45)}\n${chalk.dim(`${tags} · ${r.scope}`)}`;
380
+ table.push([
381
+ colorType(r.type),
382
+ chalk.dim(r.id),
383
+ meta,
384
+ score
385
+ ]);
386
+ }
387
+ process.stdout.write(`\n${table.toString()}\n\n`);
315
388
  }
316
389
  error(msg) {
317
390
  if (this.#isJson) process.stderr.write(`${JSON.stringify({ error: msg })}\n`);
318
- else process.stderr.write(`Error: ${msg}\n`);
319
- }
320
- #writeMemoryBlock(memory, extra) {
321
- const tags = memory.tags.length > 0 ? memory.tags.join(", ") : "(none)";
322
- process.stdout.write(`\n[${memory.type}] ${memory.id}\n`);
323
- process.stdout.write(` Content : ${memory.content}\n`);
324
- process.stdout.write(` Tags : ${tags}\n`);
325
- process.stdout.write(` Scope : ${memory.scope}\n`);
326
- if (extra !== void 0) process.stdout.write(extra);
327
- process.stdout.write("\n");
391
+ else process.stderr.write(`${chalk.red("Error:")} ${msg}\n`);
328
392
  }
329
393
  };
330
394
  //#endregion
@@ -333,18 +397,11 @@ var PromptHelper = class {
333
397
  constructor(autoConfirm) {
334
398
  this.autoConfirm = autoConfirm;
335
399
  }
336
- confirm(message) {
337
- if (this.autoConfirm) return Promise.resolve(true);
338
- return new Promise((resolve) => {
339
- const rl = readline.createInterface({
340
- input: process.stdin,
341
- output: process.stdout
342
- });
343
- rl.question(`${message} [y/N] `, (answer) => {
344
- rl.close();
345
- resolve(answer.trim().toLowerCase() === "y");
346
- });
347
- });
400
+ async confirm(message) {
401
+ if (this.autoConfirm) return true;
402
+ const result = await confirm({ message });
403
+ if (typeof result === "symbol") return false;
404
+ return result;
348
405
  }
349
406
  };
350
407
  //#endregion
@@ -384,6 +441,14 @@ async function execFileNoThrow(cmd, args) {
384
441
  }
385
442
  //#endregion
386
443
  //#region src/setup/harness-config-writer.ts
444
+ var CommandError = class extends Error {
445
+ command;
446
+ constructor(message, command) {
447
+ super(message);
448
+ this.name = "CommandError";
449
+ this.command = command;
450
+ }
451
+ };
387
452
  const defaultPathResolver$1 = {
388
453
  home: () => {
389
454
  const h = process.env.HOME ?? process.env.USERPROFILE;
@@ -408,109 +473,150 @@ function writeJsonAtomic$1(path, data) {
408
473
  function hasKey(container, key) {
409
474
  return container !== null && typeof container === "object" && key in container;
410
475
  }
411
- function assertCliFound(result, cli) {
412
- if (result.exitCode === 127) throw new Error(`${cli} CLI not found — install ${cli} first`);
476
+ function assertCliFound(result, cli, command) {
477
+ if (result.exitCode === 127) throw new CommandError(`${cli} CLI not found — install ${cli} first`, command);
413
478
  }
414
479
  const MEMBANK_NPX_ARGS = [
415
480
  "npx",
416
481
  "-y",
417
- "@membank/cli@latest",
482
+ "@membank/cli",
418
483
  "--mcp"
419
484
  ];
420
485
  const writers$1 = {
421
- "claude-code": { async write(resolver, run, { overwrite = false } = {}) {
422
- const configured = hasKey(readJson$1(join(resolver.home(), ".claude.json")).mcpServers, "membank");
423
- if (configured && !overwrite) return { status: "already-configured" };
424
- if (configured) {
425
- const remove = await run("claude", [
486
+ "claude-code": {
487
+ preview(resolver) {
488
+ return {
489
+ configPath: join(resolver.home(), ".claude.json"),
490
+ cliCommand: "claude mcp add --scope user membank -- npx -y @membank/cli --mcp"
491
+ };
492
+ },
493
+ async write(resolver, run, { overwrite = false } = {}) {
494
+ const configured = hasKey(readJson$1(join(resolver.home(), ".claude.json")).mcpServers, "membank");
495
+ if (configured && !overwrite) return { status: "already-configured" };
496
+ if (configured) {
497
+ const removeArgs = [
498
+ "mcp",
499
+ "remove",
500
+ "--scope",
501
+ "user",
502
+ "membank"
503
+ ];
504
+ const removeCmd = `claude ${removeArgs.join(" ")}`;
505
+ const remove = await run("claude", [...removeArgs]);
506
+ assertCliFound(remove, "claude", removeCmd);
507
+ if (remove.exitCode !== 0) throw new CommandError(`claude mcp remove failed: ${remove.stderr}`, removeCmd);
508
+ }
509
+ const addArgs = [
426
510
  "mcp",
427
- "remove",
511
+ "add",
428
512
  "--scope",
429
513
  "user",
430
- "membank"
431
- ]);
432
- assertCliFound(remove, "claude");
433
- if (remove.exitCode !== 0) throw new Error(`claude mcp remove failed: ${remove.stderr}`);
514
+ "membank",
515
+ "--",
516
+ ...MEMBANK_NPX_ARGS
517
+ ];
518
+ const addCmd = `claude ${addArgs.join(" ")}`;
519
+ const add = await run("claude", [...addArgs]);
520
+ assertCliFound(add, "claude", addCmd);
521
+ if (add.exitCode !== 0) throw new CommandError(`claude mcp add failed: ${add.stderr || add.stdout}`, addCmd);
522
+ return { status: "written" };
434
523
  }
435
- const add = await run("claude", [
436
- "mcp",
437
- "add",
438
- "--scope",
439
- "user",
440
- "membank",
441
- "--",
442
- ...MEMBANK_NPX_ARGS
443
- ]);
444
- assertCliFound(add, "claude");
445
- if (add.exitCode !== 0) throw new Error(`claude mcp add failed: ${add.stderr || add.stdout}`);
446
- return { status: "written" };
447
- } },
448
- copilot: { async write(resolver, _run, { overwrite = false } = {}) {
449
- const cfgPath = join(resolver.home(), ".copilot", "mcp-config.json");
450
- const cfg = readJson$1(cfgPath);
451
- if (hasKey(cfg.mcpServers, "membank") && !overwrite) return { status: "already-configured" };
452
- writeJsonAtomic$1(cfgPath, {
453
- ...cfg,
454
- mcpServers: {
455
- ...cfg.mcpServers,
456
- membank: {
457
- command: "npx",
458
- args: [
459
- "-y",
460
- "@membank/cli@latest",
461
- "--mcp"
462
- ]
524
+ },
525
+ copilot: {
526
+ preview(resolver) {
527
+ return {
528
+ configPath: join(resolver.home(), ".copilot", "mcp-config.json"),
529
+ cliCommand: null
530
+ };
531
+ },
532
+ async write(resolver, _run, { overwrite = false } = {}) {
533
+ const cfgPath = join(resolver.home(), ".copilot", "mcp-config.json");
534
+ const cfg = readJson$1(cfgPath);
535
+ if (hasKey(cfg.mcpServers, "membank") && !overwrite) return { status: "already-configured" };
536
+ writeJsonAtomic$1(cfgPath, {
537
+ ...cfg,
538
+ mcpServers: {
539
+ ...cfg.mcpServers,
540
+ membank: {
541
+ command: "npx",
542
+ args: [
543
+ "-y",
544
+ "@membank/cli",
545
+ "--mcp"
546
+ ]
547
+ }
463
548
  }
549
+ });
550
+ return { status: "written" };
551
+ }
552
+ },
553
+ codex: {
554
+ preview(_resolver) {
555
+ return {
556
+ configPath: null,
557
+ cliCommand: "codex mcp add membank -- npx -y @membank/cli --mcp"
558
+ };
559
+ },
560
+ async write(_resolver, run, { overwrite = false } = {}) {
561
+ const listCmd = "codex mcp list";
562
+ const list = await run("codex", ["mcp", "list"]);
563
+ assertCliFound(list, "codex", listCmd);
564
+ const configured = list.exitCode === 0 && list.stdout.includes("membank");
565
+ if (configured && !overwrite) return { status: "already-configured" };
566
+ if (configured) {
567
+ const removeArgs = [
568
+ "mcp",
569
+ "remove",
570
+ "membank"
571
+ ];
572
+ const removeCmd = `codex ${removeArgs.join(" ")}`;
573
+ const remove = await run("codex", [...removeArgs]);
574
+ assertCliFound(remove, "codex", removeCmd);
575
+ if (remove.exitCode !== 0) throw new CommandError(`codex mcp remove failed: ${remove.stderr}`, removeCmd);
464
576
  }
465
- });
466
- return { status: "written" };
467
- } },
468
- codex: { async write(_resolver, run, { overwrite = false } = {}) {
469
- const list = await run("codex", ["mcp", "list"]);
470
- assertCliFound(list, "codex");
471
- const configured = list.exitCode === 0 && list.stdout.includes("membank");
472
- if (configured && !overwrite) return { status: "already-configured" };
473
- if (configured) {
474
- const remove = await run("codex", [
577
+ const addArgs = [
475
578
  "mcp",
476
- "remove",
477
- "membank"
478
- ]);
479
- assertCliFound(remove, "codex");
480
- if (remove.exitCode !== 0) throw new Error(`codex mcp remove failed: ${remove.stderr}`);
579
+ "add",
580
+ "membank",
581
+ "--",
582
+ ...MEMBANK_NPX_ARGS
583
+ ];
584
+ const addCmd = `codex ${addArgs.join(" ")}`;
585
+ const add = await run("codex", [...addArgs]);
586
+ assertCliFound(add, "codex", addCmd);
587
+ if (add.exitCode !== 0) throw new CommandError(`codex mcp add failed: ${add.stderr || add.stdout}`, addCmd);
588
+ return { status: "written" };
481
589
  }
482
- const add = await run("codex", [
483
- "mcp",
484
- "add",
485
- "membank",
486
- "--",
487
- ...MEMBANK_NPX_ARGS
488
- ]);
489
- assertCliFound(add, "codex");
490
- if (add.exitCode !== 0) throw new Error(`codex mcp add failed: ${add.stderr || add.stdout}`);
491
- return { status: "written" };
492
- } },
493
- opencode: { async write(resolver, _run, { overwrite = false } = {}) {
494
- const cfgPath = join(resolver.home(), ".config", "opencode", "opencode.json");
495
- const cfg = readJson$1(cfgPath);
496
- if (hasKey(cfg.mcp, "membank") && !overwrite) return { status: "already-configured" };
497
- writeJsonAtomic$1(cfgPath, {
498
- ...cfg,
499
- mcp: {
500
- ...cfg.mcp,
501
- membank: {
502
- type: "local",
503
- command: [
504
- "npx",
505
- "-y",
506
- "@membank/cli@latest",
507
- "--mcp"
508
- ]
590
+ },
591
+ opencode: {
592
+ preview(resolver) {
593
+ return {
594
+ configPath: join(resolver.home(), ".config", "opencode", "opencode.json"),
595
+ cliCommand: null
596
+ };
597
+ },
598
+ async write(resolver, _run, { overwrite = false } = {}) {
599
+ const cfgPath = join(resolver.home(), ".config", "opencode", "opencode.json");
600
+ const cfg = readJson$1(cfgPath);
601
+ if (hasKey(cfg.mcp, "membank") && !overwrite) return { status: "already-configured" };
602
+ writeJsonAtomic$1(cfgPath, {
603
+ ...cfg,
604
+ mcp: {
605
+ ...cfg.mcp,
606
+ membank: {
607
+ type: "local",
608
+ command: [
609
+ "npx",
610
+ "-y",
611
+ "@membank/cli",
612
+ "--mcp"
613
+ ]
614
+ }
509
615
  }
510
- }
511
- });
512
- return { status: "written" };
513
- } }
616
+ });
617
+ return { status: "written" };
618
+ }
619
+ }
514
620
  };
515
621
  const SUPPORTED_HARNESSES = Object.keys(writers$1);
516
622
  var HarnessConfigWriter = class {
@@ -520,6 +626,11 @@ var HarnessConfigWriter = class {
520
626
  this.#resolver = resolver;
521
627
  this.#run = run;
522
628
  }
629
+ preview(harness) {
630
+ const writer = writers$1[harness];
631
+ if (!writer) throw new Error(`Unknown harness: ${harness}`);
632
+ return writer.preview(this.#resolver);
633
+ }
523
634
  async write(harness, { overwrite = false } = {}) {
524
635
  const writer = writers$1[harness];
525
636
  if (!writer) throw new Error(`Unknown harness: ${harness}`);
@@ -588,12 +699,14 @@ function pruneFlatEvent(hooks, eventKey) {
588
699
  const writers = {
589
700
  "claude-code": {
590
701
  inspect(resolver) {
591
- const hooks = readJson(join(resolver.home(), ".claude", "settings.json")).hooks ?? {};
702
+ const cfgPath = join(resolver.home(), ".claude", "settings.json");
703
+ const hooks = readJson(cfgPath).hooks ?? {};
592
704
  return {
593
705
  status: "ready",
706
+ configPath: cfgPath,
594
707
  hooks: [{
595
708
  event: "SessionStart",
596
- command: "npx -y @membank/cli@latest inject --harness claude-code",
709
+ command: "npx -y @membank/cli inject --harness claude-code",
597
710
  existingCommand: extractInjectCommand((Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []).flatMap(getHooksArray)) || null
598
711
  }]
599
712
  };
@@ -609,7 +722,7 @@ const writers = {
609
722
  matcher: "",
610
723
  hooks: [{
611
724
  type: "command",
612
- command: "npx -y @membank/cli@latest inject --harness claude-code"
725
+ command: "npx -y @membank/cli inject --harness claude-code"
613
726
  }]
614
727
  }];
615
728
  writeJsonAtomic(cfgPath, {
@@ -621,12 +734,14 @@ const writers = {
621
734
  },
622
735
  "copilot-cli": {
623
736
  inspect(resolver) {
624
- const hooks = readJson(join(resolver.home(), ".copilot", "settings.json")).hooks ?? {};
737
+ const cfgPath = join(resolver.home(), ".copilot", "settings.json");
738
+ const hooks = readJson(cfgPath).hooks ?? {};
625
739
  return {
626
740
  status: "ready",
741
+ configPath: cfgPath,
627
742
  hooks: [{
628
743
  event: "sessionStart",
629
- command: "npx -y @membank/cli@latest inject --harness copilot-cli",
744
+ command: "npx -y @membank/cli inject --harness copilot-cli",
630
745
  existingCommand: extractInjectCommand(Array.isArray(hooks.sessionStart) ? hooks.sessionStart : []) || null
631
746
  }]
632
747
  };
@@ -640,7 +755,7 @@ const writers = {
640
755
  pruneFlatEvent(newHooks, "postToolUseFailure");
641
756
  if (events.includes("sessionStart")) newHooks.sessionStart = [...filterOutMembankFlat(Array.isArray(hooks.sessionStart) ? hooks.sessionStart : []), {
642
757
  type: "command",
643
- bash: "npx -y @membank/cli@latest inject --harness copilot-cli",
758
+ bash: "npx -y @membank/cli inject --harness copilot-cli",
644
759
  timeoutSec: 30
645
760
  }];
646
761
  writeJsonAtomic(cfgPath, {
@@ -653,12 +768,14 @@ const writers = {
653
768
  },
654
769
  codex: {
655
770
  inspect(resolver) {
656
- const hooks = readJson(join(resolver.home(), ".codex", "hooks.json")).hooks ?? {};
771
+ const cfgPath = join(resolver.home(), ".codex", "hooks.json");
772
+ const hooks = readJson(cfgPath).hooks ?? {};
657
773
  return {
658
774
  status: "ready",
775
+ configPath: cfgPath,
659
776
  hooks: [{
660
777
  event: "SessionStart",
661
- command: "npx -y @membank/cli@latest inject --harness codex",
778
+ command: "npx -y @membank/cli inject --harness codex",
662
779
  existingCommand: extractInjectCommand((Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []).flatMap(getHooksArray)) || null
663
780
  }]
664
781
  };
@@ -674,7 +791,7 @@ const writers = {
674
791
  matcher: "",
675
792
  hooks: [{
676
793
  type: "command",
677
- command: "npx -y @membank/cli@latest inject --harness codex",
794
+ command: "npx -y @membank/cli inject --harness codex",
678
795
  timeout: 30
679
796
  }]
680
797
  }];
@@ -694,6 +811,7 @@ const writers = {
694
811
  }
695
812
  return {
696
813
  status: "ready",
814
+ configPath: pluginPath,
697
815
  hooks: [{
698
816
  event: "plugin",
699
817
  command: pluginPath,
@@ -715,7 +833,7 @@ function newOpencodePlugin() {
715
833
  "export default {",
716
834
  " hooks: {",
717
835
  " \"session.start\": async ({ $ }) => {",
718
- " return await $`npx -y @membank/cli@latest inject`.text();",
836
+ " return await $`npx -y @membank/cli inject`.text();",
719
837
  " },",
720
838
  " },",
721
839
  "};"
@@ -763,6 +881,12 @@ var ModelDownloader = class extends EventEmitter {
763
881
  super();
764
882
  this.modelPath = modelPath ?? defaultModelPath();
765
883
  }
884
+ isAlreadyCached() {
885
+ return isCached(this.modelPath);
886
+ }
887
+ get cachePath() {
888
+ return this.modelPath;
889
+ }
766
890
  async download() {
767
891
  if (isCached(this.modelPath)) return { skipped: true };
768
892
  const startTime = Date.now();
@@ -858,6 +982,7 @@ var SetupOrchestrator = class {
858
982
  #writer;
859
983
  #hookWriter;
860
984
  #prompter;
985
+ #harnessSelector;
861
986
  #modelDownloader;
862
987
  #out;
863
988
  #progressWrite;
@@ -866,6 +991,7 @@ var SetupOrchestrator = class {
866
991
  this.#writer = deps.writer;
867
992
  this.#hookWriter = deps.hookWriter;
868
993
  this.#prompter = deps.prompter ?? defaultPrompter;
994
+ this.#harnessSelector = deps.harnessSelector;
869
995
  this.#modelDownloader = deps.modelDownloader;
870
996
  this.#out = deps.out ?? ((msg) => process.stdout.write(`${msg}\n`));
871
997
  this.#progressWrite = deps.progressWrite ?? ((text) => process.stdout.write(text));
@@ -891,6 +1017,13 @@ var SetupOrchestrator = class {
891
1017
  }));
892
1018
  return [];
893
1019
  }
1020
+ if (this.#harnessSelector !== void 0 && !json) {
1021
+ detected = await this.#harnessSelector(detected);
1022
+ if (detected.length === 0) {
1023
+ out("No harnesses selected.");
1024
+ return [];
1025
+ }
1026
+ }
894
1027
  if (!json) {
895
1028
  out("Detected harnesses:");
896
1029
  for (const h of detected) out(` • ${h.name} (${h.configPath})`);
@@ -899,11 +1032,27 @@ var SetupOrchestrator = class {
899
1032
  if (dryRun) {
900
1033
  out("Planned changes (dry-run — no files written):");
901
1034
  for (const h of detected) {
902
- out(` ⚠ ${h.name}: would write MCP config`);
903
- if (this.#hookWriter) out(` ⚠ ${h.name}: would write injection hook config`);
1035
+ const mcpPreview = this.#writer.preview(h.name);
1036
+ const mcpTarget = mcpPreview.configPath ?? (mcpPreview.cliCommand ? "(via CLI)" : "");
1037
+ out(` ⚠ ${h.name} MCP config → ${mcpTarget}`);
1038
+ if (mcpPreview.cliCommand) out(` via: ${mcpPreview.cliCommand}`);
1039
+ if (this.#hookWriter) {
1040
+ const inspected = this.#hookWriter.inspect(h.name);
1041
+ if (inspected.status === "ready") {
1042
+ out(` ⚠ ${h.name} injection hook → ${inspected.configPath}`);
1043
+ for (const hook of inspected.hooks) {
1044
+ out(` event: ${hook.event}`);
1045
+ out(` command: ${hook.command}`);
1046
+ }
1047
+ }
1048
+ }
904
1049
  }
905
1050
  out("");
906
- out(" ⚠ Model download: skipped (dry-run)");
1051
+ if (this.#modelDownloader) {
1052
+ const cached = this.#modelDownloader.isAlreadyCached();
1053
+ out(` ⚠ Model: ${MODEL_NAME} → ${this.#modelDownloader.cachePath}`);
1054
+ out(` status: ${cached ? "already cached — would skip" : "not cached — would download"}`);
1055
+ } else out(" ⚠ Model download: skipped (dry-run)");
907
1056
  return detected.map((h) => ({
908
1057
  harness: h.name,
909
1058
  status: "skipped"
@@ -916,6 +1065,8 @@ var SetupOrchestrator = class {
916
1065
  }
917
1066
  }
918
1067
  const results = [];
1068
+ const totalSteps = this.#hookWriter !== void 0 ? 3 : this.#modelDownloader !== void 0 ? 2 : 1;
1069
+ out(`Step 1/${totalSteps} Writing MCP configs`);
919
1070
  for (const h of detected) {
920
1071
  let writeResult;
921
1072
  try {
@@ -923,6 +1074,7 @@ var SetupOrchestrator = class {
923
1074
  } catch (err) {
924
1075
  const msg = err instanceof Error ? err.message : String(err);
925
1076
  out(` ✗ ${h.name}: ${msg}`);
1077
+ if (err instanceof CommandError) out(` Command: ${err.command}`);
926
1078
  results.push({
927
1079
  harness: h.name,
928
1080
  status: "error",
@@ -951,6 +1103,7 @@ var SetupOrchestrator = class {
951
1103
  } catch (err) {
952
1104
  const msg = err instanceof Error ? err.message : String(err);
953
1105
  out(` ✗ ${h.name}: ${msg}`);
1106
+ if (err instanceof CommandError) out(` Command: ${err.command}`);
954
1107
  results.push({
955
1108
  harness: h.name,
956
1109
  status: "error",
@@ -968,12 +1121,15 @@ var SetupOrchestrator = class {
968
1121
  out("");
969
1122
  const injectionHooksConfigured = [];
970
1123
  if (this.#hookWriter) {
1124
+ out(`Step 2/${totalSteps} Installing injection hooks`);
971
1125
  injectionHooksConfigured.push(...await this.#runHookSetup(detected, yes, out));
972
1126
  out("");
973
1127
  }
974
1128
  let modelDownloaded = false;
975
- if (this.#modelDownloader) modelDownloaded = !(await this.#runModelDownload(this.#modelDownloader, out)).skipped;
976
- else out("Model download step: see DRA-52");
1129
+ if (this.#modelDownloader) {
1130
+ out(`Step ${this.#hookWriter !== void 0 ? 3 : 2}/${totalSteps} Embedding model`);
1131
+ modelDownloaded = !(await this.#runModelDownload(this.#modelDownloader, out)).skipped;
1132
+ } else out("Model download step: see DRA-52");
977
1133
  const written = results.filter((r) => r.status === "written").length;
978
1134
  const skipped = results.filter((r) => r.status === "already-configured").length;
979
1135
  const errors = results.filter((r) => r.status === "error").length;
@@ -994,6 +1150,7 @@ var SetupOrchestrator = class {
994
1150
  async #runHookSetup(detected, yes, out) {
995
1151
  const configured = [];
996
1152
  const w = this.#hookWriter;
1153
+ if (w === void 0) return configured;
997
1154
  for (const h of detected) try {
998
1155
  const inspected = w.inspect(h.name);
999
1156
  if (inspected.status === "not-supported") continue;
@@ -1018,6 +1175,7 @@ var SetupOrchestrator = class {
1018
1175
  } catch (err) {
1019
1176
  const msg = err instanceof Error ? err.message : String(err);
1020
1177
  out(` ✗ ${h.name} injection hooks: ${msg}`);
1178
+ if (err instanceof CommandError) out(` Command: ${err.command}`);
1021
1179
  }
1022
1180
  return configured;
1023
1181
  }
@@ -1163,28 +1321,65 @@ program.command("setup").description("detect installed harnesses and write MCP c
1163
1321
  const globalOpts = program.opts();
1164
1322
  const autoYes = cmdOptions.yes === true || globalOpts.yes === true;
1165
1323
  const formatter = Formatter.create(globalOpts.json === true);
1324
+ const interactive = !formatter.isJson && !autoYes && cmdOptions.harness === void 0;
1166
1325
  if (cmdOptions.harness !== void 0) {
1167
1326
  if (!SUPPORTED_HARNESSES.some((h) => h === cmdOptions.harness)) {
1168
1327
  formatter.error(`Unknown harness: "${cmdOptions.harness}". Supported: ${SUPPORTED_HARNESSES.join(", ")}`);
1169
1328
  process.exit(1);
1170
1329
  }
1171
1330
  }
1331
+ if (!formatter.isJson) intro(chalk.bold(" membank setup "));
1332
+ function decoratedOut(msg) {
1333
+ const decorated = msg.replace(/✓/g, chalk.green("✓")).replace(/✗/g, chalk.red("✗")).replace(/⚠/g, chalk.yellow("⚠"));
1334
+ const styled = /^Step \d/.test(msg) ? `\n${chalk.bold(decorated)}` : decorated;
1335
+ process.stdout.write(`${styled}\n`);
1336
+ }
1172
1337
  const writer = new HarnessConfigWriter();
1173
1338
  const hookWriter = new InjectionHookWriter();
1174
1339
  const promptHelper = new PromptHelper(autoYes);
1340
+ let harnessSelector;
1341
+ if (interactive) harnessSelector = async (detected) => {
1342
+ const selected = await multiselect({
1343
+ message: "Which harnesses to configure?",
1344
+ options: SUPPORTED_HARNESSES.map((name) => {
1345
+ const found = detected.find((d) => d.name === name);
1346
+ return {
1347
+ value: found ?? {
1348
+ name,
1349
+ configPath: ""
1350
+ },
1351
+ label: name,
1352
+ hint: found !== void 0 ? found.configPath : "(not detected)"
1353
+ };
1354
+ }),
1355
+ initialValues: detected
1356
+ });
1357
+ if (isCancel(selected)) {
1358
+ cancel("Setup cancelled.");
1359
+ process.exit(0);
1360
+ }
1361
+ return selected;
1362
+ };
1175
1363
  const orchestrator = new SetupOrchestrator({
1176
1364
  writer,
1177
1365
  hookWriter,
1178
1366
  prompter: (question) => promptHelper.confirm(question),
1179
- modelDownloader: new ModelDownloader()
1367
+ harnessSelector,
1368
+ modelDownloader: new ModelDownloader(),
1369
+ out: formatter.isJson ? void 0 : decoratedOut
1180
1370
  });
1181
1371
  try {
1182
- if ((await orchestrator.run({
1372
+ const results = await orchestrator.run({
1183
1373
  yes: autoYes,
1184
1374
  dryRun: cmdOptions.dryRun,
1185
1375
  harness: cmdOptions.harness,
1186
1376
  json: formatter.isJson
1187
- })).some((r) => r.status === "error")) process.exit(1);
1377
+ });
1378
+ if (!formatter.isJson && !cmdOptions.dryRun && results.length > 0) {
1379
+ note("Start a new session to activate injection\nRun membank query \"test\" to verify", "Next steps");
1380
+ outro(`${chalk.green("✓")} Setup complete`);
1381
+ }
1382
+ if (results.some((r) => r.status === "error")) process.exit(1);
1188
1383
  } catch (err) {
1189
1384
  formatter.error(err instanceof Error ? err.message : String(err));
1190
1385
  process.exit(2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@membank/cli",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -14,12 +14,15 @@
14
14
  "dist"
15
15
  ],
16
16
  "dependencies": {
17
+ "@clack/prompts": "^1.3.0",
17
18
  "@huggingface/transformers": "^4.2.0",
19
+ "chalk": "^5.6.2",
20
+ "cli-table3": "^0.6.5",
18
21
  "commander": "^14.0.3",
19
22
  "ora": "^9.4.0",
20
- "@membank/core": "0.4.1",
21
- "@membank/dashboard": "0.2.1",
22
- "@membank/mcp": "0.4.1"
23
+ "@membank/dashboard": "0.2.2",
24
+ "@membank/mcp": "0.5.0",
25
+ "@membank/core": "0.5.0"
23
26
  },
24
27
  "devDependencies": {
25
28
  "@types/node": "^25.6.0",