@membank/cli 0.2.0 → 0.4.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 +13 -5
- package/dist/index.mjs +282 -353
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ Options:
|
|
|
33
33
|
--json Machine-readable output
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
Supported harnesses: `claude-code`, `copilot`, `codex`, `opencode`
|
|
36
|
+
Supported harnesses: `claude-code`, `copilot`, `codex`, `opencode` (see `membank setup` for harness-specific setup instructions)
|
|
37
37
|
|
|
38
38
|
## Commands
|
|
39
39
|
|
|
@@ -125,6 +125,16 @@ Output session context formatted for a harness. Called automatically by session
|
|
|
125
125
|
membank inject --harness claude-code --scope <project-scope>
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
+
### `membank dashboard`
|
|
129
|
+
|
|
130
|
+
Start the web dashboard for browsing and managing memories.
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
membank dashboard
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Opens http://localhost:3847 by default. Features: full-text search, filtering by type/scope/pin status, edit memory metadata, view dedup reviews, and storage statistics.
|
|
137
|
+
|
|
128
138
|
## Global flags
|
|
129
139
|
|
|
130
140
|
```
|
|
@@ -143,11 +153,9 @@ Starts the stdio MCP server. This is what harnesses connect to — `setup` write
|
|
|
143
153
|
|
|
144
154
|
## Session hooks
|
|
145
155
|
|
|
146
|
-
`setup` installs
|
|
147
|
-
|
|
148
|
-
**Session start** — calls `membank inject` to prepend pinned memories into the LLM context at the beginning of every session.
|
|
156
|
+
`setup` installs a hook for Claude Code that injects memories at the start of each session:
|
|
149
157
|
|
|
150
|
-
**
|
|
158
|
+
**SessionStart** — calls `membank inject` to prepend pinned memories into the LLM context at the beginning of every session.
|
|
151
159
|
|
|
152
160
|
## Requirements
|
|
153
161
|
|
package/dist/index.mjs
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
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
|
-
import { homedir, tmpdir } from "node:os";
|
|
8
8
|
import * as readline from "node:readline";
|
|
9
9
|
import { createInterface } from "node:readline";
|
|
10
|
+
import { homedir, tmpdir } from "node:os";
|
|
10
11
|
import { execFile } from "node:child_process";
|
|
11
12
|
import { promisify } from "node:util";
|
|
12
13
|
import { EventEmitter } from "node:events";
|
|
@@ -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) {
|
|
@@ -128,7 +134,7 @@ async function importCommand(filePath, db, formatter, prompt) {
|
|
|
128
134
|
}
|
|
129
135
|
//#endregion
|
|
130
136
|
//#region src/commands/inject.ts
|
|
131
|
-
const MEMORY_GUIDANCE = "[Memory Guidance]:
|
|
137
|
+
const MEMORY_GUIDANCE = "[Memory Guidance]: Persistent memory is available via query_memory, save_memory, update_memory, delete_memory. Skipping save_memory when the user gives a correction or preference means they have to repeat themselves next session — that is the failure mode to avoid. Skipping query_memory on topics that touch prior decisions means contradicting yourself. Default to saving (type: correction|preference|decision|learning|fact) when in doubt; rely on dedup to handle redundancy. Pin anything that should appear at every session start.";
|
|
132
138
|
function formatContext(ctx) {
|
|
133
139
|
const lines = [];
|
|
134
140
|
const statParts = Object.entries(ctx.stats).filter(([, count]) => count > 0).map(([type, count]) => `${count} ${type}${count !== 1 ? "s" : ""}`);
|
|
@@ -140,20 +146,10 @@ function formatContext(ctx) {
|
|
|
140
146
|
lines.push(MEMORY_GUIDANCE);
|
|
141
147
|
return lines.join("\n");
|
|
142
148
|
}
|
|
143
|
-
|
|
144
|
-
const projectScope = opts.scope ?? await resolveScope();
|
|
145
|
-
const db = DatabaseManager.open();
|
|
146
|
-
let text;
|
|
147
|
-
try {
|
|
148
|
-
text = formatContext(new SessionContextBuilder(db).getSessionContext(projectScope));
|
|
149
|
-
} finally {
|
|
150
|
-
db.close();
|
|
151
|
-
}
|
|
152
|
-
if (!text) process.exit(0);
|
|
153
|
-
const harness = opts.harness;
|
|
149
|
+
function outputAdditionalContext(text, harness, eventName) {
|
|
154
150
|
if (harness === "claude-code") {
|
|
155
151
|
process.stdout.write(JSON.stringify({ hookSpecificOutput: {
|
|
156
|
-
hookEventName:
|
|
152
|
+
hookEventName: eventName,
|
|
157
153
|
additionalContext: text
|
|
158
154
|
} }));
|
|
159
155
|
return;
|
|
@@ -164,6 +160,23 @@ async function injectCommand(opts) {
|
|
|
164
160
|
}
|
|
165
161
|
process.stdout.write(`${text}\n`);
|
|
166
162
|
}
|
|
163
|
+
async function handleSessionStart(opts) {
|
|
164
|
+
const projectScope = opts.scope ?? await resolveScope();
|
|
165
|
+
const db = DatabaseManager.open();
|
|
166
|
+
let text;
|
|
167
|
+
try {
|
|
168
|
+
text = formatContext(new SessionContextBuilder(db).getSessionContext(projectScope));
|
|
169
|
+
} finally {
|
|
170
|
+
db.close();
|
|
171
|
+
}
|
|
172
|
+
if (!text) process.exit(0);
|
|
173
|
+
const harness = opts.harness;
|
|
174
|
+
outputAdditionalContext(text, harness, "SessionStart");
|
|
175
|
+
}
|
|
176
|
+
async function injectCommand(opts) {
|
|
177
|
+
if (opts.event !== void 0 && opts.event !== "session-start") process.exit(0);
|
|
178
|
+
await handleSessionStart(opts);
|
|
179
|
+
}
|
|
167
180
|
//#endregion
|
|
168
181
|
//#region src/commands/list.ts
|
|
169
182
|
async function listCommand(options, formatter) {
|
|
@@ -225,303 +238,6 @@ async function statsCommand(formatter) {
|
|
|
225
238
|
}
|
|
226
239
|
}
|
|
227
240
|
//#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
|
|
525
241
|
//#region src/commands/unpin.ts
|
|
526
242
|
function unpinCommand(id, formatter, db) {
|
|
527
243
|
const ownDb = db === void 0;
|
|
@@ -668,7 +384,7 @@ async function execFileNoThrow(cmd, args) {
|
|
|
668
384
|
}
|
|
669
385
|
//#endregion
|
|
670
386
|
//#region src/setup/harness-config-writer.ts
|
|
671
|
-
const defaultPathResolver = {
|
|
387
|
+
const defaultPathResolver$1 = {
|
|
672
388
|
home: () => {
|
|
673
389
|
const h = process.env.HOME ?? process.env.USERPROFILE;
|
|
674
390
|
if (!h) throw new Error("Cannot determine home directory");
|
|
@@ -676,14 +392,14 @@ const defaultPathResolver = {
|
|
|
676
392
|
},
|
|
677
393
|
cwd: () => process.cwd()
|
|
678
394
|
};
|
|
679
|
-
function readJson(path) {
|
|
395
|
+
function readJson$1(path) {
|
|
680
396
|
try {
|
|
681
397
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
682
398
|
} catch {
|
|
683
399
|
return {};
|
|
684
400
|
}
|
|
685
401
|
}
|
|
686
|
-
function writeJsonAtomic(path, data) {
|
|
402
|
+
function writeJsonAtomic$1(path, data) {
|
|
687
403
|
mkdirSync(dirname(path), { recursive: true });
|
|
688
404
|
const tmp = join(mkdtempSync(join(tmpdir(), "membank-")), "cfg.json");
|
|
689
405
|
writeFileSync(tmp, JSON.stringify(data, null, 2));
|
|
@@ -700,9 +416,9 @@ const MEMBANK_NPX_ARGS = [
|
|
|
700
416
|
"@membank/cli@latest",
|
|
701
417
|
"--mcp"
|
|
702
418
|
];
|
|
703
|
-
const writers = {
|
|
419
|
+
const writers$1 = {
|
|
704
420
|
"claude-code": { async write(resolver, run, { overwrite = false } = {}) {
|
|
705
|
-
const configured = hasKey(readJson(join(resolver.home(), ".claude.json")).mcpServers, "membank");
|
|
421
|
+
const configured = hasKey(readJson$1(join(resolver.home(), ".claude.json")).mcpServers, "membank");
|
|
706
422
|
if (configured && !overwrite) return { status: "already-configured" };
|
|
707
423
|
if (configured) {
|
|
708
424
|
const remove = await run("claude", [
|
|
@@ -730,9 +446,9 @@ const writers = {
|
|
|
730
446
|
} },
|
|
731
447
|
copilot: { async write(resolver, _run, { overwrite = false } = {}) {
|
|
732
448
|
const cfgPath = join(resolver.home(), ".copilot", "mcp-config.json");
|
|
733
|
-
const cfg = readJson(cfgPath);
|
|
449
|
+
const cfg = readJson$1(cfgPath);
|
|
734
450
|
if (hasKey(cfg.mcpServers, "membank") && !overwrite) return { status: "already-configured" };
|
|
735
|
-
writeJsonAtomic(cfgPath, {
|
|
451
|
+
writeJsonAtomic$1(cfgPath, {
|
|
736
452
|
...cfg,
|
|
737
453
|
mcpServers: {
|
|
738
454
|
...cfg.mcpServers,
|
|
@@ -771,9 +487,9 @@ const writers = {
|
|
|
771
487
|
} },
|
|
772
488
|
opencode: { async write(resolver, _run, { overwrite = false } = {}) {
|
|
773
489
|
const cfgPath = join(resolver.home(), ".config", "opencode", "opencode.json");
|
|
774
|
-
const cfg = readJson(cfgPath);
|
|
490
|
+
const cfg = readJson$1(cfgPath);
|
|
775
491
|
if (hasKey(cfg.mcp, "membank") && !overwrite) return { status: "already-configured" };
|
|
776
|
-
writeJsonAtomic(cfgPath, {
|
|
492
|
+
writeJsonAtomic$1(cfgPath, {
|
|
777
493
|
...cfg,
|
|
778
494
|
mcp: {
|
|
779
495
|
...cfg.mcp,
|
|
@@ -790,21 +506,232 @@ const writers = {
|
|
|
790
506
|
return { status: "written" };
|
|
791
507
|
} }
|
|
792
508
|
};
|
|
793
|
-
const SUPPORTED_HARNESSES = Object.keys(writers);
|
|
509
|
+
const SUPPORTED_HARNESSES = Object.keys(writers$1);
|
|
794
510
|
var HarnessConfigWriter = class {
|
|
795
511
|
#resolver;
|
|
796
512
|
#run;
|
|
797
|
-
constructor(resolver = defaultPathResolver, run = execFileNoThrow) {
|
|
513
|
+
constructor(resolver = defaultPathResolver$1, run = execFileNoThrow) {
|
|
798
514
|
this.#resolver = resolver;
|
|
799
515
|
this.#run = run;
|
|
800
516
|
}
|
|
801
517
|
async write(harness, { overwrite = false } = {}) {
|
|
802
|
-
const writer = writers[harness];
|
|
518
|
+
const writer = writers$1[harness];
|
|
803
519
|
if (!writer) throw new Error(`Unknown harness: ${harness}`);
|
|
804
520
|
return writer.write(this.#resolver, this.#run, { overwrite });
|
|
805
521
|
}
|
|
806
522
|
};
|
|
807
523
|
//#endregion
|
|
524
|
+
//#region src/setup/injection-hook-writer.ts
|
|
525
|
+
const defaultPathResolver = { home: () => {
|
|
526
|
+
const h = process.env.HOME ?? process.env.USERPROFILE;
|
|
527
|
+
if (!h) throw new Error("Cannot determine home directory");
|
|
528
|
+
return h;
|
|
529
|
+
} };
|
|
530
|
+
function readJson(path) {
|
|
531
|
+
try {
|
|
532
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
533
|
+
} catch {
|
|
534
|
+
return {};
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
function writeJsonAtomic(path, data) {
|
|
538
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
539
|
+
const tmp = join(mkdtempSync(join(tmpdir(), "membank-hook-")), "cfg.json");
|
|
540
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2));
|
|
541
|
+
renameSync(tmp, path);
|
|
542
|
+
}
|
|
543
|
+
function getHooksArray(group) {
|
|
544
|
+
if (typeof group !== "object" || group === null) return [];
|
|
545
|
+
const h = group.hooks;
|
|
546
|
+
return Array.isArray(h) ? h : [];
|
|
547
|
+
}
|
|
548
|
+
function findMembankHookCommand(hooks, pattern) {
|
|
549
|
+
for (const h of hooks) {
|
|
550
|
+
if (typeof h !== "object" || h === null) continue;
|
|
551
|
+
if ("command" in h && typeof h.command === "string" && h.command.includes(pattern)) return h.command;
|
|
552
|
+
if ("bash" in h && typeof h.bash === "string" && h.bash.includes(pattern)) return h.bash;
|
|
553
|
+
}
|
|
554
|
+
return "";
|
|
555
|
+
}
|
|
556
|
+
function containsMembankInject(hooks) {
|
|
557
|
+
return findMembankHookCommand(hooks, "@membank/cli") !== "";
|
|
558
|
+
}
|
|
559
|
+
function extractInjectCommand(hooks) {
|
|
560
|
+
return findMembankHookCommand(hooks, "@membank/cli");
|
|
561
|
+
}
|
|
562
|
+
function filterOutMembank(groups) {
|
|
563
|
+
return groups.filter((g) => !containsMembankInject(getHooksArray(g)));
|
|
564
|
+
}
|
|
565
|
+
function filterOutMembankFlat(hooks) {
|
|
566
|
+
return hooks.filter((h) => !containsMembankInject([h]));
|
|
567
|
+
}
|
|
568
|
+
function pruneNestedEvent(hooks, eventKey) {
|
|
569
|
+
const existing = hooks[eventKey];
|
|
570
|
+
if (!Array.isArray(existing)) return;
|
|
571
|
+
const cleaned = filterOutMembank(existing);
|
|
572
|
+
if (cleaned.length === 0) delete hooks[eventKey];
|
|
573
|
+
else hooks[eventKey] = cleaned;
|
|
574
|
+
}
|
|
575
|
+
function pruneFlatEvent(hooks, eventKey) {
|
|
576
|
+
const existing = hooks[eventKey];
|
|
577
|
+
if (!Array.isArray(existing)) return;
|
|
578
|
+
const cleaned = filterOutMembankFlat(existing);
|
|
579
|
+
if (cleaned.length === 0) delete hooks[eventKey];
|
|
580
|
+
else hooks[eventKey] = cleaned;
|
|
581
|
+
}
|
|
582
|
+
const writers = {
|
|
583
|
+
"claude-code": {
|
|
584
|
+
inspect(resolver) {
|
|
585
|
+
const hooks = readJson(join(resolver.home(), ".claude", "settings.json")).hooks ?? {};
|
|
586
|
+
return {
|
|
587
|
+
status: "ready",
|
|
588
|
+
hooks: [{
|
|
589
|
+
event: "SessionStart",
|
|
590
|
+
command: "npx @membank/cli@latest inject --harness claude-code",
|
|
591
|
+
existingCommand: extractInjectCommand((Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []).flatMap(getHooksArray)) || null
|
|
592
|
+
}]
|
|
593
|
+
};
|
|
594
|
+
},
|
|
595
|
+
write(resolver, events) {
|
|
596
|
+
const cfgPath = join(resolver.home(), ".claude", "settings.json");
|
|
597
|
+
const cfg = readJson(cfgPath);
|
|
598
|
+
const hooks = cfg.hooks ?? {};
|
|
599
|
+
const newHooks = { ...hooks };
|
|
600
|
+
pruneNestedEvent(newHooks, "UserPromptSubmit");
|
|
601
|
+
pruneNestedEvent(newHooks, "PostToolUseFailure");
|
|
602
|
+
if (events.includes("SessionStart")) newHooks.SessionStart = [...filterOutMembank(Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []), {
|
|
603
|
+
matcher: "",
|
|
604
|
+
hooks: [{
|
|
605
|
+
type: "command",
|
|
606
|
+
command: "npx @membank/cli@latest inject --harness claude-code"
|
|
607
|
+
}]
|
|
608
|
+
}];
|
|
609
|
+
writeJsonAtomic(cfgPath, {
|
|
610
|
+
...cfg,
|
|
611
|
+
hooks: newHooks
|
|
612
|
+
});
|
|
613
|
+
return { status: "written" };
|
|
614
|
+
}
|
|
615
|
+
},
|
|
616
|
+
"copilot-cli": {
|
|
617
|
+
inspect(resolver) {
|
|
618
|
+
const hooks = readJson(join(resolver.home(), ".copilot", "settings.json")).hooks ?? {};
|
|
619
|
+
return {
|
|
620
|
+
status: "ready",
|
|
621
|
+
hooks: [{
|
|
622
|
+
event: "sessionStart",
|
|
623
|
+
command: "npx @membank/cli@latest inject --harness copilot-cli",
|
|
624
|
+
existingCommand: extractInjectCommand(Array.isArray(hooks.sessionStart) ? hooks.sessionStart : []) || null
|
|
625
|
+
}]
|
|
626
|
+
};
|
|
627
|
+
},
|
|
628
|
+
write(resolver, events) {
|
|
629
|
+
const cfgPath = join(resolver.home(), ".copilot", "settings.json");
|
|
630
|
+
const cfg = readJson(cfgPath);
|
|
631
|
+
const hooks = cfg.hooks ?? {};
|
|
632
|
+
const newHooks = { ...hooks };
|
|
633
|
+
pruneFlatEvent(newHooks, "userPromptSubmitted");
|
|
634
|
+
pruneFlatEvent(newHooks, "postToolUseFailure");
|
|
635
|
+
if (events.includes("sessionStart")) newHooks.sessionStart = [...filterOutMembankFlat(Array.isArray(hooks.sessionStart) ? hooks.sessionStart : []), {
|
|
636
|
+
type: "command",
|
|
637
|
+
bash: "npx @membank/cli@latest inject --harness copilot-cli",
|
|
638
|
+
timeoutSec: 30
|
|
639
|
+
}];
|
|
640
|
+
writeJsonAtomic(cfgPath, {
|
|
641
|
+
version: cfg.version ?? 1,
|
|
642
|
+
...cfg,
|
|
643
|
+
hooks: newHooks
|
|
644
|
+
});
|
|
645
|
+
return { status: "written" };
|
|
646
|
+
}
|
|
647
|
+
},
|
|
648
|
+
codex: {
|
|
649
|
+
inspect(resolver) {
|
|
650
|
+
const hooks = readJson(join(resolver.home(), ".codex", "hooks.json")).hooks ?? {};
|
|
651
|
+
return {
|
|
652
|
+
status: "ready",
|
|
653
|
+
hooks: [{
|
|
654
|
+
event: "SessionStart",
|
|
655
|
+
command: "npx @membank/cli@latest inject --harness codex",
|
|
656
|
+
existingCommand: extractInjectCommand((Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []).flatMap(getHooksArray)) || null
|
|
657
|
+
}]
|
|
658
|
+
};
|
|
659
|
+
},
|
|
660
|
+
write(resolver, events) {
|
|
661
|
+
const cfgPath = join(resolver.home(), ".codex", "hooks.json");
|
|
662
|
+
const cfg = readJson(cfgPath);
|
|
663
|
+
const hooks = cfg.hooks ?? {};
|
|
664
|
+
const newHooks = { ...hooks };
|
|
665
|
+
pruneNestedEvent(newHooks, "UserPromptSubmit");
|
|
666
|
+
pruneNestedEvent(newHooks, "PostToolUse");
|
|
667
|
+
if (events.includes("SessionStart")) newHooks.SessionStart = [...filterOutMembank(Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []), {
|
|
668
|
+
matcher: "",
|
|
669
|
+
hooks: [{
|
|
670
|
+
type: "command",
|
|
671
|
+
command: "npx @membank/cli@latest inject --harness codex",
|
|
672
|
+
timeout: 30
|
|
673
|
+
}]
|
|
674
|
+
}];
|
|
675
|
+
writeJsonAtomic(cfgPath, {
|
|
676
|
+
...cfg,
|
|
677
|
+
hooks: newHooks
|
|
678
|
+
});
|
|
679
|
+
return { status: "written" };
|
|
680
|
+
}
|
|
681
|
+
},
|
|
682
|
+
opencode: {
|
|
683
|
+
inspect(resolver) {
|
|
684
|
+
const pluginPath = join(resolver.home(), ".config", "opencode", "plugins", "membank.js");
|
|
685
|
+
let existingCommand = null;
|
|
686
|
+
if (existsSync(pluginPath)) {
|
|
687
|
+
if (readFileSync(pluginPath, "utf8").includes("@membank/cli")) existingCommand = pluginPath;
|
|
688
|
+
}
|
|
689
|
+
return {
|
|
690
|
+
status: "ready",
|
|
691
|
+
hooks: [{
|
|
692
|
+
event: "plugin",
|
|
693
|
+
command: pluginPath,
|
|
694
|
+
existingCommand
|
|
695
|
+
}]
|
|
696
|
+
};
|
|
697
|
+
},
|
|
698
|
+
write(resolver, events) {
|
|
699
|
+
if (events.length === 0) return { status: "written" };
|
|
700
|
+
const pluginPath = join(resolver.home(), ".config", "opencode", "plugins", "membank.js");
|
|
701
|
+
mkdirSync(dirname(pluginPath), { recursive: true });
|
|
702
|
+
writeFileSync(pluginPath, `${newOpencodePlugin()}\n`, "utf8");
|
|
703
|
+
return { status: "written" };
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
function newOpencodePlugin() {
|
|
708
|
+
return [
|
|
709
|
+
"export default {",
|
|
710
|
+
" hooks: {",
|
|
711
|
+
" \"session.start\": async ({ $ }) => {",
|
|
712
|
+
" return await $`npx @membank/cli@latest inject`.text();",
|
|
713
|
+
" },",
|
|
714
|
+
" },",
|
|
715
|
+
"};"
|
|
716
|
+
].join("\n");
|
|
717
|
+
}
|
|
718
|
+
var InjectionHookWriter = class {
|
|
719
|
+
#resolver;
|
|
720
|
+
constructor(resolver = defaultPathResolver) {
|
|
721
|
+
this.#resolver = resolver;
|
|
722
|
+
}
|
|
723
|
+
inspect(harness) {
|
|
724
|
+
const writer = writers[harness];
|
|
725
|
+
if (!writer) return { status: "not-supported" };
|
|
726
|
+
return writer.inspect(this.#resolver);
|
|
727
|
+
}
|
|
728
|
+
write(harness, events) {
|
|
729
|
+
const writer = writers[harness];
|
|
730
|
+
if (!writer) return { status: "not-supported" };
|
|
731
|
+
return writer.write(this.#resolver, events);
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
//#endregion
|
|
808
735
|
//#region src/setup/model-downloader.ts
|
|
809
736
|
const MODEL_NAME = "Xenova/bge-small-en-v1.5";
|
|
810
737
|
var ModelDownloadError = class extends Error {
|
|
@@ -954,7 +881,6 @@ var SetupOrchestrator = class {
|
|
|
954
881
|
detectedHarnesses: [],
|
|
955
882
|
configuredHarnesses: [],
|
|
956
883
|
injectionHooksConfigured: [],
|
|
957
|
-
stopHooksConfigured: [],
|
|
958
884
|
modelDownloaded: false
|
|
959
885
|
}));
|
|
960
886
|
return [];
|
|
@@ -968,10 +894,7 @@ var SetupOrchestrator = class {
|
|
|
968
894
|
out("Planned changes (dry-run — no files written):");
|
|
969
895
|
for (const h of detected) {
|
|
970
896
|
out(` ⚠ ${h.name}: would write MCP config`);
|
|
971
|
-
if (this.#hookWriter) {
|
|
972
|
-
out(` ⚠ ${h.name}: would write injection hook config`);
|
|
973
|
-
out(` ⚠ ${h.name}: would write stop hook config`);
|
|
974
|
-
}
|
|
897
|
+
if (this.#hookWriter) out(` ⚠ ${h.name}: would write injection hook config`);
|
|
975
898
|
}
|
|
976
899
|
out("");
|
|
977
900
|
out(" ⚠ Model download: skipped (dry-run)");
|
|
@@ -1038,11 +961,8 @@ var SetupOrchestrator = class {
|
|
|
1038
961
|
}
|
|
1039
962
|
out("");
|
|
1040
963
|
const injectionHooksConfigured = [];
|
|
1041
|
-
const stopHooksConfigured = [];
|
|
1042
964
|
if (this.#hookWriter) {
|
|
1043
|
-
|
|
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));
|
|
965
|
+
injectionHooksConfigured.push(...await this.#runHookSetup(detected, yes, out));
|
|
1046
966
|
out("");
|
|
1047
967
|
}
|
|
1048
968
|
let modelDownloaded = false;
|
|
@@ -1056,7 +976,6 @@ var SetupOrchestrator = class {
|
|
|
1056
976
|
detectedHarnesses: detected.map((h) => h.name),
|
|
1057
977
|
configuredHarnesses: results.filter((r) => r.status === "written").map((r) => r.harness),
|
|
1058
978
|
injectionHooksConfigured,
|
|
1059
|
-
stopHooksConfigured,
|
|
1060
979
|
modelDownloaded
|
|
1061
980
|
};
|
|
1062
981
|
this.#out(JSON.stringify(output));
|
|
@@ -1066,28 +985,33 @@ var SetupOrchestrator = class {
|
|
|
1066
985
|
}
|
|
1067
986
|
return results;
|
|
1068
987
|
}
|
|
1069
|
-
async #
|
|
988
|
+
async #runHookSetup(detected, yes, out) {
|
|
1070
989
|
const configured = [];
|
|
990
|
+
const w = this.#hookWriter;
|
|
1071
991
|
for (const h of detected) try {
|
|
1072
|
-
const
|
|
1073
|
-
if (
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
992
|
+
const inspected = w.inspect(h.name);
|
|
993
|
+
if (inspected.status === "not-supported") continue;
|
|
994
|
+
const toWrite = [];
|
|
995
|
+
for (const hook of inspected.hooks) if (hook.existingCommand === null) {
|
|
996
|
+
out(` ${h.name}: ${hook.event} injection hook`);
|
|
997
|
+
out(` Command: ${hook.command}`);
|
|
998
|
+
if (yes || await this.#prompter(` Configure ${hook.event} injection hook for ${h.name}?`)) toWrite.push(hook.event);
|
|
1077
999
|
} else {
|
|
1078
|
-
out(` ⚠ ${h.name}: ${
|
|
1079
|
-
out(` Current: ${
|
|
1080
|
-
out(` New: ${
|
|
1081
|
-
if (yes || await this.#prompter(` Replace ${
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
}
|
|
1000
|
+
out(` ⚠ ${h.name}: ${hook.event} injection hook already configured`);
|
|
1001
|
+
out(` Current: ${hook.existingCommand}`);
|
|
1002
|
+
if (hook.existingCommand !== hook.command) out(` New: ${hook.command}`);
|
|
1003
|
+
if (yes || await this.#prompter(` Replace ${hook.event} injection hook for ${h.name}?`)) toWrite.push(hook.event);
|
|
1004
|
+
}
|
|
1005
|
+
if (toWrite.length > 0) {
|
|
1006
|
+
w.write(h.name, toWrite);
|
|
1007
|
+
const skippedCount = inspected.hooks.length - toWrite.length;
|
|
1008
|
+
const label = skippedCount > 0 ? `${toWrite.length} injection hook(s) written, ${skippedCount} skipped` : `${toWrite.length} injection hook(s) written`;
|
|
1009
|
+
out(` ✓ ${h.name}: ${label}`);
|
|
1010
|
+
configured.push(h.name);
|
|
1087
1011
|
}
|
|
1088
1012
|
} catch (err) {
|
|
1089
1013
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1090
|
-
out(` ✗ ${h.name}
|
|
1014
|
+
out(` ✗ ${h.name} injection hooks: ${msg}`);
|
|
1091
1015
|
}
|
|
1092
1016
|
return configured;
|
|
1093
1017
|
}
|
|
@@ -1221,7 +1145,7 @@ program.command("import <file>").description("import memories from a JSON export
|
|
|
1221
1145
|
db.close();
|
|
1222
1146
|
}
|
|
1223
1147
|
});
|
|
1224
|
-
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) => {
|
|
1148
|
+
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 (only session-start is supported; other values no-op for legacy hook compatibility)", "session-start").action(async (cmdOptions) => {
|
|
1225
1149
|
try {
|
|
1226
1150
|
await injectCommand(cmdOptions);
|
|
1227
1151
|
} catch (err) {
|
|
@@ -1229,9 +1153,6 @@ program.command("inject").description("output session context for harness inject
|
|
|
1229
1153
|
process.exit(2);
|
|
1230
1154
|
}
|
|
1231
1155
|
});
|
|
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
|
-
});
|
|
1235
1156
|
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) => {
|
|
1236
1157
|
const globalOpts = program.opts();
|
|
1237
1158
|
const autoYes = cmdOptions.yes === true || globalOpts.yes === true;
|
|
@@ -1263,6 +1184,14 @@ program.command("setup").description("detect installed harnesses and write MCP c
|
|
|
1263
1184
|
process.exit(2);
|
|
1264
1185
|
}
|
|
1265
1186
|
});
|
|
1187
|
+
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) => {
|
|
1188
|
+
try {
|
|
1189
|
+
await dashboardCommand(cmdOptions);
|
|
1190
|
+
} catch (err) {
|
|
1191
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
1192
|
+
process.exit(2);
|
|
1193
|
+
}
|
|
1194
|
+
});
|
|
1266
1195
|
program.on("command:*", () => {
|
|
1267
1196
|
program.outputHelp();
|
|
1268
1197
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@membank/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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/core": "0.
|
|
21
|
-
"@membank/
|
|
20
|
+
"@membank/core": "0.4.0",
|
|
21
|
+
"@membank/dashboard": "0.2.0",
|
|
22
|
+
"@membank/mcp": "0.4.0"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
25
|
"@types/node": "^25.6.0",
|