@membank/cli 0.1.1 → 0.2.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/README.md +154 -0
- package/dist/index.mjs +348 -146
- package/package.json +3 -3
package/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# @membank/cli
|
|
2
|
+
|
|
3
|
+
CLI and npx entrypoint for membank. Manages memories from the terminal and starts the MCP server for LLM harnesses.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @membank/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or use without installing:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx @membank/cli <command>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Setup
|
|
18
|
+
|
|
19
|
+
Run once to configure your LLM harness:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
membank setup
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
This auto-detects installed harnesses (Claude Code, GitHub Copilot CLI, Codex, OpenCode), writes MCP server config, installs session hooks, and downloads the embedding model (~33 MB).
|
|
26
|
+
|
|
27
|
+
Options:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
--harness <name> Target a specific harness instead of auto-detecting
|
|
31
|
+
--yes Skip confirmation prompts
|
|
32
|
+
--dry-run Preview changes without writing files
|
|
33
|
+
--json Machine-readable output
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Supported harnesses: `claude-code`, `copilot`, `codex`, `opencode`
|
|
37
|
+
|
|
38
|
+
## Commands
|
|
39
|
+
|
|
40
|
+
### `membank query <text>`
|
|
41
|
+
|
|
42
|
+
Semantic search over stored memories.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
membank query "how to run pnpm in one package"
|
|
46
|
+
membank query "auth decisions" --type decision --limit 5
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Options: `--type <type>`, `--limit <n>` (default 10)
|
|
50
|
+
|
|
51
|
+
### `membank add <content>`
|
|
52
|
+
|
|
53
|
+
Save a new memory.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
membank add "Use --filter flag for scoped pnpm commands" --type preference --tags "pnpm,monorepo"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Required: `--type <type>`
|
|
60
|
+
Options: `--tags <a,b,c>`, `--scope <scope>`
|
|
61
|
+
|
|
62
|
+
### `membank list`
|
|
63
|
+
|
|
64
|
+
List stored memories.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
membank list
|
|
68
|
+
membank list --type correction
|
|
69
|
+
membank list --pinned
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Options: `--type <type>`, `--pinned`
|
|
73
|
+
|
|
74
|
+
### `membank stats`
|
|
75
|
+
|
|
76
|
+
Show memory counts by type.
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
membank stats
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### `membank pin <id>` / `membank unpin <id>`
|
|
83
|
+
|
|
84
|
+
Pin a memory so it's always injected at session start.
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
membank pin abc123
|
|
88
|
+
membank unpin abc123
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### `membank delete <id>`
|
|
92
|
+
|
|
93
|
+
Delete a memory. Prompts for confirmation unless `--yes` is passed.
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
membank delete abc123
|
|
97
|
+
membank delete abc123 --yes
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### `membank export`
|
|
101
|
+
|
|
102
|
+
Export all memories to a JSON file.
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
membank export
|
|
106
|
+
membank export --output my-backup.json
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Default filename: `membank-export-<timestamp>.json`
|
|
110
|
+
|
|
111
|
+
### `membank import <file>`
|
|
112
|
+
|
|
113
|
+
Import memories from an export file.
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
membank import membank-export-2025-01-01.json
|
|
117
|
+
membank import membank-export-2025-01-01.json --yes
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### `membank inject`
|
|
121
|
+
|
|
122
|
+
Output session context formatted for a harness. Called automatically by session hooks — you don't normally run this directly.
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
membank inject --harness claude-code --scope <project-scope>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Global flags
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
--json Output machine-readable JSON
|
|
132
|
+
--yes, -y Skip confirmation prompts
|
|
133
|
+
--mcp Start MCP stdio server (used by harness config)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## MCP server mode
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
membank --mcp
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Starts the stdio MCP server. This is what harnesses connect to — `setup` writes this command into harness configs automatically.
|
|
143
|
+
|
|
144
|
+
## Session hooks
|
|
145
|
+
|
|
146
|
+
`setup` installs two hooks:
|
|
147
|
+
|
|
148
|
+
**Session start** — calls `membank inject` to prepend pinned memories into the LLM context at the beginning of every session.
|
|
149
|
+
|
|
150
|
+
**Session stop (Claude Code only)** — prompts the LLM to review the session and call `save_memory` for any notable corrections, preferences, or decisions.
|
|
151
|
+
|
|
152
|
+
## Requirements
|
|
153
|
+
|
|
154
|
+
- Node.js >=24
|
package/dist/index.mjs
CHANGED
|
@@ -4,9 +4,9 @@ import { startServer } from "@membank/mcp";
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
|
+
import { homedir, tmpdir } from "node:os";
|
|
7
8
|
import * as readline from "node:readline";
|
|
8
9
|
import { createInterface } from "node:readline";
|
|
9
|
-
import { homedir, tmpdir } from "node:os";
|
|
10
10
|
import { execFile } from "node:child_process";
|
|
11
11
|
import { promisify } from "node:util";
|
|
12
12
|
import { EventEmitter } from "node:events";
|
|
@@ -225,6 +225,303 @@ async function statsCommand(formatter) {
|
|
|
225
225
|
}
|
|
226
226
|
}
|
|
227
227
|
//#endregion
|
|
228
|
+
//#region src/setup/injection-hook-writer.ts
|
|
229
|
+
const defaultPathResolver$1 = { home: () => {
|
|
230
|
+
const h = process.env.HOME ?? process.env.USERPROFILE;
|
|
231
|
+
if (!h) throw new Error("Cannot determine home directory");
|
|
232
|
+
return h;
|
|
233
|
+
} };
|
|
234
|
+
function readJson$1(path) {
|
|
235
|
+
try {
|
|
236
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
237
|
+
} catch {
|
|
238
|
+
return {};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function writeJsonAtomic$1(path, data) {
|
|
242
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
243
|
+
const tmp = join(mkdtempSync(join(tmpdir(), "membank-hook-")), "cfg.json");
|
|
244
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2));
|
|
245
|
+
renameSync(tmp, path);
|
|
246
|
+
}
|
|
247
|
+
function getHooksArray(group) {
|
|
248
|
+
if (typeof group !== "object" || group === null) return [];
|
|
249
|
+
const h = group.hooks;
|
|
250
|
+
return Array.isArray(h) ? h : [];
|
|
251
|
+
}
|
|
252
|
+
function findMembankHookCommand(hooks, pattern) {
|
|
253
|
+
for (const h of hooks) {
|
|
254
|
+
if (typeof h !== "object" || h === null) continue;
|
|
255
|
+
if ("command" in h && typeof h.command === "string" && h.command.includes(pattern)) return h.command;
|
|
256
|
+
if ("bash" in h && typeof h.bash === "string" && h.bash.includes(pattern)) return h.bash;
|
|
257
|
+
}
|
|
258
|
+
return "";
|
|
259
|
+
}
|
|
260
|
+
function containsMembankInject(hooks) {
|
|
261
|
+
return findMembankHookCommand(hooks, "@membank/cli inject") !== "";
|
|
262
|
+
}
|
|
263
|
+
function extractInjectCommand(hooks) {
|
|
264
|
+
return findMembankHookCommand(hooks, "@membank/cli inject");
|
|
265
|
+
}
|
|
266
|
+
const writers$1 = {
|
|
267
|
+
"claude-code": {
|
|
268
|
+
replacement: "npx @membank/cli inject --harness claude-code",
|
|
269
|
+
write(resolver, overwrite = false) {
|
|
270
|
+
const cfgPath = join(resolver.home(), ".claude", "settings.json");
|
|
271
|
+
const cfg = readJson$1(cfgPath);
|
|
272
|
+
const hooks = cfg.hooks;
|
|
273
|
+
const existingGroups = Array.isArray(hooks?.SessionStart) ? hooks.SessionStart : [];
|
|
274
|
+
const innerHooks = existingGroups.flatMap(getHooksArray);
|
|
275
|
+
if (!overwrite && containsMembankInject(innerHooks)) return {
|
|
276
|
+
status: "already-configured",
|
|
277
|
+
existing: extractInjectCommand(innerHooks),
|
|
278
|
+
replacement: this.replacement
|
|
279
|
+
};
|
|
280
|
+
const filteredGroups = overwrite ? existingGroups.filter((g) => !containsMembankInject(getHooksArray(g))) : existingGroups;
|
|
281
|
+
writeJsonAtomic$1(cfgPath, {
|
|
282
|
+
...cfg,
|
|
283
|
+
hooks: {
|
|
284
|
+
...hooks ?? {},
|
|
285
|
+
SessionStart: [...filteredGroups, {
|
|
286
|
+
matcher: "",
|
|
287
|
+
hooks: [{
|
|
288
|
+
type: "command",
|
|
289
|
+
command: this.replacement
|
|
290
|
+
}]
|
|
291
|
+
}]
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
return { status: "written" };
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
"copilot-cli": {
|
|
298
|
+
replacement: "npx @membank/cli inject --harness copilot-cli",
|
|
299
|
+
write(resolver, overwrite = false) {
|
|
300
|
+
const cfgPath = join(resolver.home(), ".copilot", "settings.json");
|
|
301
|
+
const cfg = readJson$1(cfgPath);
|
|
302
|
+
const hooks = cfg.hooks;
|
|
303
|
+
const existingHooks = Array.isArray(hooks?.sessionStart) ? hooks.sessionStart : [];
|
|
304
|
+
if (!overwrite && containsMembankInject(existingHooks)) return {
|
|
305
|
+
status: "already-configured",
|
|
306
|
+
existing: extractInjectCommand(existingHooks),
|
|
307
|
+
replacement: this.replacement
|
|
308
|
+
};
|
|
309
|
+
const filteredHooks = overwrite ? existingHooks.filter((h) => !containsMembankInject([h])) : existingHooks;
|
|
310
|
+
writeJsonAtomic$1(cfgPath, {
|
|
311
|
+
version: cfg.version ?? 1,
|
|
312
|
+
...cfg,
|
|
313
|
+
hooks: {
|
|
314
|
+
...hooks ?? {},
|
|
315
|
+
sessionStart: [...filteredHooks, {
|
|
316
|
+
type: "command",
|
|
317
|
+
bash: this.replacement,
|
|
318
|
+
timeoutSec: 30
|
|
319
|
+
}]
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
return { status: "written" };
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
codex: {
|
|
326
|
+
replacement: "npx @membank/cli inject --harness codex",
|
|
327
|
+
write(resolver, overwrite = false) {
|
|
328
|
+
const cfgPath = join(resolver.home(), ".codex", "hooks.json");
|
|
329
|
+
const cfg = readJson$1(cfgPath);
|
|
330
|
+
const hooks = cfg.hooks;
|
|
331
|
+
const existingGroups = Array.isArray(hooks?.SessionStart) ? hooks.SessionStart : [];
|
|
332
|
+
const innerHooks = existingGroups.flatMap(getHooksArray);
|
|
333
|
+
if (!overwrite && containsMembankInject(innerHooks)) return {
|
|
334
|
+
status: "already-configured",
|
|
335
|
+
existing: extractInjectCommand(innerHooks),
|
|
336
|
+
replacement: this.replacement
|
|
337
|
+
};
|
|
338
|
+
const filteredGroups = overwrite ? existingGroups.filter((g) => !containsMembankInject(getHooksArray(g))) : existingGroups;
|
|
339
|
+
writeJsonAtomic$1(cfgPath, {
|
|
340
|
+
...cfg,
|
|
341
|
+
hooks: {
|
|
342
|
+
...hooks ?? {},
|
|
343
|
+
SessionStart: [...filteredGroups, {
|
|
344
|
+
matcher: "",
|
|
345
|
+
hooks: [{
|
|
346
|
+
type: "command",
|
|
347
|
+
command: this.replacement,
|
|
348
|
+
timeout: 30
|
|
349
|
+
}]
|
|
350
|
+
}]
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
return { status: "written" };
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
opencode: {
|
|
357
|
+
replacement: "npx @membank/cli inject",
|
|
358
|
+
write(resolver, overwrite = false) {
|
|
359
|
+
const pluginPath = join(resolver.home(), ".config", "opencode", "plugins", "membank.js");
|
|
360
|
+
if (!overwrite && existsSync(pluginPath)) {
|
|
361
|
+
const existing = readFileSync(pluginPath, "utf8");
|
|
362
|
+
if (existing.includes("@membank/cli inject")) return {
|
|
363
|
+
status: "already-configured",
|
|
364
|
+
existing: existing.trim(),
|
|
365
|
+
replacement: newOpencodePlugin()
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
mkdirSync(dirname(pluginPath), { recursive: true });
|
|
369
|
+
writeFileSync(pluginPath, `${newOpencodePlugin()}\n`, "utf8");
|
|
370
|
+
return { status: "written" };
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
function newOpencodePlugin(includeIdle = false) {
|
|
375
|
+
return [
|
|
376
|
+
"export default {",
|
|
377
|
+
" hooks: {",
|
|
378
|
+
" \"session.start\": async ({ $ }) => {",
|
|
379
|
+
" return await $`npx @membank/cli inject`.text();",
|
|
380
|
+
" },",
|
|
381
|
+
...includeIdle ? [
|
|
382
|
+
" \"session.idle\": async ({ $ }) => {",
|
|
383
|
+
" return await $`npx @membank/cli stop-hook --harness opencode`.text();",
|
|
384
|
+
" },"
|
|
385
|
+
] : [],
|
|
386
|
+
" },",
|
|
387
|
+
"};"
|
|
388
|
+
].join("\n");
|
|
389
|
+
}
|
|
390
|
+
const STOP_HOOK_PROMPT = "Review this session and consider whether any user preferences, corrections, decisions, or learnings are worth saving for future sessions. If so, use the save_memory MCP tool to store them. Be selective — only save what would genuinely help in a future conversation. Skip ephemeral task details.";
|
|
391
|
+
function containsMembankStopHookCmd(hooks) {
|
|
392
|
+
return findMembankHookCommand(hooks, "@membank/cli stop-hook") !== "";
|
|
393
|
+
}
|
|
394
|
+
function extractStopHookCmd(hooks) {
|
|
395
|
+
return findMembankHookCommand(hooks, "@membank/cli stop-hook");
|
|
396
|
+
}
|
|
397
|
+
function containsMembankStopPrompt(stopGroups) {
|
|
398
|
+
return stopGroups.some((g) => getHooksArray(g).some((h) => typeof h === "object" && h !== null && "type" in h && h.type === "prompt" && "prompt" in h && typeof h.prompt === "string" && h.prompt.includes("save_memory")));
|
|
399
|
+
}
|
|
400
|
+
function extractStopPrompt(stopGroups) {
|
|
401
|
+
for (const g of stopGroups) for (const h of getHooksArray(g)) if (typeof h === "object" && h !== null && "type" in h && h.type === "prompt" && "prompt" in h && typeof h.prompt === "string" && h.prompt.includes("save_memory")) return h.prompt;
|
|
402
|
+
return "";
|
|
403
|
+
}
|
|
404
|
+
const stopHookWriters = {
|
|
405
|
+
"claude-code": { write(resolver, overwrite = false) {
|
|
406
|
+
const cfgPath = join(resolver.home(), ".claude", "settings.json");
|
|
407
|
+
const cfg = readJson$1(cfgPath);
|
|
408
|
+
const hooks = cfg.hooks;
|
|
409
|
+
const existingStop = Array.isArray(hooks?.Stop) ? hooks.Stop : [];
|
|
410
|
+
if (!overwrite && containsMembankStopPrompt(existingStop)) return {
|
|
411
|
+
status: "already-configured",
|
|
412
|
+
existing: extractStopPrompt(existingStop),
|
|
413
|
+
replacement: STOP_HOOK_PROMPT
|
|
414
|
+
};
|
|
415
|
+
const filteredStop = overwrite ? existingStop.filter((g) => !containsMembankStopPrompt([g])) : existingStop;
|
|
416
|
+
writeJsonAtomic$1(cfgPath, {
|
|
417
|
+
...cfg,
|
|
418
|
+
hooks: {
|
|
419
|
+
...hooks ?? {},
|
|
420
|
+
Stop: [...filteredStop, { hooks: [{
|
|
421
|
+
type: "prompt",
|
|
422
|
+
prompt: STOP_HOOK_PROMPT
|
|
423
|
+
}] }]
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
return { status: "written" };
|
|
427
|
+
} },
|
|
428
|
+
"copilot-cli": { write(resolver, overwrite = false) {
|
|
429
|
+
const cfgPath = join(resolver.home(), ".copilot", "settings.json");
|
|
430
|
+
const cfg = readJson$1(cfgPath);
|
|
431
|
+
const replacement = "npx @membank/cli stop-hook --harness copilot-cli";
|
|
432
|
+
const hooks = cfg.hooks;
|
|
433
|
+
const existingStop = Array.isArray(hooks?.stop) ? hooks.stop : [];
|
|
434
|
+
if (!overwrite && containsMembankStopHookCmd(existingStop)) return {
|
|
435
|
+
status: "already-configured",
|
|
436
|
+
existing: extractStopHookCmd(existingStop),
|
|
437
|
+
replacement
|
|
438
|
+
};
|
|
439
|
+
const filteredStop = overwrite ? existingStop.filter((h) => !containsMembankStopHookCmd([h])) : existingStop;
|
|
440
|
+
writeJsonAtomic$1(cfgPath, {
|
|
441
|
+
version: cfg.version ?? 1,
|
|
442
|
+
...cfg,
|
|
443
|
+
hooks: {
|
|
444
|
+
...hooks ?? {},
|
|
445
|
+
stop: [...filteredStop, {
|
|
446
|
+
type: "command",
|
|
447
|
+
bash: replacement,
|
|
448
|
+
timeoutSec: 30
|
|
449
|
+
}]
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
return { status: "written" };
|
|
453
|
+
} },
|
|
454
|
+
codex: { write(resolver, overwrite = false) {
|
|
455
|
+
const cfgPath = join(resolver.home(), ".codex", "hooks.json");
|
|
456
|
+
const cfg = readJson$1(cfgPath);
|
|
457
|
+
const replacement = "npx @membank/cli stop-hook --harness codex";
|
|
458
|
+
const hooks = cfg.hooks;
|
|
459
|
+
const existingGroups = Array.isArray(hooks?.Stop) ? hooks.Stop : [];
|
|
460
|
+
const innerHooks = existingGroups.flatMap(getHooksArray);
|
|
461
|
+
if (!overwrite && containsMembankStopHookCmd(innerHooks)) return {
|
|
462
|
+
status: "already-configured",
|
|
463
|
+
existing: extractStopHookCmd(innerHooks),
|
|
464
|
+
replacement
|
|
465
|
+
};
|
|
466
|
+
const filteredGroups = overwrite ? existingGroups.filter((g) => !containsMembankStopHookCmd(getHooksArray(g))) : existingGroups;
|
|
467
|
+
writeJsonAtomic$1(cfgPath, {
|
|
468
|
+
...cfg,
|
|
469
|
+
hooks: {
|
|
470
|
+
...hooks ?? {},
|
|
471
|
+
Stop: [...filteredGroups, {
|
|
472
|
+
matcher: "",
|
|
473
|
+
hooks: [{
|
|
474
|
+
type: "command",
|
|
475
|
+
command: replacement,
|
|
476
|
+
timeout: 30
|
|
477
|
+
}]
|
|
478
|
+
}]
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
return { status: "written" };
|
|
482
|
+
} },
|
|
483
|
+
opencode: { write(resolver, overwrite = false) {
|
|
484
|
+
const pluginPath = join(resolver.home(), ".config", "opencode", "plugins", "membank.js");
|
|
485
|
+
if (!overwrite && existsSync(pluginPath)) {
|
|
486
|
+
const existing = readFileSync(pluginPath, "utf8");
|
|
487
|
+
if (existing.includes("@membank/cli stop-hook")) return {
|
|
488
|
+
status: "already-configured",
|
|
489
|
+
existing: existing.trim(),
|
|
490
|
+
replacement: newOpencodePlugin(true)
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
mkdirSync(dirname(pluginPath), { recursive: true });
|
|
494
|
+
writeFileSync(pluginPath, `${newOpencodePlugin(true)}\n`, "utf8");
|
|
495
|
+
return { status: "written" };
|
|
496
|
+
} }
|
|
497
|
+
};
|
|
498
|
+
var InjectionHookWriter = class {
|
|
499
|
+
#resolver;
|
|
500
|
+
constructor(resolver = defaultPathResolver$1) {
|
|
501
|
+
this.#resolver = resolver;
|
|
502
|
+
}
|
|
503
|
+
write(harness, overwrite) {
|
|
504
|
+
const writer = writers$1[harness];
|
|
505
|
+
if (!writer) return { status: "not-supported" };
|
|
506
|
+
return writer.write(this.#resolver, overwrite);
|
|
507
|
+
}
|
|
508
|
+
writeStopHook(harness, overwrite) {
|
|
509
|
+
const writer = stopHookWriters[harness];
|
|
510
|
+
if (!writer) return { status: "not-supported" };
|
|
511
|
+
return writer.write(this.#resolver, overwrite);
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
//#endregion
|
|
515
|
+
//#region src/commands/stop-hook.ts
|
|
516
|
+
function stopHookCommand(opts) {
|
|
517
|
+
const { harness } = opts;
|
|
518
|
+
if (harness === "copilot-cli" || harness === "codex") {
|
|
519
|
+
process.stdout.write(JSON.stringify({ systemMessage: STOP_HOOK_PROMPT }));
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
process.stdout.write(`${STOP_HOOK_PROMPT}\n`);
|
|
523
|
+
}
|
|
524
|
+
//#endregion
|
|
228
525
|
//#region src/commands/unpin.ts
|
|
229
526
|
function unpinCommand(id, formatter, db) {
|
|
230
527
|
const ownDb = db === void 0;
|
|
@@ -371,7 +668,7 @@ async function execFileNoThrow(cmd, args) {
|
|
|
371
668
|
}
|
|
372
669
|
//#endregion
|
|
373
670
|
//#region src/setup/harness-config-writer.ts
|
|
374
|
-
const defaultPathResolver
|
|
671
|
+
const defaultPathResolver = {
|
|
375
672
|
home: () => {
|
|
376
673
|
const h = process.env.HOME ?? process.env.USERPROFILE;
|
|
377
674
|
if (!h) throw new Error("Cannot determine home directory");
|
|
@@ -379,14 +676,14 @@ const defaultPathResolver$1 = {
|
|
|
379
676
|
},
|
|
380
677
|
cwd: () => process.cwd()
|
|
381
678
|
};
|
|
382
|
-
function readJson
|
|
679
|
+
function readJson(path) {
|
|
383
680
|
try {
|
|
384
681
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
385
682
|
} catch {
|
|
386
683
|
return {};
|
|
387
684
|
}
|
|
388
685
|
}
|
|
389
|
-
function writeJsonAtomic
|
|
686
|
+
function writeJsonAtomic(path, data) {
|
|
390
687
|
mkdirSync(dirname(path), { recursive: true });
|
|
391
688
|
const tmp = join(mkdtempSync(join(tmpdir(), "membank-")), "cfg.json");
|
|
392
689
|
writeFileSync(tmp, JSON.stringify(data, null, 2));
|
|
@@ -403,9 +700,9 @@ const MEMBANK_NPX_ARGS = [
|
|
|
403
700
|
"@membank/cli@latest",
|
|
404
701
|
"--mcp"
|
|
405
702
|
];
|
|
406
|
-
const writers
|
|
703
|
+
const writers = {
|
|
407
704
|
"claude-code": { async write(resolver, run, { overwrite = false } = {}) {
|
|
408
|
-
const configured = hasKey(readJson
|
|
705
|
+
const configured = hasKey(readJson(join(resolver.home(), ".claude.json")).mcpServers, "membank");
|
|
409
706
|
if (configured && !overwrite) return { status: "already-configured" };
|
|
410
707
|
if (configured) {
|
|
411
708
|
const remove = await run("claude", [
|
|
@@ -433,9 +730,9 @@ const writers$1 = {
|
|
|
433
730
|
} },
|
|
434
731
|
copilot: { async write(resolver, _run, { overwrite = false } = {}) {
|
|
435
732
|
const cfgPath = join(resolver.home(), ".copilot", "mcp-config.json");
|
|
436
|
-
const cfg = readJson
|
|
733
|
+
const cfg = readJson(cfgPath);
|
|
437
734
|
if (hasKey(cfg.mcpServers, "membank") && !overwrite) return { status: "already-configured" };
|
|
438
|
-
writeJsonAtomic
|
|
735
|
+
writeJsonAtomic(cfgPath, {
|
|
439
736
|
...cfg,
|
|
440
737
|
mcpServers: {
|
|
441
738
|
...cfg.mcpServers,
|
|
@@ -474,9 +771,9 @@ const writers$1 = {
|
|
|
474
771
|
} },
|
|
475
772
|
opencode: { async write(resolver, _run, { overwrite = false } = {}) {
|
|
476
773
|
const cfgPath = join(resolver.home(), ".config", "opencode", "opencode.json");
|
|
477
|
-
const cfg = readJson
|
|
774
|
+
const cfg = readJson(cfgPath);
|
|
478
775
|
if (hasKey(cfg.mcp, "membank") && !overwrite) return { status: "already-configured" };
|
|
479
|
-
writeJsonAtomic
|
|
776
|
+
writeJsonAtomic(cfgPath, {
|
|
480
777
|
...cfg,
|
|
481
778
|
mcp: {
|
|
482
779
|
...cfg.mcp,
|
|
@@ -493,142 +790,21 @@ const writers$1 = {
|
|
|
493
790
|
return { status: "written" };
|
|
494
791
|
} }
|
|
495
792
|
};
|
|
496
|
-
const SUPPORTED_HARNESSES = Object.keys(writers
|
|
793
|
+
const SUPPORTED_HARNESSES = Object.keys(writers);
|
|
497
794
|
var HarnessConfigWriter = class {
|
|
498
795
|
#resolver;
|
|
499
796
|
#run;
|
|
500
|
-
constructor(resolver = defaultPathResolver
|
|
797
|
+
constructor(resolver = defaultPathResolver, run = execFileNoThrow) {
|
|
501
798
|
this.#resolver = resolver;
|
|
502
799
|
this.#run = run;
|
|
503
800
|
}
|
|
504
801
|
async write(harness, { overwrite = false } = {}) {
|
|
505
|
-
const writer = writers
|
|
802
|
+
const writer = writers[harness];
|
|
506
803
|
if (!writer) throw new Error(`Unknown harness: ${harness}`);
|
|
507
804
|
return writer.write(this.#resolver, this.#run, { overwrite });
|
|
508
805
|
}
|
|
509
806
|
};
|
|
510
807
|
//#endregion
|
|
511
|
-
//#region src/setup/injection-hook-writer.ts
|
|
512
|
-
const defaultPathResolver = { home: () => {
|
|
513
|
-
const h = process.env.HOME ?? process.env.USERPROFILE;
|
|
514
|
-
if (!h) throw new Error("Cannot determine home directory");
|
|
515
|
-
return h;
|
|
516
|
-
} };
|
|
517
|
-
function readJson(path) {
|
|
518
|
-
try {
|
|
519
|
-
return JSON.parse(readFileSync(path, "utf8"));
|
|
520
|
-
} catch {
|
|
521
|
-
return {};
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
function writeJsonAtomic(path, data) {
|
|
525
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
526
|
-
const tmp = join(mkdtempSync(join(tmpdir(), "membank-hook-")), "cfg.json");
|
|
527
|
-
writeFileSync(tmp, JSON.stringify(data, null, 2));
|
|
528
|
-
renameSync(tmp, path);
|
|
529
|
-
}
|
|
530
|
-
function containsMembankInject(hooks) {
|
|
531
|
-
if (!Array.isArray(hooks)) return false;
|
|
532
|
-
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")));
|
|
533
|
-
}
|
|
534
|
-
const writers = {
|
|
535
|
-
"claude-code": { write(resolver) {
|
|
536
|
-
const cfgPath = join(resolver.home(), ".claude", "settings.json");
|
|
537
|
-
const cfg = readJson(cfgPath);
|
|
538
|
-
const hooks = cfg.hooks;
|
|
539
|
-
const sessionStart = hooks?.SessionStart;
|
|
540
|
-
if (Array.isArray(sessionStart) && containsMembankInject(sessionStart.flatMap((g) => Array.isArray(g.hooks) ? g.hooks : []))) return { status: "already-configured" };
|
|
541
|
-
const existingSessionStart = Array.isArray(sessionStart) ? sessionStart : [];
|
|
542
|
-
writeJsonAtomic(cfgPath, {
|
|
543
|
-
...cfg,
|
|
544
|
-
hooks: {
|
|
545
|
-
...hooks ?? {},
|
|
546
|
-
SessionStart: [...existingSessionStart, {
|
|
547
|
-
matcher: "",
|
|
548
|
-
hooks: [{
|
|
549
|
-
type: "command",
|
|
550
|
-
command: "npx @membank/cli inject --harness claude-code"
|
|
551
|
-
}]
|
|
552
|
-
}]
|
|
553
|
-
}
|
|
554
|
-
});
|
|
555
|
-
return { status: "written" };
|
|
556
|
-
} },
|
|
557
|
-
"copilot-cli": { write(resolver) {
|
|
558
|
-
const cfgPath = join(resolver.home(), ".copilot", "settings.json");
|
|
559
|
-
const cfg = readJson(cfgPath);
|
|
560
|
-
const hooks = cfg.hooks;
|
|
561
|
-
const sessionStart = hooks?.sessionStart;
|
|
562
|
-
if (Array.isArray(sessionStart) && containsMembankInject(sessionStart)) return { status: "already-configured" };
|
|
563
|
-
const existingSessionStart = Array.isArray(sessionStart) ? sessionStart : [];
|
|
564
|
-
writeJsonAtomic(cfgPath, {
|
|
565
|
-
version: cfg.version ?? 1,
|
|
566
|
-
...cfg,
|
|
567
|
-
hooks: {
|
|
568
|
-
...hooks ?? {},
|
|
569
|
-
sessionStart: [...existingSessionStart, {
|
|
570
|
-
type: "command",
|
|
571
|
-
bash: "npx @membank/cli inject --harness copilot-cli",
|
|
572
|
-
timeoutSec: 30
|
|
573
|
-
}]
|
|
574
|
-
}
|
|
575
|
-
});
|
|
576
|
-
return { status: "written" };
|
|
577
|
-
} },
|
|
578
|
-
codex: { write(resolver) {
|
|
579
|
-
const cfgPath = join(resolver.home(), ".codex", "hooks.json");
|
|
580
|
-
const cfg = readJson(cfgPath);
|
|
581
|
-
const hooks = cfg.hooks;
|
|
582
|
-
const sessionStart = hooks?.SessionStart;
|
|
583
|
-
if (Array.isArray(sessionStart) && containsMembankInject(sessionStart.flatMap((g) => Array.isArray(g.hooks) ? g.hooks : []))) return { status: "already-configured" };
|
|
584
|
-
const existingSessionStart = Array.isArray(sessionStart) ? sessionStart : [];
|
|
585
|
-
writeJsonAtomic(cfgPath, {
|
|
586
|
-
...cfg,
|
|
587
|
-
hooks: {
|
|
588
|
-
...hooks ?? {},
|
|
589
|
-
SessionStart: [...existingSessionStart, {
|
|
590
|
-
matcher: "",
|
|
591
|
-
hooks: [{
|
|
592
|
-
type: "command",
|
|
593
|
-
command: "npx @membank/cli inject --harness codex",
|
|
594
|
-
timeout: 30
|
|
595
|
-
}]
|
|
596
|
-
}]
|
|
597
|
-
}
|
|
598
|
-
});
|
|
599
|
-
return { status: "written" };
|
|
600
|
-
} },
|
|
601
|
-
opencode: { write(resolver) {
|
|
602
|
-
const pluginPath = join(resolver.home(), ".config", "opencode", "plugins", "membank.js");
|
|
603
|
-
if (existsSync(pluginPath)) {
|
|
604
|
-
if (readFileSync(pluginPath, "utf8").includes("@membank/cli inject")) return { status: "already-configured" };
|
|
605
|
-
}
|
|
606
|
-
mkdirSync(dirname(pluginPath), { recursive: true });
|
|
607
|
-
writeFileSync(pluginPath, [
|
|
608
|
-
"export default {",
|
|
609
|
-
" hooks: {",
|
|
610
|
-
" \"session.start\": async ({ $ }) => {",
|
|
611
|
-
" return await $`npx @membank/cli inject`.text();",
|
|
612
|
-
" },",
|
|
613
|
-
" },",
|
|
614
|
-
"};",
|
|
615
|
-
""
|
|
616
|
-
].join("\n"), "utf8");
|
|
617
|
-
return { status: "written" };
|
|
618
|
-
} }
|
|
619
|
-
};
|
|
620
|
-
var InjectionHookWriter = class {
|
|
621
|
-
#resolver;
|
|
622
|
-
constructor(resolver = defaultPathResolver) {
|
|
623
|
-
this.#resolver = resolver;
|
|
624
|
-
}
|
|
625
|
-
write(harness) {
|
|
626
|
-
const writer = writers[harness];
|
|
627
|
-
if (!writer) return { status: "not-supported" };
|
|
628
|
-
return writer.write(this.#resolver);
|
|
629
|
-
}
|
|
630
|
-
};
|
|
631
|
-
//#endregion
|
|
632
808
|
//#region src/setup/model-downloader.ts
|
|
633
809
|
const MODEL_NAME = "Xenova/bge-small-en-v1.5";
|
|
634
810
|
var ModelDownloadError = class extends Error {
|
|
@@ -778,6 +954,7 @@ var SetupOrchestrator = class {
|
|
|
778
954
|
detectedHarnesses: [],
|
|
779
955
|
configuredHarnesses: [],
|
|
780
956
|
injectionHooksConfigured: [],
|
|
957
|
+
stopHooksConfigured: [],
|
|
781
958
|
modelDownloaded: false
|
|
782
959
|
}));
|
|
783
960
|
return [];
|
|
@@ -791,7 +968,10 @@ var SetupOrchestrator = class {
|
|
|
791
968
|
out("Planned changes (dry-run — no files written):");
|
|
792
969
|
for (const h of detected) {
|
|
793
970
|
out(` ⚠ ${h.name}: would write MCP config`);
|
|
794
|
-
if (this.#hookWriter)
|
|
971
|
+
if (this.#hookWriter) {
|
|
972
|
+
out(` ⚠ ${h.name}: would write injection hook config`);
|
|
973
|
+
out(` ⚠ ${h.name}: would write stop hook config`);
|
|
974
|
+
}
|
|
795
975
|
}
|
|
796
976
|
out("");
|
|
797
977
|
out(" ⚠ Model download: skipped (dry-run)");
|
|
@@ -858,18 +1038,11 @@ var SetupOrchestrator = class {
|
|
|
858
1038
|
}
|
|
859
1039
|
out("");
|
|
860
1040
|
const injectionHooksConfigured = [];
|
|
1041
|
+
const stopHooksConfigured = [];
|
|
861
1042
|
if (this.#hookWriter) {
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
if (hookResult.status === "written") {
|
|
866
|
-
out(` ✓ ${h.name}: injection hook written`);
|
|
867
|
-
injectionHooksConfigured.push(h.name);
|
|
868
|
-
} else out(` ⚠ ${h.name}: injection hook already configured`);
|
|
869
|
-
} catch (err) {
|
|
870
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
871
|
-
out(` ✗ ${h.name} injection hook: ${msg}`);
|
|
872
|
-
}
|
|
1043
|
+
const w = this.#hookWriter;
|
|
1044
|
+
injectionHooksConfigured.push(...await this.#runHookLoop(detected, "injection hook", (h, ow) => w.write(h, ow), yes, out));
|
|
1045
|
+
stopHooksConfigured.push(...await this.#runHookLoop(detected, "stop hook", (h, ow) => w.writeStopHook(h, ow), yes, out));
|
|
873
1046
|
out("");
|
|
874
1047
|
}
|
|
875
1048
|
let modelDownloaded = false;
|
|
@@ -883,6 +1056,7 @@ var SetupOrchestrator = class {
|
|
|
883
1056
|
detectedHarnesses: detected.map((h) => h.name),
|
|
884
1057
|
configuredHarnesses: results.filter((r) => r.status === "written").map((r) => r.harness),
|
|
885
1058
|
injectionHooksConfigured,
|
|
1059
|
+
stopHooksConfigured,
|
|
886
1060
|
modelDownloaded
|
|
887
1061
|
};
|
|
888
1062
|
this.#out(JSON.stringify(output));
|
|
@@ -892,6 +1066,31 @@ var SetupOrchestrator = class {
|
|
|
892
1066
|
}
|
|
893
1067
|
return results;
|
|
894
1068
|
}
|
|
1069
|
+
async #runHookLoop(detected, label, write, yes, out) {
|
|
1070
|
+
const configured = [];
|
|
1071
|
+
for (const h of detected) try {
|
|
1072
|
+
const result = write(h.name);
|
|
1073
|
+
if (result.status === "not-supported") continue;
|
|
1074
|
+
if (result.status === "written") {
|
|
1075
|
+
out(` ✓ ${h.name}: ${label} written`);
|
|
1076
|
+
configured.push(h.name);
|
|
1077
|
+
} else {
|
|
1078
|
+
out(` ⚠ ${h.name}: ${label} already configured`);
|
|
1079
|
+
out(` Current: ${result.existing}`);
|
|
1080
|
+
out(` New: ${result.replacement}`);
|
|
1081
|
+
if (yes || await this.#prompter(` Replace ${label} for ${h.name}?`)) {
|
|
1082
|
+
if (write(h.name, true).status === "written") {
|
|
1083
|
+
out(` ✓ ${h.name}: ${label} replaced`);
|
|
1084
|
+
configured.push(h.name);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
} catch (err) {
|
|
1089
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1090
|
+
out(` ✗ ${h.name} ${label}: ${msg}`);
|
|
1091
|
+
}
|
|
1092
|
+
return configured;
|
|
1093
|
+
}
|
|
895
1094
|
async #runModelDownload(downloader, out) {
|
|
896
1095
|
out("Downloading embedding model (bge-small-en-v1.5, ~33 MB)...");
|
|
897
1096
|
downloader.on?.("progress", (p) => {
|
|
@@ -1030,6 +1229,9 @@ program.command("inject").description("output session context for harness inject
|
|
|
1030
1229
|
process.exit(2);
|
|
1031
1230
|
}
|
|
1032
1231
|
});
|
|
1232
|
+
program.command("stop-hook").description("output session-end prompt for harness stop hooks (called by hooks, not users)").option("--harness <name>", "harness name (copilot-cli, codex, opencode)").action((cmdOptions) => {
|
|
1233
|
+
stopHookCommand(cmdOptions);
|
|
1234
|
+
});
|
|
1033
1235
|
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) => {
|
|
1034
1236
|
const globalOpts = program.opts();
|
|
1035
1237
|
const autoYes = cmdOptions.yes === true || globalOpts.yes === true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@membank/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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/core": "0.
|
|
21
|
-
"@membank/mcp": "0.
|
|
20
|
+
"@membank/core": "0.2.0",
|
|
21
|
+
"@membank/mcp": "0.2.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/node": "^25.6.0",
|