@membank/cli 0.7.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +229 -106
- package/package.json +5 -4
package/dist/index.mjs
CHANGED
|
@@ -1,20 +1,60 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { cancel, confirm, intro, isCancel, multiselect, note, outro } from "@clack/prompts";
|
|
3
|
-
import { DatabaseManager, EmbeddingService,
|
|
3
|
+
import { DatabaseManager, EmbeddingService, MIGRATIONS, MemoryRepository, MemoryTypeSchema, MemoryTypeSchema as MemoryTypeSchema$1, ProjectRepository, QueryEngine, SessionContextBuilder, TagsJsonSchema as TagsRowSchema, resolveProject, runScopeToProjectsMigration } from "@membank/core";
|
|
4
4
|
import { startServer } from "@membank/mcp";
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
import ora from "ora";
|
|
8
|
+
import { z } from "zod";
|
|
8
9
|
import { startDashboard } from "@membank/dashboard";
|
|
9
10
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
|
|
10
11
|
import { dirname, join } from "node:path";
|
|
11
12
|
import Table from "cli-table3";
|
|
12
|
-
import { homedir, tmpdir } from "node:os";
|
|
13
13
|
import { execFile } from "node:child_process";
|
|
14
14
|
import { promisify } from "node:util";
|
|
15
|
+
import { homedir, tmpdir } from "node:os";
|
|
15
16
|
import { EventEmitter } from "node:events";
|
|
16
17
|
import { pipeline } from "@huggingface/transformers";
|
|
17
18
|
import { createInterface } from "node:readline";
|
|
19
|
+
//#region src/schemas.ts
|
|
20
|
+
const SETUP_HARNESS_VALUES = [
|
|
21
|
+
"claude-code",
|
|
22
|
+
"copilot",
|
|
23
|
+
"codex",
|
|
24
|
+
"opencode"
|
|
25
|
+
];
|
|
26
|
+
const SetupHarnessSchema = z.enum(SETUP_HARNESS_VALUES);
|
|
27
|
+
const InjectionHarnessSchema = z.enum([
|
|
28
|
+
"claude-code",
|
|
29
|
+
"copilot-cli",
|
|
30
|
+
"codex",
|
|
31
|
+
"opencode"
|
|
32
|
+
]);
|
|
33
|
+
const MigrateModeSchema = z.enum(["list", "run"]);
|
|
34
|
+
const LimitSchema = z.coerce.number().int().positive();
|
|
35
|
+
const PortSchema = z.coerce.number().int().min(1).max(65535);
|
|
36
|
+
const OptionalNumberSchema = z.number().optional().catch(void 0);
|
|
37
|
+
const MutableJsonObjectSchema = z.record(z.string(), z.unknown());
|
|
38
|
+
const MaybeJsonObjectSchema = z.record(z.string(), z.unknown()).optional().catch(void 0);
|
|
39
|
+
const ExportRecordSchema = z.object({
|
|
40
|
+
id: z.string().min(1),
|
|
41
|
+
content: z.string(),
|
|
42
|
+
type: MemoryTypeSchema,
|
|
43
|
+
tags: z.array(z.string()).optional().default([]),
|
|
44
|
+
sourceHarness: z.string().nullable().optional().default(null),
|
|
45
|
+
accessCount: z.number().optional().default(0),
|
|
46
|
+
pinned: z.boolean().optional().default(false),
|
|
47
|
+
needsReview: z.boolean().optional().default(false),
|
|
48
|
+
createdAt: z.string().optional(),
|
|
49
|
+
updatedAt: z.string().optional(),
|
|
50
|
+
embedding: z.string().nullable().optional().default(null)
|
|
51
|
+
});
|
|
52
|
+
const ExportFileSchema = z.object({
|
|
53
|
+
version: z.literal(1),
|
|
54
|
+
exportedAt: z.string().optional(),
|
|
55
|
+
memories: z.array(ExportRecordSchema)
|
|
56
|
+
});
|
|
57
|
+
//#endregion
|
|
18
58
|
//#region src/commands/add.ts
|
|
19
59
|
async function addCommand(content, options, formatter, db, embeddingService) {
|
|
20
60
|
const ownDb = db === void 0;
|
|
@@ -26,7 +66,7 @@ async function addCommand(content, options, formatter, db, embeddingService) {
|
|
|
26
66
|
const spinner = formatter.isJson ? null : ora("Saving memory…").start();
|
|
27
67
|
const memory = await repo.save({
|
|
28
68
|
content,
|
|
29
|
-
type: options.type,
|
|
69
|
+
type: MemoryTypeSchema$1.parse(options.type),
|
|
30
70
|
tags,
|
|
31
71
|
projectScope
|
|
32
72
|
});
|
|
@@ -39,7 +79,7 @@ async function addCommand(content, options, formatter, db, embeddingService) {
|
|
|
39
79
|
//#endregion
|
|
40
80
|
//#region src/commands/dashboard.ts
|
|
41
81
|
async function dashboardCommand(opts) {
|
|
42
|
-
await startDashboard({ port: opts.port !== void 0 ?
|
|
82
|
+
await startDashboard({ port: opts.port !== void 0 ? PortSchema.parse(opts.port) : void 0 });
|
|
43
83
|
}
|
|
44
84
|
//#endregion
|
|
45
85
|
//#region src/commands/delete.ts
|
|
@@ -59,11 +99,10 @@ function exportCommand(db, formatter, opts) {
|
|
|
59
99
|
id: row.id,
|
|
60
100
|
content: row.content,
|
|
61
101
|
type: row.type,
|
|
62
|
-
tags: JSON.parse(row.tags),
|
|
102
|
+
tags: TagsRowSchema.parse(JSON.parse(row.tags)),
|
|
63
103
|
sourceHarness: row.source,
|
|
64
104
|
accessCount: row.access_count,
|
|
65
105
|
pinned: row.pinned !== 0,
|
|
66
|
-
needsReview: row.needs_review !== 0,
|
|
67
106
|
createdAt: row.created_at,
|
|
68
107
|
updatedAt: row.updated_at,
|
|
69
108
|
embedding: row.embedding !== null ? Buffer.from(row.embedding).toString("base64") : null
|
|
@@ -83,17 +122,6 @@ function exportCommand(db, formatter, opts) {
|
|
|
83
122
|
}
|
|
84
123
|
//#endregion
|
|
85
124
|
//#region src/commands/import.ts
|
|
86
|
-
const MEMORY_TYPES = new Set(MEMORY_TYPE_VALUES);
|
|
87
|
-
function isValidRecord(r) {
|
|
88
|
-
if (typeof r !== "object" || r === null) return false;
|
|
89
|
-
const rec = r;
|
|
90
|
-
return typeof rec.id === "string" && rec.id.length > 0 && typeof rec.content === "string" && typeof rec.type === "string" && MEMORY_TYPES.has(rec.type);
|
|
91
|
-
}
|
|
92
|
-
function isExportFile(parsed) {
|
|
93
|
-
if (typeof parsed !== "object" || parsed === null) return false;
|
|
94
|
-
const obj = parsed;
|
|
95
|
-
return obj.version === 1 && Array.isArray(obj.memories);
|
|
96
|
-
}
|
|
97
125
|
async function importCommand(filePath, db, formatter, prompt) {
|
|
98
126
|
let raw;
|
|
99
127
|
try {
|
|
@@ -109,25 +137,21 @@ async function importCommand(filePath, db, formatter, prompt) {
|
|
|
109
137
|
formatter.error(`Invalid JSON in file: ${filePath}`);
|
|
110
138
|
process.exit(1);
|
|
111
139
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
const invalidIndex = parsed.memories.findIndex((r) => !isValidRecord(r));
|
|
117
|
-
if (invalidIndex !== -1) {
|
|
118
|
-
formatter.error(`Invalid memory record at index ${invalidIndex}: must have id, content, and type`);
|
|
140
|
+
const parseResult = ExportFileSchema.safeParse(parsed);
|
|
141
|
+
if (!parseResult.success) {
|
|
142
|
+
formatter.error(`Invalid export file: ${parseResult.error.issues[0]?.message ?? "unknown error"}`);
|
|
119
143
|
process.exit(1);
|
|
120
144
|
}
|
|
121
|
-
const count =
|
|
145
|
+
const count = parseResult.data.memories.length;
|
|
122
146
|
if (formatter.isJson) process.stdout.write(`${JSON.stringify({ found: count })}\n`);
|
|
123
147
|
else process.stdout.write(`Found ${count} memories to import.\n`);
|
|
124
148
|
if (!await prompt.confirm("Import?")) return;
|
|
125
|
-
const insertMemory = db.db.prepare(`INSERT OR REPLACE INTO memories (id, content, type, tags, source, access_count, pinned,
|
|
126
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?,
|
|
149
|
+
const insertMemory = db.db.prepare(`INSERT OR REPLACE INTO memories (id, content, type, tags, source, access_count, pinned, created_at, updated_at)
|
|
150
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
127
151
|
const insertEmbedding = db.db.prepare(`INSERT OR REPLACE INTO embeddings (rowid, embedding) SELECT m.rowid, ? FROM memories m WHERE m.id = ?`);
|
|
128
152
|
db.db.transaction(() => {
|
|
129
|
-
for (const rec of
|
|
130
|
-
insertMemory.run(rec.id, rec.content, rec.type, JSON.stringify(rec.tags ?? []), rec.sourceHarness ?? null, rec.accessCount ?? 0, rec.pinned ? 1 : 0, rec.
|
|
153
|
+
for (const rec of parseResult.data.memories) {
|
|
154
|
+
insertMemory.run(rec.id, rec.content, rec.type, JSON.stringify(rec.tags ?? []), rec.sourceHarness ?? null, rec.accessCount ?? 0, rec.pinned ? 1 : 0, rec.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(), rec.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString());
|
|
131
155
|
if (rec.embedding !== null && rec.embedding !== void 0) {
|
|
132
156
|
const buf = Buffer.from(rec.embedding, "base64");
|
|
133
157
|
insertEmbedding.run(buf, rec.id);
|
|
@@ -139,17 +163,21 @@ async function importCommand(filePath, db, formatter, prompt) {
|
|
|
139
163
|
}
|
|
140
164
|
//#endregion
|
|
141
165
|
//#region src/commands/inject.ts
|
|
142
|
-
const MEMORY_GUIDANCE = "
|
|
166
|
+
const MEMORY_GUIDANCE = ["Save (call save_memory) when: (1) user states a preference or makes a decision; (2) user corrects you; (3) you discover a working fix after a tool error; (4) you learn a non-obvious project fact. Type ∈ correction|preference|decision|learning|fact. When unsure, save.", "Query (call query_memory) before: answering anything that touches prior decisions, and before exploration tasks (file reads, searches, web lookups) where past corrections or preferences may apply. Skip when clearly irrelevant (e.g. trivial arithmetic). Soft guideline, not a hard rule."].join("\n");
|
|
167
|
+
function xmlEscape(s) {
|
|
168
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
169
|
+
}
|
|
143
170
|
function formatContext(ctx) {
|
|
144
|
-
const
|
|
171
|
+
const parts = [];
|
|
145
172
|
const statParts = Object.entries(ctx.stats).filter(([, count]) => count > 0).map(([type, count]) => `${count} ${type}${count !== 1 ? "s" : ""}`);
|
|
146
|
-
if (statParts.length > 0)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
173
|
+
if (statParts.length > 0) parts.push(`<memory-stats>\n${statParts.join(", ")}\n</memory-stats>`);
|
|
174
|
+
const allPinned = [...ctx.pinnedGlobal, ...ctx.pinnedProject];
|
|
175
|
+
if (allPinned.length > 0) {
|
|
176
|
+
const memLines = allPinned.map((m) => ` <memory type="${m.type}">${xmlEscape(m.content)}</memory>`);
|
|
177
|
+
parts.push(`<pinned-memories>\n${memLines.join("\n")}\n</pinned-memories>`);
|
|
178
|
+
}
|
|
179
|
+
parts.push(`<memory-guidance>\n${MEMORY_GUIDANCE}\n</memory-guidance>`);
|
|
180
|
+
return parts.join("\n");
|
|
153
181
|
}
|
|
154
182
|
function outputAdditionalContext(text, harness, eventName) {
|
|
155
183
|
if (harness === "claude-code") {
|
|
@@ -165,22 +193,36 @@ function outputAdditionalContext(text, harness, eventName) {
|
|
|
165
193
|
}
|
|
166
194
|
process.stdout.write(`${text}\n`);
|
|
167
195
|
}
|
|
168
|
-
async function
|
|
169
|
-
const
|
|
196
|
+
async function buildText() {
|
|
197
|
+
const resolved = await resolveProject();
|
|
170
198
|
const db = DatabaseManager.open();
|
|
171
|
-
let text;
|
|
172
199
|
try {
|
|
173
|
-
|
|
200
|
+
return formatContext(new SessionContextBuilder(db).getSessionContext(resolved.hash));
|
|
174
201
|
} finally {
|
|
175
202
|
db.close();
|
|
176
203
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
204
|
+
}
|
|
205
|
+
async function handleEvent(harness, eventName) {
|
|
206
|
+
const text = await buildText().catch((err) => {
|
|
207
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
208
|
+
process.stderr.write(`membank inject: ${msg}\n`);
|
|
209
|
+
return null;
|
|
210
|
+
});
|
|
211
|
+
if (text === null) process.exit(0);
|
|
212
|
+
outputAdditionalContext(text, harness, eventName);
|
|
180
213
|
}
|
|
181
214
|
async function injectCommand(opts) {
|
|
182
|
-
|
|
183
|
-
|
|
215
|
+
const harnessResult = InjectionHarnessSchema.safeParse(opts.harness);
|
|
216
|
+
const harness = harnessResult.success ? harnessResult.data : void 0;
|
|
217
|
+
if (opts.event === "session-start" || opts.event === void 0) {
|
|
218
|
+
await handleEvent(harness, "SessionStart");
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (opts.event === "user-prompt-submit") {
|
|
222
|
+
await handleEvent(harness, "UserPromptSubmit");
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
process.exit(0);
|
|
184
226
|
}
|
|
185
227
|
//#endregion
|
|
186
228
|
//#region src/commands/list.ts
|
|
@@ -188,7 +230,7 @@ async function listCommand(options, formatter) {
|
|
|
188
230
|
const db = DatabaseManager.open();
|
|
189
231
|
try {
|
|
190
232
|
const memories = new MemoryRepository(db, new EmbeddingService(), new ProjectRepository(db)).list({
|
|
191
|
-
type: options.type,
|
|
233
|
+
type: options.type !== void 0 ? MemoryTypeSchema$1.parse(options.type) : void 0,
|
|
192
234
|
pinned: options.pinned
|
|
193
235
|
});
|
|
194
236
|
formatter.outputMemories(memories);
|
|
@@ -255,12 +297,13 @@ async function queryCommand(queryText, options, formatter) {
|
|
|
255
297
|
try {
|
|
256
298
|
const embedding = new EmbeddingService();
|
|
257
299
|
const engine = new QueryEngine(db, embedding, new MemoryRepository(db, embedding, new ProjectRepository(db)));
|
|
258
|
-
const limit = options.limit !== void 0 ?
|
|
300
|
+
const limit = options.limit !== void 0 ? LimitSchema.parse(options.limit) : 10;
|
|
259
301
|
const spinner = formatter.isJson ? null : ora("Searching memories…").start();
|
|
260
302
|
const results = await engine.query({
|
|
261
303
|
query: queryText,
|
|
262
|
-
type: options.type,
|
|
263
|
-
limit
|
|
304
|
+
type: options.type !== void 0 ? MemoryTypeSchema$1.parse(options.type) : void 0,
|
|
305
|
+
limit,
|
|
306
|
+
includePinned: options.includePinned
|
|
264
307
|
});
|
|
265
308
|
spinner?.succeed(`${results.length} result${results.length === 1 ? "" : "s"} found`);
|
|
266
309
|
formatter.outputQueryResults(results);
|
|
@@ -269,6 +312,24 @@ async function queryCommand(queryText, options, formatter) {
|
|
|
269
312
|
}
|
|
270
313
|
}
|
|
271
314
|
//#endregion
|
|
315
|
+
//#region src/commands/review.ts
|
|
316
|
+
async function reviewCommand(opts, formatter) {
|
|
317
|
+
const db = DatabaseManager.open();
|
|
318
|
+
try {
|
|
319
|
+
const repo = new MemoryRepository(db, new EmbeddingService(), new ProjectRepository(db));
|
|
320
|
+
if (opts.resolve !== void 0) {
|
|
321
|
+
repo.resolveReviewEvents(opts.resolve);
|
|
322
|
+
if (!formatter.isJson) process.stdout.write(`Resolved review events for memory ${opts.resolve}\n`);
|
|
323
|
+
else process.stdout.write(`${JSON.stringify({ resolved: opts.resolve })}\n`);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const flagged = repo.listFlagged();
|
|
327
|
+
formatter.outputReview(flagged);
|
|
328
|
+
} finally {
|
|
329
|
+
db.close();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
//#endregion
|
|
272
333
|
//#region src/commands/stats.ts
|
|
273
334
|
async function statsCommand(formatter) {
|
|
274
335
|
const db = DatabaseManager.open();
|
|
@@ -419,6 +480,30 @@ var Formatter = class Formatter {
|
|
|
419
480
|
}
|
|
420
481
|
process.stdout.write(`\n${table.toString()}\n\n`);
|
|
421
482
|
}
|
|
483
|
+
outputReview(memories) {
|
|
484
|
+
if (this.#isJson) {
|
|
485
|
+
process.stdout.write(`${JSON.stringify(memories)}\n`);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (memories.length === 0) {
|
|
489
|
+
process.stdout.write(`${chalk.dim("No memories flagged for review.")}\n`);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
for (const m of memories) {
|
|
493
|
+
process.stdout.write("\n");
|
|
494
|
+
process.stdout.write(` ${colorType(m.type)} ${chalk.dim(m.id)}\n`);
|
|
495
|
+
process.stdout.write(` ${truncate(m.content, 80)}\n`);
|
|
496
|
+
for (const event of m.reviewEvents) this.#outputReviewEvent(event);
|
|
497
|
+
}
|
|
498
|
+
process.stdout.write("\n");
|
|
499
|
+
}
|
|
500
|
+
#outputReviewEvent(event) {
|
|
501
|
+
const pct = `${Math.round(event.similarity * 100)}%`;
|
|
502
|
+
const conflictRef = event.conflictingMemoryId ? chalk.dim(event.conflictingMemoryId) : chalk.dim("(deleted)");
|
|
503
|
+
const ts = new Date(event.createdAt).toLocaleString();
|
|
504
|
+
process.stdout.write(` ${chalk.yellow("⚠")} ${pct} similarity conflict: ${conflictRef} ${chalk.dim(ts)}\n`);
|
|
505
|
+
if (event.conflictContentSnapshot) process.stdout.write(` ${chalk.dim("snapshot:")} ${truncate(event.conflictContentSnapshot, 60)}\n`);
|
|
506
|
+
}
|
|
422
507
|
error(msg) {
|
|
423
508
|
if (this.#isJson) process.stderr.write(`${JSON.stringify({ error: msg })}\n`);
|
|
424
509
|
else process.stderr.write(`${chalk.red("Error:")} ${msg}\n`);
|
|
@@ -473,6 +558,21 @@ async function execFileNoThrow(cmd, args) {
|
|
|
473
558
|
}
|
|
474
559
|
}
|
|
475
560
|
//#endregion
|
|
561
|
+
//#region src/utils/json.ts
|
|
562
|
+
function readJson(path) {
|
|
563
|
+
try {
|
|
564
|
+
return MutableJsonObjectSchema.parse(JSON.parse(readFileSync(path, "utf8")));
|
|
565
|
+
} catch {
|
|
566
|
+
return {};
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function writeJsonAtomic(path, data) {
|
|
570
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
571
|
+
const tmp = join(mkdtempSync(join(tmpdir(), "membank-")), "cfg.json");
|
|
572
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2));
|
|
573
|
+
renameSync(tmp, path);
|
|
574
|
+
}
|
|
575
|
+
//#endregion
|
|
476
576
|
//#region src/setup/harness-config-writer.ts
|
|
477
577
|
var CommandError = class extends Error {
|
|
478
578
|
command;
|
|
@@ -490,21 +590,8 @@ const defaultPathResolver$1 = {
|
|
|
490
590
|
},
|
|
491
591
|
cwd: () => process.cwd()
|
|
492
592
|
};
|
|
493
|
-
function readJson$1(path) {
|
|
494
|
-
try {
|
|
495
|
-
return JSON.parse(readFileSync(path, "utf8"));
|
|
496
|
-
} catch {
|
|
497
|
-
return {};
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
function writeJsonAtomic$1(path, data) {
|
|
501
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
502
|
-
const tmp = join(mkdtempSync(join(tmpdir(), "membank-")), "cfg.json");
|
|
503
|
-
writeFileSync(tmp, JSON.stringify(data, null, 2));
|
|
504
|
-
renameSync(tmp, path);
|
|
505
|
-
}
|
|
506
593
|
function hasKey(container, key) {
|
|
507
|
-
return container !== null && typeof container === "object" && key
|
|
594
|
+
return container !== null && typeof container === "object" && Object.hasOwn(container, key);
|
|
508
595
|
}
|
|
509
596
|
function assertCliFound(result, cli, command) {
|
|
510
597
|
if (result.exitCode === 127) throw new CommandError(`${cli} CLI not found — install ${cli} first`, command);
|
|
@@ -524,7 +611,7 @@ const writers$1 = {
|
|
|
524
611
|
};
|
|
525
612
|
},
|
|
526
613
|
async write(resolver, run, { overwrite = false } = {}) {
|
|
527
|
-
const configured = hasKey(readJson
|
|
614
|
+
const configured = hasKey(readJson(join(resolver.home(), ".claude.json")).mcpServers, "membank");
|
|
528
615
|
if (configured && !overwrite) return { status: "already-configured" };
|
|
529
616
|
if (configured) {
|
|
530
617
|
const removeArgs = [
|
|
@@ -564,12 +651,12 @@ const writers$1 = {
|
|
|
564
651
|
},
|
|
565
652
|
async write(resolver, _run, { overwrite = false } = {}) {
|
|
566
653
|
const cfgPath = join(resolver.home(), ".copilot", "mcp-config.json");
|
|
567
|
-
const cfg = readJson
|
|
654
|
+
const cfg = readJson(cfgPath);
|
|
568
655
|
if (hasKey(cfg.mcpServers, "membank") && !overwrite) return { status: "already-configured" };
|
|
569
|
-
writeJsonAtomic
|
|
656
|
+
writeJsonAtomic(cfgPath, {
|
|
570
657
|
...cfg,
|
|
571
658
|
mcpServers: {
|
|
572
|
-
...cfg.mcpServers,
|
|
659
|
+
...MaybeJsonObjectSchema.parse(cfg.mcpServers),
|
|
573
660
|
membank: {
|
|
574
661
|
command: "npx",
|
|
575
662
|
args: [
|
|
@@ -630,12 +717,12 @@ const writers$1 = {
|
|
|
630
717
|
},
|
|
631
718
|
async write(resolver, _run, { overwrite = false } = {}) {
|
|
632
719
|
const cfgPath = join(resolver.home(), ".config", "opencode", "opencode.json");
|
|
633
|
-
const cfg = readJson
|
|
720
|
+
const cfg = readJson(cfgPath);
|
|
634
721
|
if (hasKey(cfg.mcp, "membank") && !overwrite) return { status: "already-configured" };
|
|
635
|
-
writeJsonAtomic
|
|
722
|
+
writeJsonAtomic(cfgPath, {
|
|
636
723
|
...cfg,
|
|
637
724
|
mcp: {
|
|
638
|
-
...cfg.mcp,
|
|
725
|
+
...MaybeJsonObjectSchema.parse(cfg.mcp),
|
|
639
726
|
membank: {
|
|
640
727
|
type: "local",
|
|
641
728
|
command: [
|
|
@@ -651,7 +738,7 @@ const writers$1 = {
|
|
|
651
738
|
}
|
|
652
739
|
}
|
|
653
740
|
};
|
|
654
|
-
const SUPPORTED_HARNESSES =
|
|
741
|
+
const SUPPORTED_HARNESSES = SETUP_HARNESS_VALUES;
|
|
655
742
|
var HarnessConfigWriter = class {
|
|
656
743
|
#resolver;
|
|
657
744
|
#run;
|
|
@@ -677,23 +764,10 @@ const defaultPathResolver = { home: () => {
|
|
|
677
764
|
if (!h) throw new Error("Cannot determine home directory");
|
|
678
765
|
return h;
|
|
679
766
|
} };
|
|
680
|
-
function readJson(path) {
|
|
681
|
-
try {
|
|
682
|
-
return JSON.parse(readFileSync(path, "utf8"));
|
|
683
|
-
} catch {
|
|
684
|
-
return {};
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
function writeJsonAtomic(path, data) {
|
|
688
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
689
|
-
const tmp = join(mkdtempSync(join(tmpdir(), "membank-hook-")), "cfg.json");
|
|
690
|
-
writeFileSync(tmp, JSON.stringify(data, null, 2));
|
|
691
|
-
renameSync(tmp, path);
|
|
692
|
-
}
|
|
693
767
|
function getHooksArray(group) {
|
|
694
768
|
if (typeof group !== "object" || group === null) return [];
|
|
695
|
-
|
|
696
|
-
return Array.isArray(
|
|
769
|
+
if (!("hooks" in group)) return [];
|
|
770
|
+
return Array.isArray(group.hooks) ? group.hooks : [];
|
|
697
771
|
}
|
|
698
772
|
function findMembankHookCommand(hooks, pattern) {
|
|
699
773
|
for (const h of hooks) {
|
|
@@ -733,23 +807,29 @@ const writers = {
|
|
|
733
807
|
"claude-code": {
|
|
734
808
|
inspect(resolver) {
|
|
735
809
|
const cfgPath = join(resolver.home(), ".claude", "settings.json");
|
|
736
|
-
const
|
|
810
|
+
const cfg = readJson(cfgPath);
|
|
811
|
+
const hooks = MaybeJsonObjectSchema.parse(cfg.hooks) ?? {};
|
|
812
|
+
const sessionStartInner = (Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []).flatMap(getHooksArray);
|
|
813
|
+
const userPromptSubmitInner = (Array.isArray(hooks.UserPromptSubmit) ? hooks.UserPromptSubmit : []).flatMap(getHooksArray);
|
|
737
814
|
return {
|
|
738
815
|
status: "ready",
|
|
739
816
|
configPath: cfgPath,
|
|
740
817
|
hooks: [{
|
|
741
818
|
event: "SessionStart",
|
|
742
819
|
command: "npx -y @membank/cli inject --harness claude-code",
|
|
743
|
-
existingCommand: extractInjectCommand(
|
|
820
|
+
existingCommand: extractInjectCommand(sessionStartInner) || null
|
|
821
|
+
}, {
|
|
822
|
+
event: "UserPromptSubmit",
|
|
823
|
+
command: "npx -y @membank/cli inject --harness claude-code --event user-prompt-submit",
|
|
824
|
+
existingCommand: extractInjectCommand(userPromptSubmitInner) || null
|
|
744
825
|
}]
|
|
745
826
|
};
|
|
746
827
|
},
|
|
747
828
|
write(resolver, events) {
|
|
748
829
|
const cfgPath = join(resolver.home(), ".claude", "settings.json");
|
|
749
830
|
const cfg = readJson(cfgPath);
|
|
750
|
-
const hooks = cfg.hooks ?? {};
|
|
831
|
+
const hooks = MaybeJsonObjectSchema.parse(cfg.hooks) ?? {};
|
|
751
832
|
const newHooks = { ...hooks };
|
|
752
|
-
pruneNestedEvent(newHooks, "UserPromptSubmit");
|
|
753
833
|
pruneNestedEvent(newHooks, "PostToolUseFailure");
|
|
754
834
|
if (events.includes("SessionStart")) newHooks.SessionStart = [...filterOutMembank(Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []), {
|
|
755
835
|
matcher: "",
|
|
@@ -758,6 +838,13 @@ const writers = {
|
|
|
758
838
|
command: "npx -y @membank/cli inject --harness claude-code"
|
|
759
839
|
}]
|
|
760
840
|
}];
|
|
841
|
+
if (events.includes("UserPromptSubmit")) newHooks.UserPromptSubmit = [...filterOutMembank(Array.isArray(hooks.UserPromptSubmit) ? hooks.UserPromptSubmit : []), {
|
|
842
|
+
matcher: "",
|
|
843
|
+
hooks: [{
|
|
844
|
+
type: "command",
|
|
845
|
+
command: "npx -y @membank/cli inject --harness claude-code --event user-prompt-submit"
|
|
846
|
+
}]
|
|
847
|
+
}];
|
|
761
848
|
writeJsonAtomic(cfgPath, {
|
|
762
849
|
...cfg,
|
|
763
850
|
hooks: newHooks
|
|
@@ -768,31 +855,42 @@ const writers = {
|
|
|
768
855
|
"copilot-cli": {
|
|
769
856
|
inspect(resolver) {
|
|
770
857
|
const cfgPath = join(resolver.home(), ".copilot", "settings.json");
|
|
771
|
-
const
|
|
858
|
+
const cfg = readJson(cfgPath);
|
|
859
|
+
const hooks = MaybeJsonObjectSchema.parse(cfg.hooks) ?? {};
|
|
860
|
+
const sessionStart = Array.isArray(hooks.sessionStart) ? hooks.sessionStart : [];
|
|
861
|
+
const userPromptSubmitted = Array.isArray(hooks.userPromptSubmitted) ? hooks.userPromptSubmitted : [];
|
|
772
862
|
return {
|
|
773
863
|
status: "ready",
|
|
774
864
|
configPath: cfgPath,
|
|
775
865
|
hooks: [{
|
|
776
866
|
event: "sessionStart",
|
|
777
867
|
command: "npx -y @membank/cli inject --harness copilot-cli",
|
|
778
|
-
existingCommand: extractInjectCommand(
|
|
868
|
+
existingCommand: extractInjectCommand(sessionStart) || null
|
|
869
|
+
}, {
|
|
870
|
+
event: "userPromptSubmitted",
|
|
871
|
+
command: "npx -y @membank/cli inject --harness copilot-cli --event user-prompt-submit",
|
|
872
|
+
existingCommand: extractInjectCommand(userPromptSubmitted) || null
|
|
779
873
|
}]
|
|
780
874
|
};
|
|
781
875
|
},
|
|
782
876
|
write(resolver, events) {
|
|
783
877
|
const cfgPath = join(resolver.home(), ".copilot", "settings.json");
|
|
784
878
|
const cfg = readJson(cfgPath);
|
|
785
|
-
const hooks = cfg.hooks ?? {};
|
|
879
|
+
const hooks = MaybeJsonObjectSchema.parse(cfg.hooks) ?? {};
|
|
786
880
|
const newHooks = { ...hooks };
|
|
787
|
-
pruneFlatEvent(newHooks, "userPromptSubmitted");
|
|
788
881
|
pruneFlatEvent(newHooks, "postToolUseFailure");
|
|
789
882
|
if (events.includes("sessionStart")) newHooks.sessionStart = [...filterOutMembankFlat(Array.isArray(hooks.sessionStart) ? hooks.sessionStart : []), {
|
|
790
883
|
type: "command",
|
|
791
884
|
bash: "npx -y @membank/cli inject --harness copilot-cli",
|
|
792
885
|
timeoutSec: 30
|
|
793
886
|
}];
|
|
887
|
+
if (events.includes("userPromptSubmitted")) newHooks.userPromptSubmitted = [...filterOutMembankFlat(Array.isArray(hooks.userPromptSubmitted) ? hooks.userPromptSubmitted : []), {
|
|
888
|
+
type: "command",
|
|
889
|
+
bash: "npx -y @membank/cli inject --harness copilot-cli --event user-prompt-submit",
|
|
890
|
+
timeoutSec: 30
|
|
891
|
+
}];
|
|
794
892
|
writeJsonAtomic(cfgPath, {
|
|
795
|
-
version: cfg.version ?? 1,
|
|
893
|
+
version: OptionalNumberSchema.parse(cfg.version) ?? 1,
|
|
796
894
|
...cfg,
|
|
797
895
|
hooks: newHooks
|
|
798
896
|
});
|
|
@@ -802,23 +900,29 @@ const writers = {
|
|
|
802
900
|
codex: {
|
|
803
901
|
inspect(resolver) {
|
|
804
902
|
const cfgPath = join(resolver.home(), ".codex", "hooks.json");
|
|
805
|
-
const
|
|
903
|
+
const cfg = readJson(cfgPath);
|
|
904
|
+
const hooks = MaybeJsonObjectSchema.parse(cfg.hooks) ?? {};
|
|
905
|
+
const sessionStartInner = (Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []).flatMap(getHooksArray);
|
|
906
|
+
const userPromptSubmitInner = (Array.isArray(hooks.UserPromptSubmit) ? hooks.UserPromptSubmit : []).flatMap(getHooksArray);
|
|
806
907
|
return {
|
|
807
908
|
status: "ready",
|
|
808
909
|
configPath: cfgPath,
|
|
809
910
|
hooks: [{
|
|
810
911
|
event: "SessionStart",
|
|
811
912
|
command: "npx -y @membank/cli inject --harness codex",
|
|
812
|
-
existingCommand: extractInjectCommand(
|
|
913
|
+
existingCommand: extractInjectCommand(sessionStartInner) || null
|
|
914
|
+
}, {
|
|
915
|
+
event: "UserPromptSubmit",
|
|
916
|
+
command: "npx -y @membank/cli inject --harness codex --event user-prompt-submit",
|
|
917
|
+
existingCommand: extractInjectCommand(userPromptSubmitInner) || null
|
|
813
918
|
}]
|
|
814
919
|
};
|
|
815
920
|
},
|
|
816
921
|
write(resolver, events) {
|
|
817
922
|
const cfgPath = join(resolver.home(), ".codex", "hooks.json");
|
|
818
923
|
const cfg = readJson(cfgPath);
|
|
819
|
-
const hooks = cfg.hooks ?? {};
|
|
924
|
+
const hooks = MaybeJsonObjectSchema.parse(cfg.hooks) ?? {};
|
|
820
925
|
const newHooks = { ...hooks };
|
|
821
|
-
pruneNestedEvent(newHooks, "UserPromptSubmit");
|
|
822
926
|
pruneNestedEvent(newHooks, "PostToolUse");
|
|
823
927
|
if (events.includes("SessionStart")) newHooks.SessionStart = [...filterOutMembank(Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []), {
|
|
824
928
|
matcher: "",
|
|
@@ -828,6 +932,14 @@ const writers = {
|
|
|
828
932
|
timeout: 30
|
|
829
933
|
}]
|
|
830
934
|
}];
|
|
935
|
+
if (events.includes("UserPromptSubmit")) newHooks.UserPromptSubmit = [...filterOutMembank(Array.isArray(hooks.UserPromptSubmit) ? hooks.UserPromptSubmit : []), {
|
|
936
|
+
matcher: "",
|
|
937
|
+
hooks: [{
|
|
938
|
+
type: "command",
|
|
939
|
+
command: "npx -y @membank/cli inject --harness codex --event user-prompt-submit",
|
|
940
|
+
timeout: 30
|
|
941
|
+
}]
|
|
942
|
+
}];
|
|
831
943
|
writeJsonAtomic(cfgPath, {
|
|
832
944
|
...cfg,
|
|
833
945
|
hooks: newHooks
|
|
@@ -1034,7 +1146,7 @@ var SetupOrchestrator = class {
|
|
|
1034
1146
|
const out = json ? () => {} : this.#out;
|
|
1035
1147
|
let detected;
|
|
1036
1148
|
if (harness !== void 0) detected = [{
|
|
1037
|
-
name: harness,
|
|
1149
|
+
name: SetupHarnessSchema.parse(harness),
|
|
1038
1150
|
configPath: ""
|
|
1039
1151
|
}];
|
|
1040
1152
|
else detected = this.#detector();
|
|
@@ -1241,7 +1353,7 @@ var SetupOrchestrator = class {
|
|
|
1241
1353
|
if (process.argv.includes("--mcp")) await startServer();
|
|
1242
1354
|
const program = new Command();
|
|
1243
1355
|
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)");
|
|
1244
|
-
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) => {
|
|
1356
|
+
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").option("--include-pinned", "include pinned memories in results (excluded by default)").action(async (queryText, cmdOptions) => {
|
|
1245
1357
|
const globalOpts = program.opts();
|
|
1246
1358
|
const formatter = Formatter.create(globalOpts.json === true);
|
|
1247
1359
|
try {
|
|
@@ -1356,7 +1468,7 @@ program.command("setup").description("detect installed harnesses and write MCP c
|
|
|
1356
1468
|
const formatter = Formatter.create(globalOpts.json === true);
|
|
1357
1469
|
const interactive = !formatter.isJson && !autoYes && cmdOptions.harness === void 0;
|
|
1358
1470
|
if (cmdOptions.harness !== void 0) {
|
|
1359
|
-
if (!
|
|
1471
|
+
if (!SetupHarnessSchema.safeParse(cmdOptions.harness).success) {
|
|
1360
1472
|
formatter.error(`Unknown harness: "${cmdOptions.harness}". Supported: ${SUPPORTED_HARNESSES.join(", ")}`);
|
|
1361
1473
|
process.exit(1);
|
|
1362
1474
|
}
|
|
@@ -1418,15 +1530,26 @@ program.command("setup").description("detect installed harnesses and write MCP c
|
|
|
1418
1530
|
process.exit(2);
|
|
1419
1531
|
}
|
|
1420
1532
|
});
|
|
1533
|
+
program.command("review").description("list memories flagged for review, or resolve review events").option("--resolve <id>", "resolve all open review events for the given memory id").action(async (cmdOptions) => {
|
|
1534
|
+
const globalOpts = program.opts();
|
|
1535
|
+
const formatter = Formatter.create(globalOpts.json === true);
|
|
1536
|
+
try {
|
|
1537
|
+
await reviewCommand(cmdOptions, formatter);
|
|
1538
|
+
} catch (err) {
|
|
1539
|
+
formatter.error(err instanceof Error ? err.message : String(err));
|
|
1540
|
+
process.exit(2);
|
|
1541
|
+
}
|
|
1542
|
+
});
|
|
1421
1543
|
program.command("migrate <mode> [name]").description("list or run a named data migration (modes: list, run)").action(async (mode, name) => {
|
|
1422
|
-
|
|
1544
|
+
const modeResult = MigrateModeSchema.safeParse(mode);
|
|
1545
|
+
if (!modeResult.success) {
|
|
1423
1546
|
process.stderr.write(`Error: mode must be "list" or "run"\n`);
|
|
1424
1547
|
process.exit(1);
|
|
1425
1548
|
}
|
|
1426
1549
|
const globalOpts = program.opts();
|
|
1427
1550
|
const formatter = Formatter.create(globalOpts.json === true);
|
|
1428
1551
|
try {
|
|
1429
|
-
await migrateCommand(
|
|
1552
|
+
await migrateCommand(modeResult.data, name, formatter);
|
|
1430
1553
|
} catch (err) {
|
|
1431
1554
|
formatter.error(err instanceof Error ? err.message : String(err));
|
|
1432
1555
|
process.exit(2);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@membank/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -20,9 +20,10 @@
|
|
|
20
20
|
"cli-table3": "^0.6.5",
|
|
21
21
|
"commander": "^14.0.3",
|
|
22
22
|
"ora": "^9.4.0",
|
|
23
|
-
"
|
|
24
|
-
"@membank/
|
|
25
|
-
"@membank/
|
|
23
|
+
"zod": "^4.4.3",
|
|
24
|
+
"@membank/dashboard": "0.5.0",
|
|
25
|
+
"@membank/core": "0.8.0",
|
|
26
|
+
"@membank/mcp": "0.10.0"
|
|
26
27
|
},
|
|
27
28
|
"devDependencies": {
|
|
28
29
|
"@types/node": "^25.6.0",
|