@membank/cli 0.4.1 → 0.5.1
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 +339 -144
- 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,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
|
|
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
|
|
482
|
+
"@membank/cli",
|
|
418
483
|
"--mcp"
|
|
419
484
|
];
|
|
420
485
|
const writers$1 = {
|
|
421
|
-
"claude-code": {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
"
|
|
511
|
+
"add",
|
|
428
512
|
"--scope",
|
|
429
513
|
"user",
|
|
430
|
-
"membank"
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
-
"
|
|
477
|
-
"membank"
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
903
|
-
|
|
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
|
-
|
|
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)
|
|
976
|
-
|
|
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
|
-
|
|
1367
|
+
harnessSelector,
|
|
1368
|
+
modelDownloader: new ModelDownloader(),
|
|
1369
|
+
out: formatter.isJson ? void 0 : decoratedOut
|
|
1180
1370
|
});
|
|
1181
1371
|
try {
|
|
1182
|
-
|
|
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
|
-
})
|
|
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.
|
|
3
|
+
"version": "0.5.1",
|
|
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.
|
|
21
|
-
"@membank/dashboard": "0.2.
|
|
22
|
-
"@membank/mcp": "0.
|
|
23
|
+
"@membank/core": "0.5.1",
|
|
24
|
+
"@membank/dashboard": "0.2.3",
|
|
25
|
+
"@membank/mcp": "0.6.0"
|
|
23
26
|
},
|
|
24
27
|
"devDependencies": {
|
|
25
28
|
"@types/node": "^25.6.0",
|