@membank/cli 0.1.1 → 0.3.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 +331 -104
- package/package.json +4 -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
|
@@ -2,6 +2,7 @@
|
|
|
2
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
|
+
import { startDashboard } from "@membank/dashboard";
|
|
5
6
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
|
|
6
7
|
import { dirname, join } from "node:path";
|
|
7
8
|
import * as readline from "node:readline";
|
|
@@ -30,6 +31,11 @@ async function addCommand(content, options, formatter, db, embeddingService) {
|
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
33
|
//#endregion
|
|
34
|
+
//#region src/commands/dashboard.ts
|
|
35
|
+
async function dashboardCommand(opts) {
|
|
36
|
+
await startDashboard({ port: opts.port !== void 0 ? parseInt(opts.port, 10) : void 0 });
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
33
39
|
//#region src/commands/delete.ts
|
|
34
40
|
async function deleteCommand(id, db, formatter, prompt) {
|
|
35
41
|
if (db.db.prepare(`SELECT id FROM memories WHERE id = ?`).get(id) === void 0) {
|
|
@@ -140,7 +146,66 @@ function formatContext(ctx) {
|
|
|
140
146
|
lines.push(MEMORY_GUIDANCE);
|
|
141
147
|
return lines.join("\n");
|
|
142
148
|
}
|
|
143
|
-
|
|
149
|
+
function outputAdditionalContext(text, harness, eventName) {
|
|
150
|
+
if (harness === "claude-code") {
|
|
151
|
+
process.stdout.write(JSON.stringify({ hookSpecificOutput: {
|
|
152
|
+
hookEventName: eventName,
|
|
153
|
+
additionalContext: text
|
|
154
|
+
} }));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (harness === "copilot-cli") {
|
|
158
|
+
process.stdout.write(JSON.stringify({ additionalContext: text }));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
process.stdout.write(`${text}\n`);
|
|
162
|
+
}
|
|
163
|
+
async function readStdin() {
|
|
164
|
+
if (process.stdin.isTTY) return "";
|
|
165
|
+
return new Promise((resolve) => {
|
|
166
|
+
const chunks = [];
|
|
167
|
+
const timeout = setTimeout(() => resolve(""), 1e3);
|
|
168
|
+
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
169
|
+
process.stdin.on("end", () => {
|
|
170
|
+
clearTimeout(timeout);
|
|
171
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
172
|
+
});
|
|
173
|
+
process.stdin.on("error", () => {
|
|
174
|
+
clearTimeout(timeout);
|
|
175
|
+
resolve("");
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
const FEEDBACK_PATTERNS = [
|
|
180
|
+
/\bdon'?t\b/i,
|
|
181
|
+
/\bstop\b/i,
|
|
182
|
+
/\bnever\b/i,
|
|
183
|
+
/\balways\b/i,
|
|
184
|
+
/\bremember\b/i,
|
|
185
|
+
/\bprefer\b/i,
|
|
186
|
+
/\bi (like|want|hate|dislike)\b/i,
|
|
187
|
+
/\bfrom now on\b/i,
|
|
188
|
+
/\bkeep in mind\b/i,
|
|
189
|
+
/\bnote that\b/i,
|
|
190
|
+
/\bstop doing\b/i,
|
|
191
|
+
/\bstop using\b/i,
|
|
192
|
+
/\bthat'?s wrong\b/i,
|
|
193
|
+
/\bno,?\s+(actually|that'?s)\b/i,
|
|
194
|
+
/\bplease (don'?t|stop|always|never)\b/i
|
|
195
|
+
];
|
|
196
|
+
function looksLikeFeedback(prompt) {
|
|
197
|
+
return FEEDBACK_PATTERNS.some((p) => p.test(prompt));
|
|
198
|
+
}
|
|
199
|
+
function isToolFailure(data) {
|
|
200
|
+
if (data.hook_event_name === "PostToolUseFailure") return true;
|
|
201
|
+
if (typeof data.error_message === "string" && data.error_message.length > 0) return true;
|
|
202
|
+
const response = data.tool_result ?? data.tool_response;
|
|
203
|
+
if (typeof response === "object" && response !== null) {
|
|
204
|
+
if (response.is_error === true) return true;
|
|
205
|
+
}
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
async function handleSessionStart(opts) {
|
|
144
209
|
const projectScope = opts.scope ?? await resolveScope();
|
|
145
210
|
const db = DatabaseManager.open();
|
|
146
211
|
let text;
|
|
@@ -151,18 +216,44 @@ async function injectCommand(opts) {
|
|
|
151
216
|
}
|
|
152
217
|
if (!text) process.exit(0);
|
|
153
218
|
const harness = opts.harness;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
219
|
+
outputAdditionalContext(text, harness, "SessionStart");
|
|
220
|
+
}
|
|
221
|
+
async function handleUserPrompt(harness) {
|
|
222
|
+
const raw = await readStdin();
|
|
223
|
+
if (!raw) process.exit(0);
|
|
224
|
+
let data;
|
|
225
|
+
try {
|
|
226
|
+
data = JSON.parse(raw);
|
|
227
|
+
} catch {
|
|
228
|
+
process.exit(0);
|
|
229
|
+
}
|
|
230
|
+
if (!looksLikeFeedback(typeof data.prompt === "string" ? data.prompt : "")) process.exit(0);
|
|
231
|
+
outputAdditionalContext("User prompt may contain a correction, preference, or decision worth saving. After responding, evaluate: should this be saved as a memory? If yes, call save_memory with the appropriate type (correction/preference/decision/learning) and scope (global or project).", harness, "UserPromptSubmit");
|
|
232
|
+
}
|
|
233
|
+
async function handleToolFailure(harness) {
|
|
234
|
+
const raw = await readStdin();
|
|
235
|
+
if (!raw) process.exit(0);
|
|
236
|
+
let data;
|
|
237
|
+
try {
|
|
238
|
+
data = JSON.parse(raw);
|
|
239
|
+
} catch {
|
|
240
|
+
process.exit(0);
|
|
241
|
+
}
|
|
242
|
+
if (!isToolFailure(data)) process.exit(0);
|
|
243
|
+
outputAdditionalContext(`Tool "${typeof data.tool_name === "string" ? data.tool_name : "unknown"}" failed. If this reveals a non-obvious constraint, environment issue, or repeatable failure pattern, call save_memory with type "learning" to prevent repeating it.`, harness, "PostToolUseFailure");
|
|
244
|
+
}
|
|
245
|
+
async function injectCommand(opts) {
|
|
246
|
+
const harness = opts.harness;
|
|
247
|
+
const event = opts.event ?? "session-start";
|
|
248
|
+
if (event === "user-prompt") {
|
|
249
|
+
await handleUserPrompt(harness);
|
|
159
250
|
return;
|
|
160
251
|
}
|
|
161
|
-
if (
|
|
162
|
-
|
|
252
|
+
if (event === "tool-failure") {
|
|
253
|
+
await handleToolFailure(harness);
|
|
163
254
|
return;
|
|
164
255
|
}
|
|
165
|
-
|
|
256
|
+
await handleSessionStart(opts);
|
|
166
257
|
}
|
|
167
258
|
//#endregion
|
|
168
259
|
//#region src/commands/list.ts
|
|
@@ -527,105 +618,217 @@ function writeJsonAtomic(path, data) {
|
|
|
527
618
|
writeFileSync(tmp, JSON.stringify(data, null, 2));
|
|
528
619
|
renameSync(tmp, path);
|
|
529
620
|
}
|
|
621
|
+
function getHooksArray(group) {
|
|
622
|
+
if (typeof group !== "object" || group === null) return [];
|
|
623
|
+
const h = group.hooks;
|
|
624
|
+
return Array.isArray(h) ? h : [];
|
|
625
|
+
}
|
|
626
|
+
function findMembankHookCommand(hooks, pattern) {
|
|
627
|
+
for (const h of hooks) {
|
|
628
|
+
if (typeof h !== "object" || h === null) continue;
|
|
629
|
+
if ("command" in h && typeof h.command === "string" && h.command.includes(pattern)) return h.command;
|
|
630
|
+
if ("bash" in h && typeof h.bash === "string" && h.bash.includes(pattern)) return h.bash;
|
|
631
|
+
}
|
|
632
|
+
return "";
|
|
633
|
+
}
|
|
530
634
|
function containsMembankInject(hooks) {
|
|
531
|
-
|
|
532
|
-
|
|
635
|
+
return findMembankHookCommand(hooks, "@membank/cli inject") !== "";
|
|
636
|
+
}
|
|
637
|
+
function extractInjectCommand(hooks) {
|
|
638
|
+
return findMembankHookCommand(hooks, "@membank/cli inject");
|
|
639
|
+
}
|
|
640
|
+
function filterOutMembank(groups) {
|
|
641
|
+
return groups.filter((g) => !containsMembankInject(getHooksArray(g)));
|
|
642
|
+
}
|
|
643
|
+
function filterOutMembankFlat(hooks) {
|
|
644
|
+
return hooks.filter((h) => !containsMembankInject([h]));
|
|
533
645
|
}
|
|
534
646
|
const writers = {
|
|
535
|
-
"claude-code": {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
647
|
+
"claude-code": {
|
|
648
|
+
replacement: "npx @membank/cli inject --harness claude-code",
|
|
649
|
+
write(resolver, overwrite = false) {
|
|
650
|
+
const cfgPath = join(resolver.home(), ".claude", "settings.json");
|
|
651
|
+
const cfg = readJson(cfgPath);
|
|
652
|
+
const hooks = cfg.hooks;
|
|
653
|
+
const existingSessionStart = Array.isArray(hooks?.SessionStart) ? hooks.SessionStart : [];
|
|
654
|
+
const innerHooks = existingSessionStart.flatMap(getHooksArray);
|
|
655
|
+
if (!overwrite && containsMembankInject(innerHooks)) return {
|
|
656
|
+
status: "already-configured",
|
|
657
|
+
existing: extractInjectCommand(innerHooks),
|
|
658
|
+
replacement: this.replacement
|
|
659
|
+
};
|
|
660
|
+
const filteredSessionStart = overwrite ? filterOutMembank(existingSessionStart) : existingSessionStart;
|
|
661
|
+
const existingUserPrompt = Array.isArray(hooks?.UserPromptSubmit) ? hooks.UserPromptSubmit : [];
|
|
662
|
+
const existingToolFailure = Array.isArray(hooks?.PostToolUseFailure) ? hooks.PostToolUseFailure : [];
|
|
663
|
+
writeJsonAtomic(cfgPath, {
|
|
664
|
+
...cfg,
|
|
665
|
+
hooks: {
|
|
666
|
+
...hooks ?? {},
|
|
667
|
+
SessionStart: [...filteredSessionStart, {
|
|
668
|
+
matcher: "",
|
|
669
|
+
hooks: [{
|
|
670
|
+
type: "command",
|
|
671
|
+
command: "npx @membank/cli inject --harness claude-code"
|
|
672
|
+
}]
|
|
673
|
+
}],
|
|
674
|
+
UserPromptSubmit: [...filterOutMembank(existingUserPrompt), {
|
|
675
|
+
matcher: "",
|
|
676
|
+
hooks: [{
|
|
677
|
+
type: "command",
|
|
678
|
+
command: "npx @membank/cli inject --event user-prompt --harness claude-code"
|
|
679
|
+
}]
|
|
680
|
+
}],
|
|
681
|
+
PostToolUseFailure: [...filterOutMembank(existingToolFailure), {
|
|
682
|
+
matcher: "",
|
|
683
|
+
hooks: [{
|
|
684
|
+
type: "command",
|
|
685
|
+
command: "npx @membank/cli inject --event tool-failure --harness claude-code"
|
|
686
|
+
}]
|
|
551
687
|
}]
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
}
|
|
557
|
-
"copilot-cli": {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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: [{
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
return { status: "written" };
|
|
691
|
+
}
|
|
692
|
+
},
|
|
693
|
+
"copilot-cli": {
|
|
694
|
+
replacement: "npx @membank/cli inject --harness copilot-cli",
|
|
695
|
+
write(resolver, overwrite = false) {
|
|
696
|
+
const cfgPath = join(resolver.home(), ".copilot", "settings.json");
|
|
697
|
+
const cfg = readJson(cfgPath);
|
|
698
|
+
const hooks = cfg.hooks;
|
|
699
|
+
const existingSessionStart = Array.isArray(hooks?.sessionStart) ? hooks.sessionStart : [];
|
|
700
|
+
if (!overwrite && containsMembankInject(existingSessionStart)) return {
|
|
701
|
+
status: "already-configured",
|
|
702
|
+
existing: extractInjectCommand(existingSessionStart),
|
|
703
|
+
replacement: this.replacement
|
|
704
|
+
};
|
|
705
|
+
const filteredSessionStart = overwrite ? filterOutMembankFlat(existingSessionStart) : existingSessionStart;
|
|
706
|
+
const existingUserPrompt = Array.isArray(hooks?.userPromptSubmitted) ? hooks.userPromptSubmitted : [];
|
|
707
|
+
const existingToolFailure = Array.isArray(hooks?.postToolUseFailure) ? hooks.postToolUseFailure : [];
|
|
708
|
+
writeJsonAtomic(cfgPath, {
|
|
709
|
+
version: cfg.version ?? 1,
|
|
710
|
+
...cfg,
|
|
711
|
+
hooks: {
|
|
712
|
+
...hooks ?? {},
|
|
713
|
+
sessionStart: [...filteredSessionStart, {
|
|
714
|
+
type: "command",
|
|
715
|
+
bash: "npx @membank/cli inject --harness copilot-cli",
|
|
716
|
+
timeoutSec: 30
|
|
717
|
+
}],
|
|
718
|
+
userPromptSubmitted: [...filterOutMembankFlat(existingUserPrompt), {
|
|
592
719
|
type: "command",
|
|
593
|
-
|
|
594
|
-
|
|
720
|
+
bash: "npx @membank/cli inject --event user-prompt --harness copilot-cli",
|
|
721
|
+
timeoutSec: 30
|
|
722
|
+
}],
|
|
723
|
+
postToolUseFailure: [...filterOutMembankFlat(existingToolFailure), {
|
|
724
|
+
type: "command",
|
|
725
|
+
bash: "npx @membank/cli inject --event tool-failure --harness copilot-cli",
|
|
726
|
+
timeoutSec: 30
|
|
727
|
+
}]
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
return { status: "written" };
|
|
731
|
+
}
|
|
732
|
+
},
|
|
733
|
+
codex: {
|
|
734
|
+
replacement: "npx @membank/cli inject --harness codex",
|
|
735
|
+
write(resolver, overwrite = false) {
|
|
736
|
+
const cfgPath = join(resolver.home(), ".codex", "hooks.json");
|
|
737
|
+
const cfg = readJson(cfgPath);
|
|
738
|
+
const hooks = cfg.hooks;
|
|
739
|
+
const existingSessionStart = Array.isArray(hooks?.SessionStart) ? hooks.SessionStart : [];
|
|
740
|
+
const innerHooks = existingSessionStart.flatMap(getHooksArray);
|
|
741
|
+
if (!overwrite && containsMembankInject(innerHooks)) return {
|
|
742
|
+
status: "already-configured",
|
|
743
|
+
existing: extractInjectCommand(innerHooks),
|
|
744
|
+
replacement: this.replacement
|
|
745
|
+
};
|
|
746
|
+
const filteredSessionStart = overwrite ? filterOutMembank(existingSessionStart) : existingSessionStart;
|
|
747
|
+
const existingUserPrompt = Array.isArray(hooks?.UserPromptSubmit) ? hooks.UserPromptSubmit : [];
|
|
748
|
+
const existingToolFailure = Array.isArray(hooks?.PostToolUse) ? hooks.PostToolUse : [];
|
|
749
|
+
writeJsonAtomic(cfgPath, {
|
|
750
|
+
...cfg,
|
|
751
|
+
hooks: {
|
|
752
|
+
...hooks ?? {},
|
|
753
|
+
SessionStart: [...filteredSessionStart, {
|
|
754
|
+
matcher: "",
|
|
755
|
+
hooks: [{
|
|
756
|
+
type: "command",
|
|
757
|
+
command: "npx @membank/cli inject --harness codex",
|
|
758
|
+
timeout: 30
|
|
759
|
+
}]
|
|
760
|
+
}],
|
|
761
|
+
UserPromptSubmit: [...filterOutMembank(existingUserPrompt), {
|
|
762
|
+
matcher: "",
|
|
763
|
+
hooks: [{
|
|
764
|
+
type: "command",
|
|
765
|
+
command: "npx @membank/cli inject --event user-prompt --harness codex",
|
|
766
|
+
timeout: 30
|
|
767
|
+
}]
|
|
768
|
+
}],
|
|
769
|
+
PostToolUse: [...filterOutMembank(existingToolFailure), {
|
|
770
|
+
matcher: "",
|
|
771
|
+
hooks: [{
|
|
772
|
+
type: "command",
|
|
773
|
+
command: "npx @membank/cli inject --event tool-failure --harness codex",
|
|
774
|
+
timeout: 30
|
|
775
|
+
}]
|
|
595
776
|
}]
|
|
596
|
-
}
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
return { status: "written" };
|
|
780
|
+
}
|
|
781
|
+
},
|
|
782
|
+
opencode: {
|
|
783
|
+
replacement: "npx @membank/cli inject",
|
|
784
|
+
write(resolver, overwrite = false) {
|
|
785
|
+
const pluginPath = join(resolver.home(), ".config", "opencode", "plugins", "membank.js");
|
|
786
|
+
if (!overwrite && existsSync(pluginPath)) {
|
|
787
|
+
const existing = readFileSync(pluginPath, "utf8");
|
|
788
|
+
if (existing.includes("@membank/cli inject")) return {
|
|
789
|
+
status: "already-configured",
|
|
790
|
+
existing: existing.trim(),
|
|
791
|
+
replacement: newOpencodePlugin()
|
|
792
|
+
};
|
|
597
793
|
}
|
|
598
|
-
|
|
599
|
-
|
|
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" };
|
|
794
|
+
mkdirSync(dirname(pluginPath), { recursive: true });
|
|
795
|
+
writeFileSync(pluginPath, `${newOpencodePlugin()}\n`, "utf8");
|
|
796
|
+
return { status: "written" };
|
|
605
797
|
}
|
|
606
|
-
|
|
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
|
-
} }
|
|
798
|
+
}
|
|
619
799
|
};
|
|
800
|
+
function newOpencodePlugin() {
|
|
801
|
+
return [
|
|
802
|
+
"export default {",
|
|
803
|
+
" hooks: {",
|
|
804
|
+
" \"session.start\": async ({ $ }) => {",
|
|
805
|
+
" return await $`npx @membank/cli inject`.text();",
|
|
806
|
+
" },",
|
|
807
|
+
" \"chat.message\": async ({ $, message }) => {",
|
|
808
|
+
" const input = JSON.stringify({ prompt: message?.content ?? \"\" });",
|
|
809
|
+
" return await $`npx @membank/cli inject --event user-prompt`.stdin(input).text();",
|
|
810
|
+
" },",
|
|
811
|
+
" \"tool.execute.after\": async ({ $, result }) => {",
|
|
812
|
+
" if (!result?.exitCode && !result?.error) return;",
|
|
813
|
+
" const payload = JSON.stringify({",
|
|
814
|
+
" tool_name: result.tool ?? \"unknown\",",
|
|
815
|
+
" error_message: result.error ?? (\"exit code \" + result.exitCode),",
|
|
816
|
+
" });",
|
|
817
|
+
" return await $`npx @membank/cli inject --event tool-failure`.stdin(payload).text();",
|
|
818
|
+
" },",
|
|
819
|
+
" },",
|
|
820
|
+
"};"
|
|
821
|
+
].join("\n");
|
|
822
|
+
}
|
|
620
823
|
var InjectionHookWriter = class {
|
|
621
824
|
#resolver;
|
|
622
825
|
constructor(resolver = defaultPathResolver) {
|
|
623
826
|
this.#resolver = resolver;
|
|
624
827
|
}
|
|
625
|
-
write(harness) {
|
|
828
|
+
write(harness, overwrite) {
|
|
626
829
|
const writer = writers[harness];
|
|
627
830
|
if (!writer) return { status: "not-supported" };
|
|
628
|
-
return writer.write(this.#resolver);
|
|
831
|
+
return writer.write(this.#resolver, overwrite);
|
|
629
832
|
}
|
|
630
833
|
};
|
|
631
834
|
//#endregion
|
|
@@ -859,17 +1062,8 @@ var SetupOrchestrator = class {
|
|
|
859
1062
|
out("");
|
|
860
1063
|
const injectionHooksConfigured = [];
|
|
861
1064
|
if (this.#hookWriter) {
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
if (hookResult.status === "not-supported") continue;
|
|
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
|
-
}
|
|
1065
|
+
const w = this.#hookWriter;
|
|
1066
|
+
injectionHooksConfigured.push(...await this.#runHookLoop(detected, "injection hook", (h, ow) => w.write(h, ow), yes, out));
|
|
873
1067
|
out("");
|
|
874
1068
|
}
|
|
875
1069
|
let modelDownloaded = false;
|
|
@@ -892,6 +1086,31 @@ var SetupOrchestrator = class {
|
|
|
892
1086
|
}
|
|
893
1087
|
return results;
|
|
894
1088
|
}
|
|
1089
|
+
async #runHookLoop(detected, label, write, yes, out) {
|
|
1090
|
+
const configured = [];
|
|
1091
|
+
for (const h of detected) try {
|
|
1092
|
+
const result = write(h.name);
|
|
1093
|
+
if (result.status === "not-supported") continue;
|
|
1094
|
+
if (result.status === "written") {
|
|
1095
|
+
out(` ✓ ${h.name}: ${label} written`);
|
|
1096
|
+
configured.push(h.name);
|
|
1097
|
+
} else {
|
|
1098
|
+
out(` ⚠ ${h.name}: ${label} already configured`);
|
|
1099
|
+
out(` Current: ${result.existing}`);
|
|
1100
|
+
out(` New: ${result.replacement}`);
|
|
1101
|
+
if (yes || await this.#prompter(` Replace ${label} for ${h.name}?`)) {
|
|
1102
|
+
if (write(h.name, true).status === "written") {
|
|
1103
|
+
out(` ✓ ${h.name}: ${label} replaced`);
|
|
1104
|
+
configured.push(h.name);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
} catch (err) {
|
|
1109
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1110
|
+
out(` ✗ ${h.name} ${label}: ${msg}`);
|
|
1111
|
+
}
|
|
1112
|
+
return configured;
|
|
1113
|
+
}
|
|
895
1114
|
async #runModelDownload(downloader, out) {
|
|
896
1115
|
out("Downloading embedding model (bge-small-en-v1.5, ~33 MB)...");
|
|
897
1116
|
downloader.on?.("progress", (p) => {
|
|
@@ -1022,7 +1241,7 @@ program.command("import <file>").description("import memories from a JSON export
|
|
|
1022
1241
|
db.close();
|
|
1023
1242
|
}
|
|
1024
1243
|
});
|
|
1025
|
-
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) => {
|
|
1244
|
+
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)").option("--event <event>", "hook event type (session-start|user-prompt|tool-failure)", "session-start").action(async (cmdOptions) => {
|
|
1026
1245
|
try {
|
|
1027
1246
|
await injectCommand(cmdOptions);
|
|
1028
1247
|
} catch (err) {
|
|
@@ -1061,6 +1280,14 @@ program.command("setup").description("detect installed harnesses and write MCP c
|
|
|
1061
1280
|
process.exit(2);
|
|
1062
1281
|
}
|
|
1063
1282
|
});
|
|
1283
|
+
program.command("dashboard").description("open the memory management dashboard in the browser").option("--port <port>", "port to listen on (default: 3847, fallback to random)").action(async (cmdOptions) => {
|
|
1284
|
+
try {
|
|
1285
|
+
await dashboardCommand(cmdOptions);
|
|
1286
|
+
} catch (err) {
|
|
1287
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
1288
|
+
process.exit(2);
|
|
1289
|
+
}
|
|
1290
|
+
});
|
|
1064
1291
|
program.on("command:*", () => {
|
|
1065
1292
|
program.outputHelp();
|
|
1066
1293
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@membank/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
"@huggingface/transformers": "^4.2.0",
|
|
18
18
|
"commander": "^14.0.3",
|
|
19
19
|
"ora": "^9.4.0",
|
|
20
|
-
"@membank/
|
|
21
|
-
"@membank/mcp": "0.
|
|
20
|
+
"@membank/dashboard": "0.1.0",
|
|
21
|
+
"@membank/mcp": "0.3.0",
|
|
22
|
+
"@membank/core": "0.3.0"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
25
|
"@types/node": "^25.6.0",
|