@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.
- package/dist/index.mjs +340 -139
- 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
|
|
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(
|
|
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]:
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
308
|
+
process.stdout.write(`${chalk.dim("No memories found.")}\n`);
|
|
284
309
|
return;
|
|
285
310
|
}
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
340
|
+
const types = [
|
|
295
341
|
"correction",
|
|
296
342
|
"preference",
|
|
297
343
|
"decision",
|
|
298
344
|
"learning",
|
|
299
345
|
"fact"
|
|
300
|
-
]
|
|
301
|
-
process.stdout.write(
|
|
302
|
-
process.stdout.write(`
|
|
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
|
|
360
|
+
process.stdout.write(`${chalk.dim("No memories found.")}\n`);
|
|
311
361
|
return;
|
|
312
362
|
}
|
|
313
|
-
|
|
314
|
-
|
|
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(
|
|
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
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
|
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",
|
|
482
|
+
"@membank/cli",
|
|
417
483
|
"--mcp"
|
|
418
484
|
];
|
|
419
485
|
const writers$1 = {
|
|
420
|
-
"claude-code": {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
"
|
|
511
|
+
"add",
|
|
427
512
|
"--scope",
|
|
428
513
|
"user",
|
|
429
|
-
"membank"
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
"
|
|
472
|
-
"membank"
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
897
|
-
|
|
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
|
-
|
|
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)
|
|
970
|
-
|
|
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
|
-
|
|
1367
|
+
harnessSelector,
|
|
1368
|
+
modelDownloader: new ModelDownloader(),
|
|
1369
|
+
out: formatter.isJson ? void 0 : decoratedOut
|
|
1174
1370
|
});
|
|
1175
1371
|
try {
|
|
1176
|
-
|
|
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
|
-
})
|
|
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.
|
|
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/
|
|
21
|
-
"@membank/
|
|
22
|
-
"@membank/
|
|
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",
|