@membank/cli 0.12.0 → 0.13.1

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.
Files changed (2) hide show
  1. package/dist/index.mjs +127 -60
  2. package/package.json +3 -3
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { cancel, confirm, intro, isCancel, multiselect, note, outro } from "@clack/prompts";
3
3
  import { DatabaseManager, EmbeddingService, MIGRATIONS, MODEL_NAME, MemoryTypeSchema, MemoryTypeSchema as MemoryTypeSchema$1, ModelDownloader, PIN_BUDGET_THRESHOLD, QueryEngine, SessionContextBuilder, createMemoryRepository, createProjectRepository, createSynthesisRepository, resolveProject, runScopeToProjectsMigration, saveMemory } from "@membank/core";
4
- import { runSynthesis, startServer } from "@membank/mcp";
4
+ import { runExtraction, runSynthesis, startServer } from "@membank/mcp";
5
5
  import chalk from "chalk";
6
6
  import { Command } from "commander";
7
7
  import ora from "ora";
@@ -192,6 +192,95 @@ function exportCommand(db, formatter, opts) {
192
192
  else process.stdout.write(`Exported ${memories.length} memories to ${outputPath}\n`);
193
193
  }
194
194
  //#endregion
195
+ //#region src/commands/extract.ts
196
+ const ExtractionHarnessSchema = z.enum(["claude-code"]);
197
+ const ClaudeCodeStopInputSchema = z.object({
198
+ session_id: z.string(),
199
+ transcript_path: z.string(),
200
+ cwd: z.string().optional(),
201
+ hook_event_name: z.string().optional(),
202
+ stop_hook_active: z.boolean().optional()
203
+ });
204
+ function parseHookPayload(harness, raw) {
205
+ let parsedJson;
206
+ try {
207
+ parsedJson = JSON.parse(raw);
208
+ } catch {
209
+ return {
210
+ ok: false,
211
+ reason: "stdin is not valid JSON"
212
+ };
213
+ }
214
+ if (harness === "claude-code") {
215
+ const parsed = ClaudeCodeStopInputSchema.safeParse(parsedJson);
216
+ if (!parsed.success) return {
217
+ ok: false,
218
+ reason: `invalid hook payload: ${parsed.error.message}`
219
+ };
220
+ return {
221
+ ok: true,
222
+ value: {
223
+ sessionId: parsed.data.session_id,
224
+ transcriptPath: parsed.data.transcript_path,
225
+ stopHookActive: parsed.data.stop_hook_active === true
226
+ }
227
+ };
228
+ }
229
+ return {
230
+ ok: false,
231
+ reason: `unsupported harness: ${harness}`
232
+ };
233
+ }
234
+ async function readStdin() {
235
+ if (process.stdin.isTTY) return "";
236
+ const chunks = [];
237
+ for await (const chunk of process.stdin) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
238
+ return Buffer.concat(chunks).toString("utf8");
239
+ }
240
+ async function extractCommand(opts) {
241
+ const harnessResult = ExtractionHarnessSchema.safeParse(opts.harness ?? "claude-code");
242
+ if (!harnessResult.success) {
243
+ process.stderr.write(`membank extract: unsupported harness "${opts.harness}". Supported: ${ExtractionHarnessSchema.options.join(", ")}\n`);
244
+ return;
245
+ }
246
+ const harness = harnessResult.data;
247
+ let sessionId = opts.sessionId;
248
+ let transcriptPath = opts.transcript;
249
+ let stopHookActive = false;
250
+ if (sessionId === void 0 || transcriptPath === void 0) {
251
+ const raw = await readStdin();
252
+ if (raw.trim().length > 0) {
253
+ const parsed = parseHookPayload(harness, raw);
254
+ if (!parsed.ok) {
255
+ process.stderr.write(`membank extract: ${parsed.reason}; skipping.\n`);
256
+ return;
257
+ }
258
+ sessionId = sessionId ?? parsed.value.sessionId;
259
+ transcriptPath = transcriptPath ?? parsed.value.transcriptPath;
260
+ stopHookActive = parsed.value.stopHookActive;
261
+ }
262
+ }
263
+ if (sessionId === void 0 || transcriptPath === void 0) {
264
+ process.stderr.write("membank extract: missing session_id or transcript_path (provide via stdin or --session/--transcript).\n");
265
+ return;
266
+ }
267
+ if (stopHookActive) {
268
+ process.stderr.write("membank extract: stop_hook_active=true; skipping to avoid recursion.\n");
269
+ return;
270
+ }
271
+ try {
272
+ const result = await runExtraction({
273
+ sessionId,
274
+ transcriptPath
275
+ });
276
+ if (result.status === "skipped") process.stderr.write(`membank extract: skipped (${result.reason})\n`);
277
+ else if (result.status === "failed") process.stderr.write(`membank extract: failed: ${result.error}\n`);
278
+ } catch (err) {
279
+ const msg = err instanceof Error ? err.message : String(err);
280
+ process.stderr.write(`membank extract: ${msg}\n`);
281
+ }
282
+ }
283
+ //#endregion
195
284
  //#region src/commands/import.ts
