@membank/cli 0.5.1 → 0.7.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.
Files changed (2) hide show
  1. package/dist/index.mjs +84 -37
  2. package/package.json +4 -4
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { cancel, confirm, intro, isCancel, multiselect, note, outro } from "@clack/prompts";
3
- import { DatabaseManager, EmbeddingService, MEMORY_TYPE_VALUES, MemoryRepository, QueryEngine, SessionContextBuilder, resolveScope } from "@membank/core";
3
+ import { DatabaseManager, EmbeddingService, MEMORY_TYPE_VALUES, MIGRATIONS, MemoryRepository, ProjectRepository, QueryEngine, SessionContextBuilder, resolveProject, runScopeToProjectsMigration } from "@membank/core";
4
4
  import { startServer } from "@membank/mcp";
5
5
  import chalk from "chalk";
6
6
  import { Command } from "commander";
@@ -20,14 +20,15 @@ async function addCommand(content, options, formatter, db, embeddingService) {
20
20
  const ownDb = db === void 0;
21
21
  const resolvedDb = db ?? DatabaseManager.open();
22
22
  try {
23
- const repo = new MemoryRepository(resolvedDb, embeddingService ?? new EmbeddingService());
23
+ const repo = new MemoryRepository(resolvedDb, embeddingService ?? new EmbeddingService(), new ProjectRepository(resolvedDb));
24
24
  const tags = options.tags !== void 0 ? options.tags.split(",").map((t) => t.trim()) : [];
25
+ const projectScope = options.global ? void 0 : await resolveProject();
25
26
  const spinner = formatter.isJson ? null : ora("Saving memory…").start();
26
27
  const memory = await repo.save({
27
28
  content,
28
29
  type: options.type,
29
30
  tags,
30
- scope: options.scope
31
+ projectScope
31
32
  });
32
33
  spinner?.succeed("Memory saved");
33
34
  formatter.outputMemory(memory);
@@ -48,7 +49,7 @@ async function deleteCommand(id, db, formatter, prompt) {
48
49
  process.exit(1);
49
50
  }
50
51
  if (!await prompt.confirm(`Delete memory ${id}?`)) return;
51
- await new MemoryRepository(db, new EmbeddingService()).delete(id);
52
+ await new MemoryRepository(db, new EmbeddingService(), new ProjectRepository(db)).delete(id);
52
53
  process.stdout.write(`${chalk.green("✓")} Deleted memory: ${chalk.dim(id)}\n`);
53
54
  }
54
55
  //#endregion
@@ -59,7 +60,6 @@ function exportCommand(db, formatter, opts) {
59
60
  content: row.content,
60
61
  type: row.type,
61
62
  tags: JSON.parse(row.tags),
62
- scope: row.scope,
63
63
  sourceHarness: row.source,
64
64
  accessCount: row.access_count,
65
65
  pinned: row.pinned !== 0,
@@ -87,7 +87,7 @@ const MEMORY_TYPES = new Set(MEMORY_TYPE_VALUES);
87
87
  function isValidRecord(r) {
88
88
  if (typeof r !== "object" || r === null) return false;
89
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) && typeof rec.scope === "string" && rec.scope.length > 0;
90
+ return typeof rec.id === "string" && rec.id.length > 0 && typeof rec.content === "string" && typeof rec.type === "string" && MEMORY_TYPES.has(rec.type);
91
91
  }
92
92
  function isExportFile(parsed) {
93
93
  if (typeof parsed !== "object" || parsed === null) return false;
@@ -115,19 +115,19 @@ async function importCommand(filePath, db, formatter, prompt) {
115
115
  }
116
116
  const invalidIndex = parsed.memories.findIndex((r) => !isValidRecord(r));
117
117
  if (invalidIndex !== -1) {
118
- formatter.error(`Invalid memory record at index ${invalidIndex}: must have id, content, type, and scope`);
118
+ formatter.error(`Invalid memory record at index ${invalidIndex}: must have id, content, and type`);
119
119
  process.exit(1);
120
120
  }
121
121
  const count = parsed.memories.length;
122
122
  if (formatter.isJson) process.stdout.write(`${JSON.stringify({ found: count })}\n`);
123
123
  else process.stdout.write(`Found ${count} memories to import.\n`);
124
124
  if (!await prompt.confirm("Import?")) return;
125
- const insertMemory = db.db.prepare(`INSERT OR REPLACE INTO memories (id, content, type, tags, scope, source, access_count, pinned, needs_review, created_at, updated_at)
126
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
125
+ const insertMemory = db.db.prepare(`INSERT OR REPLACE INTO memories (id, content, type, tags, source, access_count, pinned, needs_review, created_at, updated_at)
126
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
127
127
  const insertEmbedding = db.db.prepare(`INSERT OR REPLACE INTO embeddings (rowid, embedding) SELECT m.rowid, ? FROM memories m WHERE m.id = ?`);
128
128
  db.db.transaction(() => {
129
129
  for (const rec of parsed.memories) {
130
- insertMemory.run(rec.id, rec.content, rec.type, JSON.stringify(rec.tags ?? []), rec.scope, rec.sourceHarness ?? null, rec.accessCount ?? 0, rec.pinned ? 1 : 0, rec.needsReview ? 1 : 0, rec.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(), rec.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString());
130
+ insertMemory.run(rec.id, rec.content, rec.type, JSON.stringify(rec.tags ?? []), rec.sourceHarness ?? null, rec.accessCount ?? 0, rec.pinned ? 1 : 0, rec.needsReview ? 1 : 0, rec.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(), rec.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString());
131
131
  if (rec.embedding !== null && rec.embedding !== void 0) {
132
132
  const buf = Buffer.from(rec.embedding, "base64");
133
133
  insertEmbedding.run(buf, rec.id);
@@ -166,7 +166,7 @@ function outputAdditionalContext(text, harness, eventName) {
166
166
  process.stdout.write(`${text}\n`);
167
167
  }
168
168
  async function handleSessionStart(opts) {
169
- const projectScope = opts.scope ?? await resolveScope();
169
+ const projectScope = (await resolveProject()).hash;
170
170
  const db = DatabaseManager.open();
171
171
  let text;
172
172
  try {
@@ -187,7 +187,7 @@ async function injectCommand(opts) {
187
187
  async function listCommand(options, formatter) {
188
188
  const db = DatabaseManager.open();
189
189
  try {
190
- const memories = new MemoryRepository(db, new EmbeddingService()).list({
190
+ const memories = new MemoryRepository(db, new EmbeddingService(), new ProjectRepository(db)).list({
191
191
  type: options.type,
192
192
  pinned: options.pinned
193
193
  });
@@ -197,18 +197,53 @@ async function listCommand(options, formatter) {
197
197
  }
198
198
  }
199
199
  //#endregion
200
+ //#region src/commands/migrate.ts
201
+ async function migrateCommand(mode, name, formatter) {
202
+ if (mode === "list") {
203
+ if (formatter.isJson) process.stdout.write(`${JSON.stringify(MIGRATIONS.map((m) => ({
204
+ name: m.name,
205
+ description: m.description
206
+ })))}\n`);
207
+ else for (const m of MIGRATIONS) process.stdout.write(`${m.name}\n ${m.description}\n`);
208
+ return;
209
+ }
210
+ if (name === void 0) {
211
+ formatter.error("Migration name is required for run mode.");
212
+ process.exit(1);
213
+ }
214
+ if (!MIGRATIONS.some((m) => m.name === name)) {
215
+ formatter.error(`Unknown migration: "${name}". Available: ${MIGRATIONS.map((m) => m.name).join(", ")}`);
216
+ process.exit(1);
217
+ }
218
+ const db = DatabaseManager.open();
219
+ try {
220
+ if (name === "scope-to-projects") {
221
+ const result = await runScopeToProjectsMigration(new ProjectRepository(db));
222
+ if (result === null) {
223
+ formatter.error("No project found for current directory.");
224
+ return;
225
+ }
226
+ if (formatter.isJson) process.stdout.write(`${JSON.stringify(result)}\n`);
227
+ else {
228
+ const { oldName, newName, memoryCount } = result;
229
+ process.stdout.write(`Found project: ${oldName} (${memoryCount} ${memoryCount === 1 ? "memory" : "memories"})\n`);
230
+ process.stdout.write(`Resolved name: ${newName}\n`);
231
+ process.stdout.write(`Renamed → ${newName}\n`);
232
+ process.stdout.write("Done.\n");
233
+ }
234
+ }
235
+ } finally {
236
+ db.close();
237
+ }
238
+ }
239
+ //#endregion
200
240
  //#region src/commands/pin.ts
201
- function pinCommand(id, formatter, db) {
241
+ function pinCommand(id, db) {
202
242
  const ownDb = db === void 0;
203
243
  const resolvedDb = db ?? DatabaseManager.open();
204
244
  try {
205
- if (resolvedDb.db.prepare("SELECT id FROM memories WHERE id = ?").get(id) === void 0) {
206
- formatter.error(`Memory not found: ${id}`);
207
- process.exit(2);
208
- } else {
209
- resolvedDb.db.prepare("UPDATE memories SET pinned = 1 WHERE id = ?").run(id);
210
- process.stdout.write(`${chalk.green("✓")} Pinned: ${chalk.dim(id)}\n`);
211
- }
245
+ new MemoryRepository(resolvedDb, new EmbeddingService(), new ProjectRepository(resolvedDb)).setPin(id, true);
246
+ process.stdout.write(`${chalk.green("✓")} Pinned: ${chalk.dim(id)}\n`);
212
247
  } finally {
213
248
  if (ownDb) resolvedDb.close();
214
249
  }
@@ -219,7 +254,7 @@ async function queryCommand(queryText, options, formatter) {
219
254
  const db = DatabaseManager.open();
220
255
  try {
221
256
  const embedding = new EmbeddingService();
222
- const engine = new QueryEngine(db, embedding, new MemoryRepository(db, embedding));
257
+ const engine = new QueryEngine(db, embedding, new MemoryRepository(db, embedding, new ProjectRepository(db)));
223
258
  const limit = options.limit !== void 0 ? Number.parseInt(options.limit, 10) : 10;
224
259
  const spinner = formatter.isJson ? null : ora("Searching memories…").start();
225
260
  const results = await engine.query({
@@ -238,7 +273,7 @@ async function queryCommand(queryText, options, formatter) {
238
273
  async function statsCommand(formatter) {
239
274
  const db = DatabaseManager.open();
240
275
  try {
241
- const stats = new MemoryRepository(db, new EmbeddingService()).stats();
276
+ const stats = new MemoryRepository(db, new EmbeddingService(), new ProjectRepository(db)).stats();
242
277
  formatter.outputStats(stats);
243
278
  } finally {
244
279
  db.close();
@@ -246,17 +281,12 @@ async function statsCommand(formatter) {
246
281
  }
247
282
  //#endregion
248
283
  //#region src/commands/unpin.ts
249
- function unpinCommand(id, formatter, db) {
284
+ function unpinCommand(id, db) {
250
285
  const ownDb = db === void 0;
251
286
  const resolvedDb = db ?? DatabaseManager.open();
252
287
  try {
253
- if (resolvedDb.db.prepare("SELECT id FROM memories WHERE id = ?").get(id) === void 0) {
254
- formatter.error(`Memory not found: ${id}`);
255
- process.exit(2);
256
- } else {
257
- resolvedDb.db.prepare("UPDATE memories SET pinned = 0 WHERE id = ?").run(id);
258
- process.stdout.write(`${chalk.green("✓")} Unpinned: ${chalk.dim(id)}\n`);
259
- }
288
+ new MemoryRepository(resolvedDb, new EmbeddingService(), new ProjectRepository(resolvedDb)).setPin(id, false);
289
+ process.stdout.write(`${chalk.green("✓")} Unpinned: ${chalk.dim(id)}\n`);
260
290
  } finally {
261
291
  if (ownDb) resolvedDb.close();
262
292
  }
@@ -296,7 +326,8 @@ var Formatter = class Formatter {
296
326
  process.stdout.write("\n");
297
327
  process.stdout.write(` ${colorType(memory.type)} ${chalk.dim(memory.id)}\n`);
298
328
  process.stdout.write(` ${memory.content}\n`);
299
- process.stdout.write(` ${chalk.dim("Tags:")} ${tags} ${chalk.dim("Scope:")} ${memory.scope}\n`);
329
+ const scope = memory.projects.length > 0 ? memory.projects.map((p) => p.name).join(", ") : "global";
330
+ process.stdout.write(` ${chalk.dim("Tags:")} ${tags} ${chalk.dim("Project:")} ${scope}\n`);
300
331
  process.stdout.write(`\n ${chalk.dim(`Hint: pin with membank pin ${memory.id}`)}\n\n`);
301
332
  }
302
333
  outputMemories(memories) {
@@ -322,7 +353,8 @@ var Formatter = class Formatter {
322
353
  });
323
354
  for (const m of memories) {
324
355
  const tags = m.tags.length > 0 ? m.tags.join(", ") : "(none)";
325
- const meta = `${truncate(m.content, 45)}\n${chalk.dim(`${tags} · ${m.scope}`)}`;
356
+ const mScope = m.projects.length > 0 ? m.projects.map((p) => p.name).join(", ") : "global";
357
+ const meta = `${truncate(m.content, 45)}\n${chalk.dim(`${tags} · ${mScope}`)}`;
326
358
  table.push([
327
359
  colorType(m.type),
328
360
  chalk.dim(m.id),
@@ -376,7 +408,8 @@ var Formatter = class Formatter {
376
408
  const scoreStr = r.score.toFixed(4);
377
409
  const score = r.score >= .85 ? chalk.bold(scoreStr) : r.score < .75 ? chalk.dim(scoreStr) : scoreStr;
378
410
  const tags = r.tags.length > 0 ? r.tags.join(", ") : "(none)";
379
- const meta = `${truncate(r.content, 45)}\n${chalk.dim(`${tags} · ${r.scope}`)}`;
411
+ const rScope = r.projects.length > 0 ? r.projects.map((p) => p.name).join(", ") : "global";
412
+ const meta = `${truncate(r.content, 45)}\n${chalk.dim(`${tags} · ${rScope}`)}`;
380
413
  table.push([
381
414
  colorType(r.type),
382
415
  chalk.dim(r.id),
@@ -1252,7 +1285,7 @@ program.command("delete <id>").description("delete a memory by ID").action(async
1252
1285
  db.close();
1253
1286
  }
1254
1287
  });
1255
- program.command("add <content>").description("save a new memory").requiredOption("--type <type>", "memory type (correction|preference|decision|learning|fact)").option("--tags <tags>", "comma-separated tags").option("--scope <scope>", "scope (global or project identifier)").action(async (content, cmdOptions) => {
1288
+ program.command("add <content>").description("save a new memory").requiredOption("--type <type>", "memory type (correction|preference|decision|learning|fact)").option("--tags <tags>", "comma-separated tags").option("--global", "save as a global memory, not tied to any project").action(async (content, cmdOptions) => {
1256
1289
  const globalOpts = program.opts();
1257
1290
  const formatter = Formatter.create(globalOpts.json === true);
1258
1291
  try {
@@ -1266,7 +1299,7 @@ program.command("pin <id>").description("pin a memory by ID").action((id) => {
1266
1299
  const globalOpts = program.opts();
1267
1300
  const formatter = Formatter.create(globalOpts.json === true);
1268
1301
  try {
1269
- pinCommand(id, formatter);
1302
+ pinCommand(id);
1270
1303
  } catch (err) {
1271
1304
  formatter.error(err instanceof Error ? err.message : String(err));
1272
1305
  process.exit(2);
@@ -1276,7 +1309,7 @@ program.command("unpin <id>").description("unpin a memory by ID").action((id) =>
1276
1309
  const globalOpts = program.opts();
1277
1310
  const formatter = Formatter.create(globalOpts.json === true);
1278
1311
  try {
1279
- unpinCommand(id, formatter);
1312
+ unpinCommand(id);
1280
1313
  } catch (err) {
1281
1314
  formatter.error(err instanceof Error ? err.message : String(err));
1282
1315
  process.exit(2);
@@ -1309,7 +1342,7 @@ program.command("import <file>").description("import memories from a JSON export
1309
1342
  db.close();
1310
1343
  }
1311
1344
  });
1312
- 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) => {
1345
+ 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("--event <event>", "hook event type (only session-start is supported; other values no-op for legacy hook compatibility)", "session-start").action(async (cmdOptions) => {
1313
1346
  try {
1314
1347
  await injectCommand(cmdOptions);
1315
1348
  } catch (err) {
@@ -1385,6 +1418,20 @@ program.command("setup").description("detect installed harnesses and write MCP c
1385
1418
  process.exit(2);
1386
1419
  }
1387
1420
  });
1421
+ program.command("migrate <mode> [name]").description("list or run a named data migration (modes: list, run)").action(async (mode, name) => {
1422
+ if (mode !== "list" && mode !== "run") {
1423
+ process.stderr.write(`Error: mode must be "list" or "run"\n`);
1424
+ process.exit(1);
1425
+ }
1426
+ const globalOpts = program.opts();
1427
+ const formatter = Formatter.create(globalOpts.json === true);
1428
+ try {
1429
+ await migrateCommand(mode, name, formatter);
1430
+ } catch (err) {
1431
+ formatter.error(err instanceof Error ? err.message : String(err));
1432
+ process.exit(2);
1433
+ }
1434
+ });
1388
1435
  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) => {
1389
1436
  try {
1390
1437
  await dashboardCommand(cmdOptions);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@membank/cli",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -20,9 +20,9 @@
20
20
  "cli-table3": "^0.6.5",
21
21
  "commander": "^14.0.3",
22
22
  "ora": "^9.4.0",
23
- "@membank/core": "0.5.1",
24
- "@membank/dashboard": "0.2.3",
25
- "@membank/mcp": "0.6.0"
23
+ "@membank/core": "0.6.1",
24
+ "@membank/dashboard": "0.3.1",
25
+ "@membank/mcp": "0.8.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^25.6.0",