@membank/cli 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +367 -117
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { DatabaseManager, EmbeddingService, MemoryRepository, QueryEngine } from "@membank/core";
|
|
2
|
+
import { DatabaseManager, EmbeddingService, MEMORY_TYPE_VALUES, MemoryRepository, QueryEngine, SessionContextBuilder, resolveScope } from "@membank/core";
|
|
3
3
|
import { startServer } from "@membank/mcp";
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
|
|
@@ -7,6 +7,8 @@ import { dirname, join } from "node:path";
|
|
|
7
7
|
import * as readline from "node:readline";
|
|
8
8
|
import { createInterface } from "node:readline";
|
|
9
9
|
import { homedir, tmpdir } from "node:os";
|
|
10
|
+
import { execFile } from "node:child_process";
|
|
11
|
+
import { promisify } from "node:util";
|
|
10
12
|
import { EventEmitter } from "node:events";
|
|
11
13
|
import { pipeline } from "@huggingface/transformers";
|
|
12
14
|
//#region src/commands/add.ts
|
|
@@ -70,22 +72,16 @@ function exportCommand(db, formatter, opts) {
|
|
|
70
72
|
}
|
|
71
73
|
//#endregion
|
|
72
74
|
//#region src/commands/import.ts
|
|
73
|
-
const MEMORY_TYPES = new Set(
|
|
74
|
-
"correction",
|
|
75
|
-
"preference",
|
|
76
|
-
"decision",
|
|
77
|
-
"learning",
|
|
78
|
-
"fact"
|
|
79
|
-
]);
|
|
75
|
+
const MEMORY_TYPES = new Set(MEMORY_TYPE_VALUES);
|
|
80
76
|
function isValidRecord(r) {
|
|
81
77
|
if (typeof r !== "object" || r === null) return false;
|
|
82
78
|
const rec = r;
|
|
83
|
-
return typeof rec
|
|
79
|
+
return typeof rec.id === "string" && rec.id.length > 0 && typeof rec.content === "string" && typeof rec.type === "string" && MEMORY_TYPES.has(rec.type) && typeof rec.scope === "string" && rec.scope.length > 0;
|
|
84
80
|
}
|
|
85
81
|
function isExportFile(parsed) {
|
|
86
82
|
if (typeof parsed !== "object" || parsed === null) return false;
|
|
87
83
|
const obj = parsed;
|
|
88
|
-
return obj
|
|
84
|
+
return obj.version === 1 && Array.isArray(obj.memories);
|
|
89
85
|
}
|
|
90
86
|
async function importCommand(filePath, db, formatter, prompt) {
|
|
91
87
|
let raw;
|
|
@@ -131,6 +127,41 @@ async function importCommand(filePath, db, formatter, prompt) {
|
|
|
131
127
|
else process.stdout.write(`Imported ${count} memories.\n`);
|
|
132
128
|
}
|
|
133
129
|
//#endregion
|
|
130
|
+
//#region src/commands/inject.ts
|
|
131
|
+
function formatContext(ctx) {
|
|
132
|
+
const lines = [];
|
|
133
|
+
const statParts = Object.entries(ctx.stats).filter(([, count]) => count > 0).map(([type, count]) => `${count} ${type}${count !== 1 ? "s" : ""}`);
|
|
134
|
+
if (statParts.length > 0) lines.push(`[Memory Stats]: ${statParts.join(", ")}`);
|
|
135
|
+
const formatMemory = (m) => `"${m.content}" (${m.type})`;
|
|
136
|
+
for (const m of ctx.pinnedGlobal) lines.push(`[Pinned Global]: ${formatMemory(m)}`);
|
|
137
|
+
for (const m of ctx.pinnedProject) lines.push(`[Pinned Project]: ${formatMemory(m)}`);
|
|
138
|
+
return lines.join("\n");
|
|
139
|
+
}
|
|
140
|
+
async function injectCommand(opts) {
|
|
141
|
+
const projectScope = opts.scope ?? await resolveScope();
|
|
142
|
+
const db = DatabaseManager.open();
|
|
143
|
+
let text;
|
|
144
|
+
try {
|
|
145
|
+
text = formatContext(new SessionContextBuilder(db).getSessionContext(projectScope));
|
|
146
|
+
} finally {
|
|
147
|
+
db.close();
|
|
148
|
+
}
|
|
149
|
+
if (!text) process.exit(0);
|
|
150
|
+
const harness = opts.harness;
|
|
151
|
+
if (harness === "claude-code") {
|
|
152
|
+
process.stdout.write(JSON.stringify({ hookSpecificOutput: {
|
|
153
|
+
hookEventName: "SessionStart",
|
|
154
|
+
additionalContext: text
|
|
155
|
+
} }));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (harness === "copilot-cli") {
|
|
159
|
+
process.stdout.write(JSON.stringify({ additionalContext: text }));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
process.stdout.write(`${text}\n`);
|
|
163
|
+
}
|
|
164
|
+
//#endregion
|
|
134
165
|
//#region src/commands/list.ts
|
|
135
166
|
async function listCommand(options, formatter) {
|
|
136
167
|
const db = DatabaseManager.open();
|
|
@@ -214,11 +245,8 @@ var Formatter = class Formatter {
|
|
|
214
245
|
constructor(isJson) {
|
|
215
246
|
this.#isJson = isJson;
|
|
216
247
|
}
|
|
217
|
-
static create() {
|
|
218
|
-
return new Formatter(!process.stdout.isTTY);
|
|
219
|
-
}
|
|
220
|
-
withJson(isJson) {
|
|
221
|
-
return new Formatter(isJson);
|
|
248
|
+
static create(forceJson = false) {
|
|
249
|
+
return new Formatter(forceJson || !process.stdout.isTTY);
|
|
222
250
|
}
|
|
223
251
|
get isJson() {
|
|
224
252
|
return this.#isJson;
|
|
@@ -228,13 +256,7 @@ var Formatter = class Formatter {
|
|
|
228
256
|
process.stdout.write(`${JSON.stringify(memory)}\n`);
|
|
229
257
|
return;
|
|
230
258
|
}
|
|
231
|
-
|
|
232
|
-
process.stdout.write(`\n[${memory.type}] ${memory.id}\n`);
|
|
233
|
-
process.stdout.write(` Content : ${memory.content}\n`);
|
|
234
|
-
process.stdout.write(` Tags : ${tags}\n`);
|
|
235
|
-
process.stdout.write(` Scope : ${memory.scope}\n`);
|
|
236
|
-
process.stdout.write(` Pinned : ${memory.pinned}\n`);
|
|
237
|
-
process.stdout.write("\n");
|
|
259
|
+
this.#writeMemoryBlock(memory, ` Pinned : ${memory.pinned}\n`);
|
|
238
260
|
}
|
|
239
261
|
outputMemories(memories) {
|
|
240
262
|
if (this.#isJson) {
|
|
@@ -245,13 +267,7 @@ var Formatter = class Formatter {
|
|
|
245
267
|
process.stdout.write("No memories found.\n");
|
|
246
268
|
return;
|
|
247
269
|
}
|
|
248
|
-
for (const memory of memories)
|
|
249
|
-
const tags = memory.tags.length > 0 ? memory.tags.join(", ") : "(none)";
|
|
250
|
-
process.stdout.write(`\n[${memory.type}] ${memory.id}\n`);
|
|
251
|
-
process.stdout.write(` Content : ${memory.content}\n`);
|
|
252
|
-
process.stdout.write(` Tags : ${tags}\n`);
|
|
253
|
-
process.stdout.write(` Scope : ${memory.scope}\n`);
|
|
254
|
-
}
|
|
270
|
+
for (const memory of memories) this.#writeMemoryBlock(memory);
|
|
255
271
|
process.stdout.write("\n");
|
|
256
272
|
}
|
|
257
273
|
outputStats(stats) {
|
|
@@ -278,20 +294,22 @@ var Formatter = class Formatter {
|
|
|
278
294
|
process.stdout.write("No memories found.\n");
|
|
279
295
|
return;
|
|
280
296
|
}
|
|
281
|
-
for (const result of results) {
|
|
282
|
-
const tags = result.tags.length > 0 ? result.tags.join(", ") : "(none)";
|
|
283
|
-
process.stdout.write(`\n[${result.type}] ${result.id}\n`);
|
|
284
|
-
process.stdout.write(` Content : ${result.content}\n`);
|
|
285
|
-
process.stdout.write(` Tags : ${tags}\n`);
|
|
286
|
-
process.stdout.write(` Scope : ${result.scope}\n`);
|
|
287
|
-
process.stdout.write(` Score : ${result.score.toFixed(4)}\n`);
|
|
288
|
-
}
|
|
297
|
+
for (const result of results) this.#writeMemoryBlock(result, ` Score : ${result.score.toFixed(4)}\n`);
|
|
289
298
|
process.stdout.write("\n");
|
|
290
299
|
}
|
|
291
300
|
error(msg) {
|
|
292
301
|
if (this.#isJson) process.stderr.write(`${JSON.stringify({ error: msg })}\n`);
|
|
293
302
|
else process.stderr.write(`Error: ${msg}\n`);
|
|
294
303
|
}
|
|
304
|
+
#writeMemoryBlock(memory, extra) {
|
|
305
|
+
const tags = memory.tags.length > 0 ? memory.tags.join(", ") : "(none)";
|
|
306
|
+
process.stdout.write(`\n[${memory.type}] ${memory.id}\n`);
|
|
307
|
+
process.stdout.write(` Content : ${memory.content}\n`);
|
|
308
|
+
process.stdout.write(` Tags : ${tags}\n`);
|
|
309
|
+
process.stdout.write(` Scope : ${memory.scope}\n`);
|
|
310
|
+
if (extra !== void 0) process.stdout.write(extra);
|
|
311
|
+
process.stdout.write("\n");
|
|
312
|
+
}
|
|
295
313
|
};
|
|
296
314
|
//#endregion
|
|
297
315
|
//#region src/prompt-helper.ts
|
|
@@ -314,27 +332,58 @@ var PromptHelper = class {
|
|
|
314
332
|
}
|
|
315
333
|
};
|
|
316
334
|
//#endregion
|
|
335
|
+
//#region src/utils/execFileNoThrow.ts
|
|
336
|
+
const execFileAsync = promisify(execFile);
|
|
337
|
+
function resolveCmd(cmd) {
|
|
338
|
+
return process.platform === "win32" ? `${cmd}.cmd` : cmd;
|
|
339
|
+
}
|
|
340
|
+
async function execFileNoThrow(cmd, args) {
|
|
341
|
+
try {
|
|
342
|
+
const { stdout, stderr } = await execFileAsync(resolveCmd(cmd), args, { encoding: "utf8" });
|
|
343
|
+
return {
|
|
344
|
+
stdout: stdout ?? "",
|
|
345
|
+
stderr: stderr ?? "",
|
|
346
|
+
exitCode: 0
|
|
347
|
+
};
|
|
348
|
+
} catch (err) {
|
|
349
|
+
if (err !== null && typeof err === "object" && "code" in err) {
|
|
350
|
+
const e = err;
|
|
351
|
+
if (e.code === "ENOENT") return {
|
|
352
|
+
stdout: "",
|
|
353
|
+
stderr: `Command not found: ${cmd}`,
|
|
354
|
+
exitCode: 127
|
|
355
|
+
};
|
|
356
|
+
return {
|
|
357
|
+
stdout: typeof e.stdout === "string" ? e.stdout : "",
|
|
358
|
+
stderr: typeof e.stderr === "string" ? e.stderr : "",
|
|
359
|
+
exitCode: typeof e.code === "number" ? e.code : 1
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
stdout: "",
|
|
364
|
+
stderr: err instanceof Error ? err.message : String(err),
|
|
365
|
+
exitCode: 1
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
//#endregion
|
|
317
370
|
//#region src/setup/harness-config-writer.ts
|
|
318
|
-
const defaultPathResolver = {
|
|
371
|
+
const defaultPathResolver$1 = {
|
|
319
372
|
home: () => {
|
|
320
|
-
const h = process.env
|
|
373
|
+
const h = process.env.HOME ?? process.env.USERPROFILE;
|
|
321
374
|
if (!h) throw new Error("Cannot determine home directory");
|
|
322
375
|
return h;
|
|
323
376
|
},
|
|
324
377
|
cwd: () => process.cwd()
|
|
325
378
|
};
|
|
326
|
-
|
|
327
|
-
command: "npx",
|
|
328
|
-
args: ["@membank/cli", "--mcp"]
|
|
329
|
-
};
|
|
330
|
-
function readJson(path) {
|
|
379
|
+
function readJson$1(path) {
|
|
331
380
|
try {
|
|
332
381
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
333
382
|
} catch {
|
|
334
383
|
return {};
|
|
335
384
|
}
|
|
336
385
|
}
|
|
337
|
-
function writeJsonAtomic(path, data) {
|
|
386
|
+
function writeJsonAtomic$1(path, data) {
|
|
338
387
|
mkdirSync(dirname(path), { recursive: true });
|
|
339
388
|
const tmp = join(mkdtempSync(join(tmpdir(), "membank-")), "cfg.json");
|
|
340
389
|
writeFileSync(tmp, JSON.stringify(data, null, 2));
|
|
@@ -343,66 +392,237 @@ function writeJsonAtomic(path, data) {
|
|
|
343
392
|
function hasKey(container, key) {
|
|
344
393
|
return container !== null && typeof container === "object" && key in container;
|
|
345
394
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
395
|
+
function assertCliFound(result, cli) {
|
|
396
|
+
if (result.exitCode === 127) throw new Error(`${cli} CLI not found — install ${cli} first`);
|
|
397
|
+
}
|
|
398
|
+
const MEMBANK_NPX_ARGS = [
|
|
399
|
+
"npx",
|
|
400
|
+
"@membank/cli@latest",
|
|
401
|
+
"--mcp"
|
|
402
|
+
];
|
|
403
|
+
const writers$1 = {
|
|
404
|
+
"claude-code": { async write(resolver, run, { overwrite = false } = {}) {
|
|
405
|
+
const configured = hasKey(readJson$1(join(resolver.home(), ".claude.json")).mcpServers, "membank");
|
|
406
|
+
if (configured && !overwrite) return { status: "already-configured" };
|
|
407
|
+
if (configured) {
|
|
408
|
+
const remove = await run("claude", [
|
|
409
|
+
"mcp",
|
|
410
|
+
"remove",
|
|
411
|
+
"--scope",
|
|
412
|
+
"user",
|
|
413
|
+
"membank"
|
|
414
|
+
]);
|
|
415
|
+
assertCliFound(remove, "claude");
|
|
416
|
+
if (remove.exitCode !== 0) throw new Error(`claude mcp remove failed: ${remove.stderr}`);
|
|
417
|
+
}
|
|
418
|
+
const add = await run("claude", [
|
|
419
|
+
"mcp",
|
|
420
|
+
"add",
|
|
421
|
+
"--scope",
|
|
422
|
+
"user",
|
|
423
|
+
"membank",
|
|
424
|
+
"--",
|
|
425
|
+
...MEMBANK_NPX_ARGS
|
|
426
|
+
]);
|
|
427
|
+
assertCliFound(add, "claude");
|
|
428
|
+
if (add.exitCode !== 0) throw new Error(`claude mcp add failed: ${add.stderr || add.stdout}`);
|
|
429
|
+
return { status: "written" };
|
|
430
|
+
} },
|
|
431
|
+
copilot: { async write(resolver, _run, { overwrite = false } = {}) {
|
|
432
|
+
const cfgPath = join(resolver.home(), ".copilot", "mcp-config.json");
|
|
433
|
+
const cfg = readJson$1(cfgPath);
|
|
434
|
+
if (hasKey(cfg.mcpServers, "membank") && !overwrite) return { status: "already-configured" };
|
|
435
|
+
writeJsonAtomic$1(cfgPath, {
|
|
351
436
|
...cfg,
|
|
352
437
|
mcpServers: {
|
|
353
|
-
...cfg
|
|
354
|
-
membank:
|
|
438
|
+
...cfg.mcpServers,
|
|
439
|
+
membank: {
|
|
440
|
+
command: "npx",
|
|
441
|
+
args: ["@membank/cli@latest", "--mcp"]
|
|
442
|
+
}
|
|
355
443
|
}
|
|
356
|
-
})
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
444
|
+
});
|
|
445
|
+
return { status: "written" };
|
|
446
|
+
} },
|
|
447
|
+
codex: { async write(_resolver, run, { overwrite = false } = {}) {
|
|
448
|
+
const list = await run("codex", ["mcp", "list"]);
|
|
449
|
+
assertCliFound(list, "codex");
|
|
450
|
+
const configured = list.exitCode === 0 && list.stdout.includes("membank");
|
|
451
|
+
if (configured && !overwrite) return { status: "already-configured" };
|
|
452
|
+
if (configured) {
|
|
453
|
+
const remove = await run("codex", [
|
|
454
|
+
"mcp",
|
|
455
|
+
"remove",
|
|
456
|
+
"membank"
|
|
457
|
+
]);
|
|
458
|
+
assertCliFound(remove, "codex");
|
|
459
|
+
if (remove.exitCode !== 0) throw new Error(`codex mcp remove failed: ${remove.stderr}`);
|
|
460
|
+
}
|
|
461
|
+
const add = await run("codex", [
|
|
462
|
+
"mcp",
|
|
463
|
+
"add",
|
|
464
|
+
"membank",
|
|
465
|
+
"--",
|
|
466
|
+
...MEMBANK_NPX_ARGS
|
|
467
|
+
]);
|
|
468
|
+
assertCliFound(add, "codex");
|
|
469
|
+
if (add.exitCode !== 0) throw new Error(`codex mcp add failed: ${add.stderr || add.stdout}`);
|
|
470
|
+
return { status: "written" };
|
|
471
|
+
} },
|
|
472
|
+
opencode: { async write(resolver, _run, { overwrite = false } = {}) {
|
|
473
|
+
const cfgPath = join(resolver.home(), ".config", "opencode", "opencode.json");
|
|
474
|
+
const cfg = readJson$1(cfgPath);
|
|
475
|
+
if (hasKey(cfg.mcp, "membank") && !overwrite) return { status: "already-configured" };
|
|
476
|
+
writeJsonAtomic$1(cfgPath, {
|
|
362
477
|
...cfg,
|
|
363
|
-
|
|
364
|
-
...cfg
|
|
365
|
-
membank:
|
|
478
|
+
mcp: {
|
|
479
|
+
...cfg.mcp,
|
|
480
|
+
membank: {
|
|
481
|
+
type: "local",
|
|
482
|
+
command: [
|
|
483
|
+
"npx",
|
|
484
|
+
"@membank/cli@latest",
|
|
485
|
+
"--mcp"
|
|
486
|
+
]
|
|
487
|
+
}
|
|
366
488
|
}
|
|
367
|
-
})
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
489
|
+
});
|
|
490
|
+
return { status: "written" };
|
|
491
|
+
} }
|
|
492
|
+
};
|
|
493
|
+
const SUPPORTED_HARNESSES = Object.keys(writers$1);
|
|
494
|
+
var HarnessConfigWriter = class {
|
|
495
|
+
#resolver;
|
|
496
|
+
#run;
|
|
497
|
+
constructor(resolver = defaultPathResolver$1, run = execFileNoThrow) {
|
|
498
|
+
this.#resolver = resolver;
|
|
499
|
+
this.#run = run;
|
|
500
|
+
}
|
|
501
|
+
async write(harness, { overwrite = false } = {}) {
|
|
502
|
+
const writer = writers$1[harness];
|
|
503
|
+
if (!writer) throw new Error(`Unknown harness: ${harness}`);
|
|
504
|
+
return writer.write(this.#resolver, this.#run, { overwrite });
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
//#endregion
|
|
508
|
+
//#region src/setup/injection-hook-writer.ts
|
|
509
|
+
const defaultPathResolver = { home: () => {
|
|
510
|
+
const h = process.env.HOME ?? process.env.USERPROFILE;
|
|
511
|
+
if (!h) throw new Error("Cannot determine home directory");
|
|
512
|
+
return h;
|
|
513
|
+
} };
|
|
514
|
+
function readJson(path) {
|
|
515
|
+
try {
|
|
516
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
517
|
+
} catch {
|
|
518
|
+
return {};
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
function writeJsonAtomic(path, data) {
|
|
522
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
523
|
+
const tmp = join(mkdtempSync(join(tmpdir(), "membank-hook-")), "cfg.json");
|
|
524
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2));
|
|
525
|
+
renameSync(tmp, path);
|
|
526
|
+
}
|
|
527
|
+
function containsMembankInject(hooks) {
|
|
528
|
+
if (!Array.isArray(hooks)) return false;
|
|
529
|
+
return hooks.some((h) => typeof h === "object" && h !== null && ("command" in h && typeof h.command === "string" && h.command.includes("@membank/cli inject") || "bash" in h && typeof h.bash === "string" && h.bash.includes("@membank/cli inject")));
|
|
530
|
+
}
|
|
531
|
+
const writers = {
|
|
532
|
+
"claude-code": { write(resolver) {
|
|
533
|
+
const cfgPath = join(resolver.home(), ".claude", "settings.json");
|
|
534
|
+
const cfg = readJson(cfgPath);
|
|
535
|
+
const hooks = cfg.hooks;
|
|
536
|
+
const sessionStart = hooks?.SessionStart;
|
|
537
|
+
if (Array.isArray(sessionStart) && containsMembankInject(sessionStart.flatMap((g) => Array.isArray(g.hooks) ? g.hooks : []))) return { status: "already-configured" };
|
|
538
|
+
const existingSessionStart = Array.isArray(sessionStart) ? sessionStart : [];
|
|
539
|
+
writeJsonAtomic(cfgPath, {
|
|
373
540
|
...cfg,
|
|
374
|
-
|
|
375
|
-
...
|
|
376
|
-
|
|
541
|
+
hooks: {
|
|
542
|
+
...hooks ?? {},
|
|
543
|
+
SessionStart: [...existingSessionStart, {
|
|
544
|
+
matcher: "",
|
|
545
|
+
hooks: [{
|
|
546
|
+
type: "command",
|
|
547
|
+
command: "npx @membank/cli inject --harness claude-code"
|
|
548
|
+
}]
|
|
549
|
+
}]
|
|
377
550
|
}
|
|
378
|
-
})
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
551
|
+
});
|
|
552
|
+
return { status: "written" };
|
|
553
|
+
} },
|
|
554
|
+
"copilot-cli": { write(resolver) {
|
|
555
|
+
const cfgPath = join(resolver.home(), ".copilot", "settings.json");
|
|
556
|
+
const cfg = readJson(cfgPath);
|
|
557
|
+
const hooks = cfg.hooks;
|
|
558
|
+
const sessionStart = hooks?.sessionStart;
|
|
559
|
+
if (Array.isArray(sessionStart) && containsMembankInject(sessionStart)) return { status: "already-configured" };
|
|
560
|
+
const existingSessionStart = Array.isArray(sessionStart) ? sessionStart : [];
|
|
561
|
+
writeJsonAtomic(cfgPath, {
|
|
562
|
+
version: cfg.version ?? 1,
|
|
384
563
|
...cfg,
|
|
385
|
-
|
|
386
|
-
...
|
|
387
|
-
|
|
564
|
+
hooks: {
|
|
565
|
+
...hooks ?? {},
|
|
566
|
+
sessionStart: [...existingSessionStart, {
|
|
567
|
+
type: "command",
|
|
568
|
+
bash: "npx @membank/cli inject --harness copilot-cli",
|
|
569
|
+
timeoutSec: 30
|
|
570
|
+
}]
|
|
388
571
|
}
|
|
389
|
-
})
|
|
390
|
-
|
|
572
|
+
});
|
|
573
|
+
return { status: "written" };
|
|
574
|
+
} },
|
|
575
|
+
codex: { write(resolver) {
|
|
576
|
+
const cfgPath = join(resolver.home(), ".codex", "hooks.json");
|
|
577
|
+
const cfg = readJson(cfgPath);
|
|
578
|
+
const hooks = cfg.hooks;
|
|
579
|
+
const sessionStart = hooks?.SessionStart;
|
|
580
|
+
if (Array.isArray(sessionStart) && containsMembankInject(sessionStart.flatMap((g) => Array.isArray(g.hooks) ? g.hooks : []))) return { status: "already-configured" };
|
|
581
|
+
const existingSessionStart = Array.isArray(sessionStart) ? sessionStart : [];
|
|
582
|
+
writeJsonAtomic(cfgPath, {
|
|
583
|
+
...cfg,
|
|
584
|
+
hooks: {
|
|
585
|
+
...hooks ?? {},
|
|
586
|
+
SessionStart: [...existingSessionStart, {
|
|
587
|
+
matcher: "",
|
|
588
|
+
hooks: [{
|
|
589
|
+
type: "command",
|
|
590
|
+
command: "npx @membank/cli inject --harness codex",
|
|
591
|
+
timeout: 30
|
|
592
|
+
}]
|
|
593
|
+
}]
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
return { status: "written" };
|
|
597
|
+
} },
|
|
598
|
+
opencode: { write(resolver) {
|
|
599
|
+
const pluginPath = join(resolver.home(), ".config", "opencode", "plugins", "membank.js");
|
|
600
|
+
if (existsSync(pluginPath)) {
|
|
601
|
+
if (readFileSync(pluginPath, "utf8").includes("@membank/cli inject")) return { status: "already-configured" };
|
|
602
|
+
}
|
|
603
|
+
mkdirSync(dirname(pluginPath), { recursive: true });
|
|
604
|
+
writeFileSync(pluginPath, [
|
|
605
|
+
"export default {",
|
|
606
|
+
" hooks: {",
|
|
607
|
+
" \"session.start\": async ({ $ }) => {",
|
|
608
|
+
" return await $`npx @membank/cli inject`.text();",
|
|
609
|
+
" },",
|
|
610
|
+
" },",
|
|
611
|
+
"};",
|
|
612
|
+
""
|
|
613
|
+
].join("\n"), "utf8");
|
|
614
|
+
return { status: "written" };
|
|
615
|
+
} }
|
|
391
616
|
};
|
|
392
|
-
|
|
393
|
-
var HarnessConfigWriter = class {
|
|
617
|
+
var InjectionHookWriter = class {
|
|
394
618
|
#resolver;
|
|
395
619
|
constructor(resolver = defaultPathResolver) {
|
|
396
620
|
this.#resolver = resolver;
|
|
397
621
|
}
|
|
398
|
-
write(harness
|
|
622
|
+
write(harness) {
|
|
399
623
|
const writer = writers[harness];
|
|
400
|
-
if (!writer)
|
|
401
|
-
|
|
402
|
-
const existing = readJson(path);
|
|
403
|
-
if (!overwrite && writer.isConfigured(existing)) return { status: "already-configured" };
|
|
404
|
-
writeJsonAtomic(path, writer.merge(existing));
|
|
405
|
-
return { status: "written" };
|
|
624
|
+
if (!writer) return { status: "not-supported" };
|
|
625
|
+
return writer.write(this.#resolver);
|
|
406
626
|
}
|
|
407
627
|
};
|
|
408
628
|
//#endregion
|
|
@@ -471,34 +691,33 @@ var ModelDownloader = class extends EventEmitter {
|
|
|
471
691
|
};
|
|
472
692
|
//#endregion
|
|
473
693
|
//#region src/setup/harness-detector.ts
|
|
474
|
-
const defaultResolver = {
|
|
475
|
-
homeDir: homedir,
|
|
476
|
-
cwd: () => process.cwd()
|
|
477
|
-
};
|
|
694
|
+
const defaultResolver = { homeDir: homedir };
|
|
478
695
|
function harnessConfigs(resolver) {
|
|
479
696
|
const home = resolver.homeDir();
|
|
480
|
-
const cwd = resolver.cwd();
|
|
481
697
|
return [
|
|
482
698
|
{
|
|
483
699
|
name: "claude-code",
|
|
484
|
-
configPath: join(home, ".claude
|
|
700
|
+
configPath: join(home, ".claude.json"),
|
|
701
|
+
fallbackPaths: [join(home, ".claude", "settings.json")]
|
|
485
702
|
},
|
|
486
703
|
{
|
|
487
|
-
name: "
|
|
488
|
-
configPath: join(
|
|
704
|
+
name: "copilot",
|
|
705
|
+
configPath: join(home, ".copilot", "mcp-config.json")
|
|
489
706
|
},
|
|
490
707
|
{
|
|
491
708
|
name: "codex",
|
|
492
|
-
configPath: join(home, ".codex", "config.
|
|
709
|
+
configPath: join(home, ".codex", "config.toml"),
|
|
710
|
+
fallbackPaths: [join(home, ".codex", "config.json")]
|
|
493
711
|
},
|
|
494
712
|
{
|
|
495
713
|
name: "opencode",
|
|
496
|
-
configPath: join(home, ".config", "opencode", "
|
|
714
|
+
configPath: join(home, ".config", "opencode", "opencode.json"),
|
|
715
|
+
fallbackPaths: [join(home, ".config", "opencode", "config.json")]
|
|
497
716
|
}
|
|
498
717
|
];
|
|
499
718
|
}
|
|
500
719
|
function detectHarnesses(resolver = defaultResolver) {
|
|
501
|
-
return harnessConfigs(resolver).filter((h) => existsSync(h.configPath)).map((h) => ({
|
|
720
|
+
return harnessConfigs(resolver).filter((h) => existsSync(h.configPath) || (h.fallbackPaths?.some((p) => existsSync(p)) ?? false)).map((h) => ({
|
|
502
721
|
name: h.name,
|
|
503
722
|
configPath: h.configPath
|
|
504
723
|
}));
|
|
@@ -525,6 +744,7 @@ function defaultPrompter(question) {
|
|
|
525
744
|
var SetupOrchestrator = class {
|
|
526
745
|
#detector;
|
|
527
746
|
#writer;
|
|
747
|
+
#hookWriter;
|
|
528
748
|
#prompter;
|
|
529
749
|
#modelDownloader;
|
|
530
750
|
#out;
|
|
@@ -532,6 +752,7 @@ var SetupOrchestrator = class {
|
|
|
532
752
|
constructor(deps) {
|
|
533
753
|
this.#detector = deps.detector ?? (() => detectHarnesses());
|
|
534
754
|
this.#writer = deps.writer;
|
|
755
|
+
this.#hookWriter = deps.hookWriter;
|
|
535
756
|
this.#prompter = deps.prompter ?? defaultPrompter;
|
|
536
757
|
this.#modelDownloader = deps.modelDownloader;
|
|
537
758
|
this.#out = deps.out ?? ((msg) => process.stdout.write(`${msg}\n`));
|
|
@@ -553,6 +774,7 @@ var SetupOrchestrator = class {
|
|
|
553
774
|
if (json) this.#out(JSON.stringify({
|
|
554
775
|
detectedHarnesses: [],
|
|
555
776
|
configuredHarnesses: [],
|
|
777
|
+
injectionHooksConfigured: [],
|
|
556
778
|
modelDownloaded: false
|
|
557
779
|
}));
|
|
558
780
|
return [];
|
|
@@ -564,7 +786,10 @@ var SetupOrchestrator = class {
|
|
|
564
786
|
}
|
|
565
787
|
if (dryRun) {
|
|
566
788
|
out("Planned changes (dry-run — no files written):");
|
|
567
|
-
for (const h of detected)
|
|
789
|
+
for (const h of detected) {
|
|
790
|
+
out(` ⚠ ${h.name}: would write MCP config`);
|
|
791
|
+
if (this.#hookWriter) out(` ⚠ ${h.name}: would write injection hook config`);
|
|
792
|
+
}
|
|
568
793
|
out("");
|
|
569
794
|
out(" ⚠ Model download: skipped (dry-run)");
|
|
570
795
|
return detected.map((h) => ({
|
|
@@ -582,7 +807,7 @@ var SetupOrchestrator = class {
|
|
|
582
807
|
for (const h of detected) {
|
|
583
808
|
let writeResult;
|
|
584
809
|
try {
|
|
585
|
-
writeResult = this.#writer.write(h.name);
|
|
810
|
+
writeResult = await this.#writer.write(h.name);
|
|
586
811
|
} catch (err) {
|
|
587
812
|
const msg = err instanceof Error ? err.message : String(err);
|
|
588
813
|
out(` ✗ ${h.name}: ${msg}`);
|
|
@@ -605,7 +830,7 @@ var SetupOrchestrator = class {
|
|
|
605
830
|
continue;
|
|
606
831
|
}
|
|
607
832
|
try {
|
|
608
|
-
this.#writer.write(h.name, { overwrite: true });
|
|
833
|
+
await this.#writer.write(h.name, { overwrite: true });
|
|
609
834
|
out(` ✓ ${h.name}: written (overwritten)`);
|
|
610
835
|
results.push({
|
|
611
836
|
harness: h.name,
|
|
@@ -629,6 +854,21 @@ var SetupOrchestrator = class {
|
|
|
629
854
|
});
|
|
630
855
|
}
|
|
631
856
|
out("");
|
|
857
|
+
const injectionHooksConfigured = [];
|
|
858
|
+
if (this.#hookWriter) {
|
|
859
|
+
for (const h of detected) try {
|
|
860
|
+
const hookResult = this.#hookWriter.write(h.name);
|
|
861
|
+
if (hookResult.status === "not-supported") continue;
|
|
862
|
+
if (hookResult.status === "written") {
|
|
863
|
+
out(` ✓ ${h.name}: injection hook written`);
|
|
864
|
+
injectionHooksConfigured.push(h.name);
|
|
865
|
+
} else out(` ⚠ ${h.name}: injection hook already configured`);
|
|
866
|
+
} catch (err) {
|
|
867
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
868
|
+
out(` ✗ ${h.name} injection hook: ${msg}`);
|
|
869
|
+
}
|
|
870
|
+
out("");
|
|
871
|
+
}
|
|
632
872
|
let modelDownloaded = false;
|
|
633
873
|
if (this.#modelDownloader) modelDownloaded = !(await this.#runModelDownload(this.#modelDownloader, out)).skipped;
|
|
634
874
|
else out("Model download step: see DRA-52");
|
|
@@ -639,6 +879,7 @@ var SetupOrchestrator = class {
|
|
|
639
879
|
const output = {
|
|
640
880
|
detectedHarnesses: detected.map((h) => h.name),
|
|
641
881
|
configuredHarnesses: results.filter((r) => r.status === "written").map((r) => r.harness),
|
|
882
|
+
injectionHooksConfigured,
|
|
642
883
|
modelDownloaded
|
|
643
884
|
};
|
|
644
885
|
this.#out(JSON.stringify(output));
|
|
@@ -679,7 +920,7 @@ const program = new Command();
|
|
|
679
920
|
program.name("membank").description("LLM memory management system").option("--json", "emit machine-readable JSON only").option("-y, --yes", "skip all confirmation prompts").option("--mcp", "start the MCP stdio server (for harness integration)");
|
|
680
921
|
program.command("query <queryText>").description("search memories by semantic similarity").option("--type <type>", "filter by memory type (correction|preference|decision|learning|fact)").option("--limit <n>", "maximum number of results", "10").action(async (queryText, cmdOptions) => {
|
|
681
922
|
const globalOpts = program.opts();
|
|
682
|
-
const formatter = Formatter.create(
|
|
923
|
+
const formatter = Formatter.create(globalOpts.json === true);
|
|
683
924
|
try {
|
|
684
925
|
await queryCommand(queryText, cmdOptions, formatter);
|
|
685
926
|
} catch (err) {
|
|
@@ -689,7 +930,7 @@ program.command("query <queryText>").description("search memories by semantic si
|
|
|
689
930
|
});
|
|
690
931
|
program.command("list").description("list memories with optional filters").option("--type <type>", "filter by memory type (correction|preference|decision|learning|fact)").option("--pinned", "return only pinned memories").action(async (cmdOptions) => {
|
|
691
932
|
const globalOpts = program.opts();
|
|
692
|
-
const formatter = Formatter.create(
|
|
933
|
+
const formatter = Formatter.create(globalOpts.json === true);
|
|
693
934
|
try {
|
|
694
935
|
await listCommand(cmdOptions, formatter);
|
|
695
936
|
} catch (err) {
|
|
@@ -699,7 +940,7 @@ program.command("list").description("list memories with optional filters").optio
|
|
|
699
940
|
});
|
|
700
941
|
program.command("stats").description("show memory counts by type, total, and needs_review").action(async () => {
|
|
701
942
|
const globalOpts = program.opts();
|
|
702
|
-
const formatter = Formatter.create(
|
|
943
|
+
const formatter = Formatter.create(globalOpts.json === true);
|
|
703
944
|
try {
|
|
704
945
|
await statsCommand(formatter);
|
|
705
946
|
} catch (err) {
|
|
@@ -709,7 +950,7 @@ program.command("stats").description("show memory counts by type, total, and nee
|
|
|
709
950
|
});
|
|
710
951
|
program.command("delete <id>").description("delete a memory by ID").action(async (id) => {
|
|
711
952
|
const globalOpts = program.opts();
|
|
712
|
-
const formatter = Formatter.create(
|
|
953
|
+
const formatter = Formatter.create(globalOpts.json === true);
|
|
713
954
|
const prompt = new PromptHelper(globalOpts.yes === true || !process.stdout.isTTY);
|
|
714
955
|
const db = DatabaseManager.open();
|
|
715
956
|
try {
|
|
@@ -723,7 +964,7 @@ program.command("delete <id>").description("delete a memory by ID").action(async
|
|
|
723
964
|
});
|
|
724
965
|
program.command("add <content>").description("save a new memory").requiredOption("--type <type>", "memory type (correction|preference|decision|learning|fact)").option("--tags <tags>", "comma-separated tags").option("--scope <scope>", "scope (global or project identifier)").action(async (content, cmdOptions) => {
|
|
725
966
|
const globalOpts = program.opts();
|
|
726
|
-
const formatter = Formatter.create(
|
|
967
|
+
const formatter = Formatter.create(globalOpts.json === true);
|
|
727
968
|
try {
|
|
728
969
|
await addCommand(content, cmdOptions, formatter);
|
|
729
970
|
} catch (err) {
|
|
@@ -733,7 +974,7 @@ program.command("add <content>").description("save a new memory").requiredOption
|
|
|
733
974
|
});
|
|
734
975
|
program.command("pin <id>").description("pin a memory by ID").action((id) => {
|
|
735
976
|
const globalOpts = program.opts();
|
|
736
|
-
const formatter = Formatter.create(
|
|
977
|
+
const formatter = Formatter.create(globalOpts.json === true);
|
|
737
978
|
try {
|
|
738
979
|
pinCommand(id, formatter);
|
|
739
980
|
} catch (err) {
|
|
@@ -743,7 +984,7 @@ program.command("pin <id>").description("pin a memory by ID").action((id) => {
|
|
|
743
984
|
});
|
|
744
985
|
program.command("unpin <id>").description("unpin a memory by ID").action((id) => {
|
|
745
986
|
const globalOpts = program.opts();
|
|
746
|
-
const formatter = Formatter.create(
|
|
987
|
+
const formatter = Formatter.create(globalOpts.json === true);
|
|
747
988
|
try {
|
|
748
989
|
unpinCommand(id, formatter);
|
|
749
990
|
} catch (err) {
|
|
@@ -753,7 +994,7 @@ program.command("unpin <id>").description("unpin a memory by ID").action((id) =>
|
|
|
753
994
|
});
|
|
754
995
|
program.command("export").description("export all memories to a JSON file").option("--output <path>", "output file path (default: membank-export-<timestamp>.json in cwd)").action((cmdOptions) => {
|
|
755
996
|
const globalOpts = program.opts();
|
|
756
|
-
const formatter = Formatter.create(
|
|
997
|
+
const formatter = Formatter.create(globalOpts.json === true);
|
|
757
998
|
const db = DatabaseManager.open();
|
|
758
999
|
try {
|
|
759
1000
|
exportCommand(db, formatter, cmdOptions);
|
|
@@ -766,7 +1007,7 @@ program.command("export").description("export all memories to a JSON file").opti
|
|
|
766
1007
|
});
|
|
767
1008
|
program.command("import <file>").description("import memories from a JSON export file").action(async (file) => {
|
|
768
1009
|
const globalOpts = program.opts();
|
|
769
|
-
const formatter = Formatter.create(
|
|
1010
|
+
const formatter = Formatter.create(globalOpts.json === true);
|
|
770
1011
|
const prompt = new PromptHelper(globalOpts.yes === true || !process.stdout.isTTY);
|
|
771
1012
|
const db = DatabaseManager.open();
|
|
772
1013
|
try {
|
|
@@ -778,21 +1019,30 @@ program.command("import <file>").description("import memories from a JSON export
|
|
|
778
1019
|
db.close();
|
|
779
1020
|
}
|
|
780
1021
|
});
|
|
1022
|
+
program.command("inject").description("output session context for harness injection (used by setup hooks)").option("--harness <name>", "format output for a specific harness (claude-code|copilot-cli|codex|opencode)").option("--scope <scope>", "project scope override (default: auto-detect from git remote)").action(async (cmdOptions) => {
|
|
1023
|
+
try {
|
|
1024
|
+
await injectCommand(cmdOptions);
|
|
1025
|
+
} catch (err) {
|
|
1026
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
1027
|
+
process.exit(2);
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
781
1030
|
program.command("setup").description("detect installed harnesses and write MCP config for each").option("--yes", "skip all confirmation prompts").option("--dry-run", "print planned changes without writing any file").option("--harness <name>", "target only the named harness (skip detection)").action(async (cmdOptions) => {
|
|
782
1031
|
const globalOpts = program.opts();
|
|
783
1032
|
const autoYes = cmdOptions.yes === true || globalOpts.yes === true;
|
|
784
|
-
const
|
|
785
|
-
const formatter = Formatter.create().withJson(useJson);
|
|
1033
|
+
const formatter = Formatter.create(globalOpts.json === true);
|
|
786
1034
|
if (cmdOptions.harness !== void 0) {
|
|
787
|
-
if (!SUPPORTED_HARNESSES.
|
|
1035
|
+
if (!SUPPORTED_HARNESSES.some((h) => h === cmdOptions.harness)) {
|
|
788
1036
|
formatter.error(`Unknown harness: "${cmdOptions.harness}". Supported: ${SUPPORTED_HARNESSES.join(", ")}`);
|
|
789
1037
|
process.exit(1);
|
|
790
1038
|
}
|
|
791
1039
|
}
|
|
792
1040
|
const writer = new HarnessConfigWriter();
|
|
1041
|
+
const hookWriter = new InjectionHookWriter();
|
|
793
1042
|
const promptHelper = new PromptHelper(autoYes);
|
|
794
1043
|
const orchestrator = new SetupOrchestrator({
|
|
795
1044
|
writer,
|
|
1045
|
+
hookWriter,
|
|
796
1046
|
prompter: (question) => promptHelper.confirm(question),
|
|
797
1047
|
modelDownloader: new ModelDownloader()
|
|
798
1048
|
});
|
|
@@ -801,7 +1051,7 @@ program.command("setup").description("detect installed harnesses and write MCP c
|
|
|
801
1051
|
yes: autoYes,
|
|
802
1052
|
dryRun: cmdOptions.dryRun,
|
|
803
1053
|
harness: cmdOptions.harness,
|
|
804
|
-
json:
|
|
1054
|
+
json: formatter.isJson
|
|
805
1055
|
})).some((r) => r.status === "error")) process.exit(1);
|
|
806
1056
|
} catch (err) {
|
|
807
1057
|
formatter.error(err instanceof Error ? err.message : String(err));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@membank/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"@huggingface/transformers": "^4.2.0",
|
|
18
18
|
"commander": "^14.0.3",
|
|
19
19
|
"ora": "^9.4.0",
|
|
20
|
-
"@membank/
|
|
21
|
-
"@membank/
|
|
20
|
+
"@membank/mcp": "0.0.4",
|
|
21
|
+
"@membank/core": "0.0.4"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/node": "^25.6.0",
|