196
285
  async function importCommand(filePath, db, formatter, prompt) {
197
286
  let raw;
@@ -290,10 +379,6 @@ async function buildText() {
290
379
  }
291
380
  }
292
381
  async function handleEvent(harness, eventName) {
293
- if (harness === "claude-code" && eventName === "Stop") {
294
- process.stdout.write("{}");
295
- return;
296
- }
297
382
  const text = await buildText().catch((err) => {
298
383
  const msg = err instanceof Error ? err.message : String(err);
299
384
  process.stderr.write(`membank inject: ${msg}\n`);
@@ -313,10 +398,6 @@ async function injectCommand(opts) {
313
398
  await handleEvent(harness, "UserPromptSubmit");
314
399
  return;
315
400
  }
316
- if (opts.event === "session-stop" || opts.event === "stop") {
317
- await handleEvent(harness, "Stop");
318
- return;
319
- }
320
401
  process.exit(0);
321
402
  }
322
403
  //#endregion
@@ -1017,7 +1098,7 @@ const writers = {
1017
1098
  },
1018
1099
  {
1019
1100
  event: "Stop",
1020
- command: "npx -y @membank/cli inject --harness claude-code --event session-stop",
1101
+ command: "npx -y @membank/cli extract --harness claude-code",
1021
1102
  existingCommand: extractInjectCommand(stopInner) || null
1022
1103
  }
1023
1104
  ]
@@ -1029,6 +1110,7 @@ const writers = {
1029
1110
  const hooks = MaybeJsonObjectSchema.parse(cfg.hooks) ?? {};
1030
1111
  const newHooks = { ...hooks };
1031
1112
  pruneNestedEvent(newHooks, "PostToolUseFailure");
1113
+ pruneNestedEvent(newHooks, "Stop");
1032
1114
  if (events.includes("SessionStart")) newHooks.SessionStart = [...filterOutMembank(Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []), {
1033
1115
  matcher: "",
1034
1116
  hooks: [{
@@ -1047,7 +1129,9 @@ const writers = {
1047
1129
  matcher: "",
1048
1130
  hooks: [{
1049
1131
  type: "command",
1050
- command: "npx -y @membank/cli inject --harness claude-code --event session-stop"
1132
+ command: "npx -y @membank/cli extract --harness claude-code",
1133
+ async: true,
1134
+ timeout: 600
1051
1135
  }]
1052
1136
  }];
1053
1137
  writeJsonAtomic(cfgPath, {
@@ -1064,27 +1148,18 @@ const writers = {
1064
1148
  const hooks = MaybeJsonObjectSchema.parse(cfg.hooks) ?? {};
1065
1149
  const sessionStart = Array.isArray(hooks.sessionStart) ? hooks.sessionStart : [];
1066
1150
  const userPromptSubmitted = Array.isArray(hooks.userPromptSubmitted) ? hooks.userPromptSubmitted : [];
1067
- const sessionEnd = Array.isArray(hooks.sessionEnd) ? hooks.sessionEnd : [];
1068
1151
  return {
1069
1152
  status: "ready",
1070
1153
  configPath: cfgPath,
1071
- hooks: [
1072
- {
1073
- event: "sessionStart",
1074
- command: "npx -y @membank/cli inject --harness copilot-cli",
1075
- existingCommand: extractInjectCommand(sessionStart) || null
1076
- },
1077
- {
1078
- event: "userPromptSubmitted",
1079
- command: "npx -y @membank/cli inject --harness copilot-cli --event user-prompt-submit",
1080
- existingCommand: extractInjectCommand(userPromptSubmitted) || null
1081
- },
1082
- {
1083
- event: "sessionEnd",
1084
- command: "npx -y @membank/cli inject --harness copilot-cli --event session-stop",
1085
- existingCommand: extractInjectCommand(sessionEnd) || null
1086
- }
1087
- ]
1154
+ hooks: [{
1155
+ event: "sessionStart",
1156
+ command: "npx -y @membank/cli inject --harness copilot-cli",
1157
+ existingCommand: extractInjectCommand(sessionStart) || null
1158
+ }, {
1159
+ event: "userPromptSubmitted",
1160
+ command: "npx -y @membank/cli inject --harness copilot-cli --event user-prompt-submit",
1161
+ existingCommand: extractInjectCommand(userPromptSubmitted) || null
1162
+ }]
1088
1163
  };
1089
1164
  },
1090
1165
  write(resolver, events) {
@@ -1093,6 +1168,7 @@ const writers = {
1093
1168
  const hooks = MaybeJsonObjectSchema.parse(cfg.hooks) ?? {};
1094
1169
  const newHooks = { ...hooks };
1095
1170
  pruneFlatEvent(newHooks, "postToolUseFailure");
1171
+ pruneFlatEvent(newHooks, "sessionEnd");
1096
1172
  if (events.includes("sessionStart")) newHooks.sessionStart = [...filterOutMembankFlat(Array.isArray(hooks.sessionStart) ? hooks.sessionStart : []), {
1097
1173
  type: "command",
1098
1174
  bash: "npx -y @membank/cli inject --harness copilot-cli",
@@ -1103,11 +1179,6 @@ const writers = {
1103
1179
  bash: "npx -y @membank/cli inject --harness copilot-cli --event user-prompt-submit",
1104
1180
  timeoutSec: 30
1105
1181
  }];
1106
- if (events.includes("sessionEnd")) newHooks.sessionEnd = [...filterOutMembankFlat(Array.isArray(hooks.sessionEnd) ? hooks.sessionEnd : []), {
1107
- type: "command",
1108
- bash: "npx -y @membank/cli inject --harness copilot-cli --event session-stop",
1109
- timeoutSec: 30
1110
- }];
1111
1182
  writeJsonAtomic(cfgPath, {
1112
1183
  version: OptionalNumberSchema.parse(cfg.version) ?? 1,
1113
1184
  ...cfg,
@@ -1123,27 +1194,18 @@ const writers = {
1123
1194
  const hooks = MaybeJsonObjectSchema.parse(cfg.hooks) ?? {};
1124
1195
  const sessionStartInner = (Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []).flatMap(getHooksArray);
1125
1196
  const userPromptSubmitInner = (Array.isArray(hooks.UserPromptSubmit) ? hooks.UserPromptSubmit : []).flatMap(getHooksArray);
1126
- const stopInner = (Array.isArray(hooks.Stop) ? hooks.Stop : []).flatMap(getHooksArray);
1127
1197
  return {
1128
1198
  status: "ready",
1129
1199
  configPath: cfgPath,
1130
- hooks: [
1131
- {
1132
- event: "SessionStart",
1133
- command: "npx -y @membank/cli inject --harness codex",
1134
- existingCommand: extractInjectCommand(sessionStartInner) || null
1135
- },
1136
- {
1137
- event: "UserPromptSubmit",
1138
- command: "npx -y @membank/cli inject --harness codex --event user-prompt-submit",
1139
- existingCommand: extractInjectCommand(userPromptSubmitInner) || null
1140
- },
1141
- {
1142
- event: "Stop",
1143
- command: "npx -y @membank/cli inject --harness codex --event session-stop",
1144
- existingCommand: extractInjectCommand(stopInner) || null
1145
- }
1146
- ]
1200
+ hooks: [{
1201
+ event: "SessionStart",
1202
+ command: "npx -y @membank/cli inject --harness codex",
1203
+ existingCommand: extractInjectCommand(sessionStartInner) || null
1204
+ }, {
1205
+ event: "UserPromptSubmit",
1206
+ command: "npx -y @membank/cli inject --harness codex --event user-prompt-submit",
1207
+ existingCommand: extractInjectCommand(userPromptSubmitInner) || null
1208
+ }]
1147
1209
  };
1148
1210
  },
1149
1211
  write(resolver, events) {
@@ -1152,6 +1214,7 @@ const writers = {
1152
1214
  const hooks = MaybeJsonObjectSchema.parse(cfg.hooks) ?? {};
1153
1215
  const newHooks = { ...hooks };
1154
1216
  pruneNestedEvent(newHooks, "PostToolUse");
1217
+ pruneNestedEvent(newHooks, "Stop");
1155
1218
  if (events.includes("SessionStart")) newHooks.SessionStart = [...filterOutMembank(Array.isArray(hooks.SessionStart) ? hooks.SessionStart : []), {
1156
1219
  matcher: "",
1157
1220
  hooks: [{
@@ -1168,14 +1231,6 @@ const writers = {
1168
1231
  timeout: 30
1169
1232
  }]
1170
1233
  }];
1171
- if (events.includes("Stop")) newHooks.Stop = [...filterOutMembank(Array.isArray(hooks.Stop) ? hooks.Stop : []), {
1172
- matcher: "",
1173
- hooks: [{
1174
- type: "command",
1175
- command: "npx -y @membank/cli inject --harness codex --event session-stop",
1176
- timeout: 30
1177
- }]
1178
- }];
1179
1234
  writeJsonAtomic(cfgPath, {
1180
1235
  ...cfg,
1181
1236
  hooks: newHooks
@@ -1639,6 +1694,18 @@ program.command("inject").description("output session context for harness inject
1639
1694
  process.exit(2);
1640
1695
  }
1641
1696
  });
1697
+ program.command("extract").description("(internal) run session-end memory extraction; reads the harness's Stop hook payload from stdin").option("--harness <name>", "harness whose stop-hook payload is on stdin (only claude-code is supported today)", "claude-code").option("--session <id>", "session id (otherwise read from stdin)").option("--transcript <path>", "transcript JSONL path (otherwise read from stdin)").action(async (cmdOptions) => {
1698
+ try {
1699
+ await extractCommand({
1700
+ harness: cmdOptions.harness,
1701
+ sessionId: cmdOptions.session,
1702
+ transcript: cmdOptions.transcript
1703
+ });
1704
+ } catch (err) {
1705
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
1706
+ }
1707
+ process.exit(0);
1708
+ });
1642
1709
  const setupCmd = 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)");
1643
1710
  setupCmd.command("upgrade").description("upgrade harness configs from membank --mcp to standalone membank-mcp").action(async () => {
1644
1711
  const isJson = program.opts().json === true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@membank/cli",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -21,8 +21,8 @@
21
21
  "commander": "^14.0.3",
22
22
  "ora": "^9.4.0",
23
23
  "zod": "^4.4.3",
24
- "@membank/core": "0.10.0",
25
- "@membank/mcp": "0.13.0"
24
+ "@membank/mcp": "0.14.1",
25
+ "@membank/core": "0.11.1"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^25.6.0",