@membank/cli 0.4.0 → 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 +340 -139
  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,103 +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
- "@membank/cli@latest",
481
+ "-y",
482
+ "@membank/cli",
417
483
  "--mcp"
418
484
  ];
419
485
  const writers$1 = {
420
- "claude-code": { async write(resolver, run, { overwrite = false } = {}) {
421
- const configured = hasKey(readJson$1(join(resolver.home(), ".claude.json")).mcpServers, "membank");
422
- if (configured && !overwrite) return { status: "already-configured" };
423
- if (configured) {
424
- 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 = [
425
510
  "mcp",
426
- "remove",
511
+ "add",
427
512
  "--scope",
428
513
  "user",
429
- "membank"
430
- ]);
431
- assertCliFound(remove, "claude");
432
- 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" };
433
523
  }
434
- const add = await run("claude", [
435
- "mcp",
436
- "add",
437
- "--scope",
438
- "user",
439
- "membank",
440
- "--",
441
- ...MEMBANK_NPX_ARGS
442
- ]);
443
- assertCliFound(add, "claude");
444
- if (add.exitCode !== 0) throw new Error(`claude mcp add failed: ${add.stderr || add.stdout}`);
445
- return { status: "written" };
446
- } },
447
- copilot: { async write(resolver, _run, { overwrite = false } = {}) {
448
- const cfgPath = join(resolver.home(), ".copilot", "mcp-config.json");
449
- const cfg = readJson$1(cfgPath);
450
- if (hasKey(cfg.mcpServers, "membank") && !overwrite) return { status: "already-configured" };
451
- writeJsonAtomic$1(cfgPath, {
452
- ...cfg,
453
- mcpServers: {
454
- ...cfg.mcpServers,
455
- membank: {
456
- command: "npx",
457
- args: ["@membank/cli@latest", "--mcp"]
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
+ }
458
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);
459
576
  }
460
- });
461
- return { status: "written" };
462
- } },
463
- codex: { async write(_resolver, run, { overwrite = false } = {}) {
464
- const list = await run("codex", ["mcp", "list"]);
465
- assertCliFound(list, "codex");
466
- const configured = list.exitCode === 0 && list.stdout.includes("membank");
467
- if (configured && !overwrite) return { status: "already-configured" };
468
- if (configured) {
469
- const remove = await run("codex", [
577
+ const addArgs = [
470
578
  "mcp",
471
- "remove",
472
- "membank"
473
- ]);
474
- assertCliFound(remove, "codex");
475
- 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" };
476
589
  }
477
- const add = await run("codex", [
478
- "mcp",
479
- "add",
480
- "membank",
481
- "--",
482
- ...MEMBANK_NPX_ARGS
483
- ]);
484
- assertCliFound(add, "codex");
485
- if (add.exitCode !== 0) throw new Error(`codex mcp add failed: ${add.stderr || add.stdout}`);
486
- return { status: "written" };
487
- } },
488
- opencode: { async write(resolver, _run, { overwrite = false } = {}) {
489
- const cfgPath = join(resolver.home(), ".config", "opencode", "opencode.json");
490
- const cfg = readJson$1(cfgPath);
491
- if (hasKey(cfg.mcp, "membank") && !overwrite) return { status: "already-configured" };
492
- writeJsonAtomic$1(cfgPath, {
493
- ...cfg,
494
- mcp: {
495
- ...cfg.mcp,
496
- membank: {
497
- type: "local",
498
- command: [
499
- "npx",
500
- "@membank/cli@latest",
501
- "--mcp"
502
- ]
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
+ }
503
615
  }
504
- }
505
- });
506
- return { status: "written" };
507
- } }
616
+ });
617
+ return { status: "written" };
618
+ }
619
+ }
508
620
  };
509
621
  const SUPPORTED_HARNESSES = Object.keys(writers$1);
510
622
  var HarnessConfigWriter = class {
@@ -514,6 +626,11 @@ var HarnessConfigWriter = class {
514
626
  this.#resolver = resolver;
515
627
  this.#run = run;
516
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
+ }
517
634
  async write(harness, { overwrite = false } = {}) {
518
635
  const writer = writers$1[harness];
519
636
  if (!writer) throw new Error(`Unknown harness: ${harness}`);
@@ -582,12 +699,14 @@ function pruneFlatEvent(hooks, eventKey) {
582
699
  const writers = {
583
700
  "claude-code": {
584
701
  inspect(resolver) {
585
- 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 ?? {};
586
704
  return {
587
705
  status: "ready",
706
+ configPath: cfgPath,
588
707
  hooks: [{
589
708
  event: "SessionStart",
590
- command: "npx @membank/cli@latest inject --harness claude-code",
709
+ command: "npx -y @membank/cli inject --harness claude-code",
591
710
  existingCommand: extractInjectCommand((Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []).flatMap(getHooksArray)) || null
592
711
  }]
593
712
  };
@@ -603,7 +722,7 @@ const writers = {
603
722
  matcher: "",
604
723
  hooks: [{
605
724
  type: "command",
606
- command: "npx @membank/cli@latest inject --harness claude-code"
725
+ command: "npx -y @membank/cli inject --harness claude-code"
607
726
  }]
608
727
  }];
609
728
  writeJsonAtomic(cfgPath, {
@@ -615,12 +734,14 @@ const writers = {
615
734
  },
616
735
  "copilot-cli": {
617
736
  inspect(resolver) {
618
- 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 ?? {};
619
739
  return {
620
740
  status: "ready",
741
+ configPath: cfgPath,
621
742
  hooks: [{
622
743
  event: "sessionStart",
623
- command: "npx @membank/cli@latest inject --harness copilot-cli",
744
+ command: "npx -y @membank/cli inject --harness copilot-cli",
624
745
  existingCommand: extractInjectCommand(Array.isArray(hooks.sessionStart) ? hooks.sessionStart : []) || null
625
746
  }]
626
747
  };
@@ -634,7 +755,7 @@ const writers = {
634
755
  pruneFlatEvent(newHooks, "postToolUseFailure");
635
756
  if (events.includes("sessionStart")) newHooks.sessionStart = [...filterOutMembankFlat(Array.isArray(hooks.sessionStart) ? hooks.sessionStart : []), {
636
757
  type: "command",
637
- bash: "npx @membank/cli@latest inject --harness copilot-cli",
758
+ bash: "npx -y @membank/cli inject --harness copilot-cli",
638
759
  timeoutSec: 30
639
760
  }];
640
761
  writeJsonAtomic(cfgPath, {
@@ -647,12 +768,14 @@ const writers = {
647
768
  },
648
769
  codex: {
649
770
  inspect(resolver) {
650
- 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 ?? {};
651
773
  return {
652
774
  status: "ready",
775
+ configPath: cfgPath,
653
776
  hooks: [{
654
777
  event: "SessionStart",
655
- command: "npx @membank/cli@latest inject --harness codex",
778
+ command: "npx -y @membank/cli inject --harness codex",
656
779
  existingCommand: extractInjectCommand((Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []).flatMap(getHooksArray)) || null
657
780
  }]
658
781
  };
@@ -668,7 +791,7 @@ const writers = {
668
791
  matcher: "",
669
792
  hooks: [{
670
793
  type: "command",
671
- command: "npx @membank/cli@latest inject --harness codex",
794
+ command: "npx -y @membank/cli inject --harness codex",
672
795
  timeout: 30
673
796
  }]
674
797
  }];
@@ -688,6 +811,7 @@ const writers = {
688
811
  }
689
812
  return {
690
813
  status: "ready",
814
+ configPath: pluginPath,
691
815
  hooks: [{
692
816
  event: "plugin",
693
817
  command: pluginPath,
@@ -709,7 +833,7 @@ function newOpencodePlugin() {
709
833
  "export default {",
710
834
  " hooks: {",
711
835
  " \"session.start\": async ({ $ }) => {",
712
- " return await $`npx @membank/cli@latest inject`.text();",
836
+ " return await $`npx -y @membank/cli inject`.text();",
713
837
  " },",
714
838
  " },",
715
839
  "};"
@@ -757,6 +881,12 @@ var ModelDownloader = class extends EventEmitter {
757
881
  super();
758
882
  this.modelPath = modelPath ?? defaultModelPath();
759
883
  }
884
+ isAlreadyCached() {
885
+ return isCached(this.modelPath);
886
+ }
887
+ get cachePath() {
888
+ return this.modelPath;
889
+ }
760
890
  async download() {
761
891
  if (isCached(this.modelPath)) return { skipped: true };
762
892
  const startTime = Date.now();
@@ -852,6 +982,7 @@ var SetupOrchestrator = class {
852
982
  #writer;
853
983
  #hookWriter;
854
984
  #prompter;
985
+ #harnessSelector;
855
986
  #modelDownloader;
856
987
  #out;
857
988
  #progressWrite;
@@ -860,6 +991,7 @@ var SetupOrchestrator = class {
860
991
  this.#writer = deps.writer;
861
992
  this.#hookWriter = deps.hookWriter;
862
993
  this.#prompter = deps.prompter ?? defaultPrompter;
994
+ this.#harnessSelector = deps.harnessSelector;
863
995
  this.#modelDownloader = deps.modelDownloader;
864
996
  this.#out = deps.out ?? ((msg) => process.stdout.write(`${msg}\n`));
865
997
  this.#progressWrite = deps.progressWrite ?? ((text) => process.stdout.write(text));
@@ -885,6 +1017,13 @@ var SetupOrchestrator = class {
885
1017
  }));
886
1018
  return [];
887
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
+ }
888
1027
  if (!json) {
889
1028
  out("Detected harnesses:");
890
1029
  for (const h of detected) out(` • ${h.name} (${h.configPath})`);
@@ -893,11 +1032,27 @@ var SetupOrchestrator = class {
893
1032
  if (dryRun) {
894
1033
  out("Planned changes (dry-run — no files written):");
895
1034
  for (const h of detected) {
896
- out(` ⚠ ${h.name}: would write MCP config`);
897
- 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
+ }
898
1049
  }
899
1050
  out("");
900
- 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)");
901
1056
  return detected.map((h) => ({
902
1057
  harness: h.name,
903
1058
  status: "skipped"
@@ -910,6 +1065,8 @@ var SetupOrchestrator = class {
910
1065
  }
911
1066
  }
912
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`);
913
1070
  for (const h of detected) {
914
1071
  let writeResult;
915
1072
  try {
@@ -917,6 +1074,7 @@ var SetupOrchestrator = class {
917
1074
  } catch (err) {
918
1075
  const msg = err instanceof Error ? err.message : String(err);
919
1076
  out(` ✗ ${h.name}: ${msg}`);
1077
+ if (err instanceof CommandError) out(` Command: ${err.command}`);
920
1078
  results.push({
921
1079
  harness: h.name,
922
1080
  status: "error",
@@ -945,6 +1103,7 @@ var SetupOrchestrator = class {
945
1103
  } catch (err) {
946
1104
  const msg = err instanceof Error ? err.message : String(err);
947
1105
  out(` ✗ ${h.name}: ${msg}`);
1106
+ if (err instanceof CommandError) out(` Command: ${err.command}`);
948
1107
  results.push({
949
1108
  harness: h.name,
950
1109
  status: "error",
@@ -962,12 +1121,15 @@ var SetupOrchestrator = class {
962
1121
  out("");
963
1122
  const injectionHooksConfigured = [];
964
1123
  if (this.#hookWriter) {
1124
+ out(`Step 2/${totalSteps} Installing injection hooks`);
965
1125
  injectionHooksConfigured.push(...await this.#runHookSetup(detected, yes, out));
966
1126
  out("");
967
1127
  }
968
1128
  let modelDownloaded = false;
969
- if (this.#modelDownloader) modelDownloaded = !(await this.#runModelDownload(this.#modelDownloader, out)).skipped;
970
- 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");
971
1133
  const written = results.filter((r) => r.status === "written").length;
972
1134
  const skipped = results.filter((r) => r.status === "already-configured").length;
973
1135
  const errors = results.filter((r) => r.status === "error").length;
@@ -988,6 +1150,7 @@ var SetupOrchestrator = class {
988
1150
  async #runHookSetup(detected, yes, out) {
989
1151
  const configured = [];
990
1152
  const w = this.#hookWriter;
1153
+ if (w === void 0) return configured;
991
1154
  for (const h of detected) try {
992
1155
  const inspected = w.inspect(h.name);
993
1156
  if (inspected.status === "not-supported") continue;
@@ -1012,6 +1175,7 @@ var SetupOrchestrator = class {
1012
1175
  } catch (err) {
1013
1176
  const msg = err instanceof Error ? err.message : String(err);
1014
1177
  out(` ✗ ${h.name} injection hooks: ${msg}`);
1178
+ if (err instanceof CommandError) out(` Command: ${err.command}`);
1015
1179
  }
1016
1180
  return configured;
1017
1181
  }
@@ -1157,28 +1321,65 @@ program.command("setup").description("detect installed harnesses and write MCP c
1157
1321
  const globalOpts = program.opts();
1158
1322
  const autoYes = cmdOptions.yes === true || globalOpts.yes === true;
1159
1323
  const formatter = Formatter.create(globalOpts.json === true);
1324
+ const interactive = !formatter.isJson && !autoYes && cmdOptions.harness === void 0;
1160
1325
  if (cmdOptions.harness !== void 0) {
1161
1326
  if (!SUPPORTED_HARNESSES.some((h) => h === cmdOptions.harness)) {
1162
1327
  formatter.error(`Unknown harness: "${cmdOptions.harness}". Supported: ${SUPPORTED_HARNESSES.join(", ")}`);
1163
1328
  process.exit(1);
1164
1329
  }
1165
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
+ }
1166
1337
  const writer = new HarnessConfigWriter();
1167
1338
  const hookWriter = new InjectionHookWriter();
1168
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
+ };
1169
1363
  const orchestrator = new SetupOrchestrator({
1170
1364
  writer,
1171
1365
  hookWriter,
1172
1366
  prompter: (question) => promptHelper.confirm(question),
1173
- modelDownloader: new ModelDownloader()
1367
+ harnessSelector,
1368
+ modelDownloader: new ModelDownloader(),
1369
+ out: formatter.isJson ? void 0 : decoratedOut
1174
1370
  });
1175
1371
  try {
1176
- if ((await orchestrator.run({
1372
+ const results = await orchestrator.run({
1177
1373
  yes: autoYes,
1178
1374
  dryRun: cmdOptions.dryRun,
1179
1375
  harness: cmdOptions.harness,
1180
1376
  json: formatter.isJson
1181
- })).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);
1182
1383
  } catch (err) {
1183
1384
  formatter.error(err instanceof Error ? err.message : String(err));
1184
1385
  process.exit(2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@membank/cli",
3
- "version": "0.4.0",
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.0",
21
- "@membank/dashboard": "0.2.0",
22
- "@membank/mcp": "0.4.0"
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",