memory-lancedb-pro 1.0.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/cli.ts ADDED
@@ -0,0 +1,611 @@
1
+ /**
2
+ * CLI Commands for Memory Management
3
+ */
4
+
5
+ import type { Command } from "commander";
6
+ import { loadLanceDB, type MemoryEntry, type MemoryStore } from "./src/store.js";
7
+ import type { MemoryRetriever } from "./src/retriever.js";
8
+ import type { MemoryScopeManager } from "./src/scopes.js";
9
+ import type { MemoryMigrator } from "./src/migrate.js";
10
+
11
+ // ============================================================================
12
+ // Types
13
+ // ============================================================================
14
+
15
+ interface CLIContext {
16
+ store: MemoryStore;
17
+ retriever: MemoryRetriever;
18
+ scopeManager: MemoryScopeManager;
19
+ migrator: MemoryMigrator;
20
+ embedder?: import("./src/embedder.js").Embedder;
21
+ }
22
+
23
+ // ============================================================================
24
+ // Utility Functions
25
+ // ============================================================================
26
+
27
+ function formatMemory(memory: any, index?: number): string {
28
+ const prefix = index !== undefined ? `${index + 1}. ` : "";
29
+ const date = new Date(memory.timestamp || memory.createdAt || Date.now()).toISOString().split('T')[0];
30
+ const text = memory.text.slice(0, 100) + (memory.text.length > 100 ? "..." : "");
31
+ return `${prefix}[${memory.category}:${memory.scope}] ${text} (${date})`;
32
+ }
33
+
34
+ function formatJson(obj: any): string {
35
+ return JSON.stringify(obj, null, 2);
36
+ }
37
+
38
+ // ============================================================================
39
+ // CLI Command Implementations
40
+ // ============================================================================
41
+
42
+ export function registerMemoryCLI(program: Command, context: CLIContext): void {
43
+ const memory = program
44
+ .command("memory")
45
+ .description("Enhanced memory management commands");
46
+
47
+ // List memories
48
+ memory
49
+ .command("list")
50
+ .description("List memories with optional filtering")
51
+ .option("--scope <scope>", "Filter by scope")
52
+ .option("--category <category>", "Filter by category")
53
+ .option("--limit <n>", "Maximum number of results", "20")
54
+ .option("--offset <n>", "Number of results to skip", "0")
55
+ .option("--json", "Output as JSON")
56
+ .action(async (options) => {
57
+ try {
58
+ const limit = parseInt(options.limit) || 20;
59
+ const offset = parseInt(options.offset) || 0;
60
+
61
+ let scopeFilter: string[] | undefined;
62
+ if (options.scope) {
63
+ scopeFilter = [options.scope];
64
+ }
65
+
66
+ const memories = await context.store.list(
67
+ scopeFilter,
68
+ options.category,
69
+ limit,
70
+ offset
71
+ );
72
+
73
+ if (options.json) {
74
+ console.log(formatJson(memories));
75
+ } else {
76
+ if (memories.length === 0) {
77
+ console.log("No memories found.");
78
+ } else {
79
+ console.log(`Found ${memories.length} memories:\n`);
80
+ memories.forEach((memory, i) => {
81
+ console.log(formatMemory(memory, offset + i));
82
+ });
83
+ }
84
+ }
85
+ } catch (error) {
86
+ console.error("Failed to list memories:", error);
87
+ process.exit(1);
88
+ }
89
+ });
90
+
91
+ // Search memories
92
+ memory
93
+ .command("search <query>")
94
+ .description("Search memories using hybrid retrieval")
95
+ .option("--scope <scope>", "Search within specific scope")
96
+ .option("--category <category>", "Filter by category")
97
+ .option("--limit <n>", "Maximum number of results", "10")
98
+ .option("--json", "Output as JSON")
99
+ .action(async (query, options) => {
100
+ try {
101
+ const limit = parseInt(options.limit) || 10;
102
+
103
+ let scopeFilter: string[] | undefined;
104
+ if (options.scope) {
105
+ scopeFilter = [options.scope];
106
+ }
107
+
108
+ const results = await context.retriever.retrieve({
109
+ query,
110
+ limit,
111
+ scopeFilter,
112
+ category: options.category,
113
+ });
114
+
115
+ if (options.json) {
116
+ console.log(formatJson(results));
117
+ } else {
118
+ if (results.length === 0) {
119
+ console.log("No relevant memories found.");
120
+ } else {
121
+ console.log(`Found ${results.length} memories:\n`);
122
+ results.forEach((result, i) => {
123
+ const sources = [];
124
+ if (result.sources.vector) sources.push("vector");
125
+ if (result.sources.bm25) sources.push("BM25");
126
+ if (result.sources.reranked) sources.push("reranked");
127
+
128
+ console.log(
129
+ `${i + 1}. [${result.entry.category}:${result.entry.scope}] ${result.entry.text} ` +
130
+ `(${(result.score * 100).toFixed(0)}%, ${sources.join('+')})`
131
+ );
132
+ });
133
+ }
134
+ }
135
+ } catch (error) {
136
+ console.error("Search failed:", error);
137
+ process.exit(1);
138
+ }
139
+ });
140
+
141
+ // Memory statistics
142
+ memory
143
+ .command("stats")
144
+ .description("Show memory statistics")
145
+ .option("--scope <scope>", "Stats for specific scope")
146
+ .option("--json", "Output as JSON")
147
+ .action(async (options) => {
148
+ try {
149
+ let scopeFilter: string[] | undefined;
150
+ if (options.scope) {
151
+ scopeFilter = [options.scope];
152
+ }
153
+
154
+ const stats = await context.store.stats(scopeFilter);
155
+ const scopeStats = context.scopeManager.getStats();
156
+ const retrievalConfig = context.retriever.getConfig();
157
+
158
+ const summary = {
159
+ memory: stats,
160
+ scopes: scopeStats,
161
+ retrieval: {
162
+ mode: retrievalConfig.mode,
163
+ hasFtsSupport: context.store.hasFtsSupport,
164
+ },
165
+ };
166
+
167
+ if (options.json) {
168
+ console.log(formatJson(summary));
169
+ } else {
170
+ console.log(`Memory Statistics:`);
171
+ console.log(`• Total memories: ${stats.totalCount}`);
172
+ console.log(`• Available scopes: ${scopeStats.totalScopes}`);
173
+ console.log(`• Retrieval mode: ${retrievalConfig.mode}`);
174
+ console.log(`• FTS support: ${context.store.hasFtsSupport ? 'Yes' : 'No'}`);
175
+ console.log();
176
+
177
+ console.log("Memories by scope:");
178
+ Object.entries(stats.scopeCounts).forEach(([scope, count]) => {
179
+ console.log(` • ${scope}: ${count}`);
180
+ });
181
+ console.log();
182
+
183
+ console.log("Memories by category:");
184
+ Object.entries(stats.categoryCounts).forEach(([category, count]) => {
185
+ console.log(` • ${category}: ${count}`);
186
+ });
187
+ }
188
+ } catch (error) {
189
+ console.error("Failed to get statistics:", error);
190
+ process.exit(1);
191
+ }
192
+ });
193
+
194
+ // Delete memory
195
+ memory
196
+ .command("delete <id>")
197
+ .description("Delete a specific memory by ID")
198
+ .option("--scope <scope>", "Scope to delete from (for access control)")
199
+ .action(async (id, options) => {
200
+ try {
201
+ let scopeFilter: string[] | undefined;
202
+ if (options.scope) {
203
+ scopeFilter = [options.scope];
204
+ }
205
+
206
+ const deleted = await context.store.delete(id, scopeFilter);
207
+
208
+ if (deleted) {
209
+ console.log(`Memory ${id} deleted successfully.`);
210
+ } else {
211
+ console.log(`Memory ${id} not found or access denied.`);
212
+ process.exit(1);
213
+ }
214
+ } catch (error) {
215
+ console.error("Failed to delete memory:", error);
216
+ process.exit(1);
217
+ }
218
+ });
219
+
220
+ // Bulk delete
221
+ memory
222
+ .command("delete-bulk")
223
+ .description("Bulk delete memories with filters")
224
+ .option("--scope <scopes...>", "Scopes to delete from (required)")
225
+ .option("--before <date>", "Delete memories before this date (YYYY-MM-DD)")
226
+ .option("--dry-run", "Show what would be deleted without actually deleting")
227
+ .action(async (options) => {
228
+ try {
229
+ if (!options.scope || options.scope.length === 0) {
230
+ console.error("At least one scope must be specified for safety.");
231
+ process.exit(1);
232
+ }
233
+
234
+ let beforeTimestamp: number | undefined;
235
+ if (options.before) {
236
+ const date = new Date(options.before);
237
+ if (isNaN(date.getTime())) {
238
+ console.error("Invalid date format. Use YYYY-MM-DD.");
239
+ process.exit(1);
240
+ }
241
+ beforeTimestamp = date.getTime();
242
+ }
243
+
244
+ if (options.dryRun) {
245
+ console.log("DRY RUN - No memories will be deleted");
246
+ console.log(`Filters: scopes=${options.scope.join(',')}, before=${options.before || 'none'}`);
247
+
248
+ // Show what would be deleted
249
+ const stats = await context.store.stats(options.scope);
250
+ console.log(`Would delete from ${stats.totalCount} memories in matching scopes.`);
251
+ } else {
252
+ const deletedCount = await context.store.bulkDelete(options.scope, beforeTimestamp);
253
+ console.log(`Deleted ${deletedCount} memories.`);
254
+ }
255
+ } catch (error) {
256
+ console.error("Bulk delete failed:", error);
257
+ process.exit(1);
258
+ }
259
+ });
260
+
261
+ // Export memories
262
+ memory
263
+ .command("export")
264
+ .description("Export memories to JSON")
265
+ .option("--scope <scope>", "Export specific scope")
266
+ .option("--category <category>", "Export specific category")
267
+ .option("--output <file>", "Output file (default: stdout)")
268
+ .action(async (options) => {
269
+ try {
270
+ let scopeFilter: string[] | undefined;
271
+ if (options.scope) {
272
+ scopeFilter = [options.scope];
273
+ }
274
+
275
+ const memories = await context.store.list(
276
+ scopeFilter,
277
+ options.category,
278
+ 1000 // Large limit for export
279
+ );
280
+
281
+ const exportData = {
282
+ version: "1.0",
283
+ exportedAt: new Date().toISOString(),
284
+ count: memories.length,
285
+ filters: {
286
+ scope: options.scope,
287
+ category: options.category,
288
+ },
289
+ memories: memories.map(m => ({
290
+ ...m,
291
+ vector: undefined, // Exclude vectors to reduce size
292
+ })),
293
+ };
294
+
295
+ const output = formatJson(exportData);
296
+
297
+ if (options.output) {
298
+ const fs = await import("node:fs/promises");
299
+ await fs.writeFile(options.output, output);
300
+ console.log(`Exported ${memories.length} memories to ${options.output}`);
301
+ } else {
302
+ console.log(output);
303
+ }
304
+ } catch (error) {
305
+ console.error("Export failed:", error);
306
+ process.exit(1);
307
+ }
308
+ });
309
+
310
+ // Import memories
311
+ memory
312
+ .command("import <file>")
313
+ .description("Import memories from JSON file")
314
+ .option("--scope <scope>", "Import into specific scope")
315
+ .option("--dry-run", "Show what would be imported without actually importing")
316
+ .action(async (file, options) => {
317
+ try {
318
+ const fs = await import("node:fs/promises");
319
+ const content = await fs.readFile(file, "utf-8");
320
+ const data = JSON.parse(content);
321
+
322
+ if (!data.memories || !Array.isArray(data.memories)) {
323
+ throw new Error("Invalid import file format");
324
+ }
325
+
326
+ if (options.dryRun) {
327
+ console.log("DRY RUN - No memories will be imported");
328
+ console.log(`Would import ${data.memories.length} memories`);
329
+ if (options.scope) {
330
+ console.log(`Target scope: ${options.scope}`);
331
+ }
332
+ return;
333
+ }
334
+
335
+ console.log(`Importing ${data.memories.length} memories...`);
336
+
337
+ let imported = 0;
338
+ let skipped = 0;
339
+
340
+ if (!context.embedder) {
341
+ console.error("Import requires an embedder (not available in basic CLI mode).");
342
+ console.error("Use the plugin's memory_store tool or pass embedder to createMemoryCLI.");
343
+ return;
344
+ }
345
+
346
+ const targetScope = options.scope || context.scopeManager.getDefaultScope();
347
+
348
+ for (const memory of data.memories) {
349
+ try {
350
+ const text = memory.text;
351
+ if (!text || typeof text !== "string" || text.length < 2) {
352
+ skipped++;
353
+ continue;
354
+ }
355
+
356
+ // Check for duplicates
357
+ const existing = await context.retriever.retrieve({
358
+ query: text,
359
+ limit: 1,
360
+ scopeFilter: [targetScope],
361
+ });
362
+ if (existing.length > 0 && existing[0].score > 0.95) {
363
+ skipped++;
364
+ continue;
365
+ }
366
+
367
+ const vector = await context.embedder.embedPassage(text);
368
+ await context.store.store({
369
+ text,
370
+ vector,
371
+ importance: memory.importance ?? 0.7,
372
+ category: memory.category || "other",
373
+ scope: targetScope,
374
+ });
375
+ imported++;
376
+ } catch (error) {
377
+ console.warn(`Failed to import memory: ${error}`);
378
+ skipped++;
379
+ }
380
+ }
381
+
382
+ console.log(`Import completed: ${imported} imported, ${skipped} skipped`);
383
+ } catch (error) {
384
+ console.error("Import failed:", error);
385
+ process.exit(1);
386
+ }
387
+ });
388
+
389
+ // Re-embed an existing LanceDB into the current target DB (A/B testing)
390
+ memory
391
+ .command("reembed")
392
+ .description("Re-embed memories from a source LanceDB database into the current target database")
393
+ .requiredOption("--source-db <path>", "Source LanceDB database directory")
394
+ .option("--batch-size <n>", "Batch size for embedding calls", "32")
395
+ .option("--limit <n>", "Limit number of rows to process (for testing)")
396
+ .option("--dry-run", "Show what would be re-embedded without writing")
397
+ .option("--skip-existing", "Skip entries whose id already exists in the target DB")
398
+ .option("--force", "Allow using the same source-db as the target dbPath (DANGEROUS)")
399
+ .action(async (options) => {
400
+ try {
401
+ if (!context.embedder) {
402
+ console.error("Re-embed requires an embedder (not available in basic CLI mode).");
403
+ return;
404
+ }
405
+
406
+ const fs = await import("node:fs/promises");
407
+
408
+ const sourceDbPath = options.sourceDb as string;
409
+ const batchSize = clampInt(parseInt(options.batchSize, 10) || 32, 1, 128);
410
+ const limit = options.limit ? clampInt(parseInt(options.limit, 10) || 0, 1, 1000000) : undefined;
411
+ const dryRun = options.dryRun === true;
412
+ const skipExisting = options.skipExisting === true;
413
+ const force = options.force === true;
414
+
415
+ // Safety: prevent accidental in-place re-embedding
416
+ let sourceReal = sourceDbPath;
417
+ let targetReal = context.store.dbPath;
418
+ try {
419
+ sourceReal = await fs.realpath(sourceDbPath);
420
+ } catch {}
421
+ try {
422
+ targetReal = await fs.realpath(context.store.dbPath);
423
+ } catch {}
424
+
425
+ if (!force && sourceReal === targetReal) {
426
+ console.error("Refusing to re-embed in-place: source-db equals target dbPath. Use a new dbPath or pass --force.");
427
+ process.exit(1);
428
+ }
429
+
430
+ const lancedb = await loadLanceDB();
431
+ const db = await lancedb.connect(sourceDbPath);
432
+ const table = await db.openTable("memories");
433
+
434
+ let query = table
435
+ .query()
436
+ .select(["id", "text", "category", "scope", "importance", "timestamp", "metadata"]);
437
+
438
+ if (limit) query = query.limit(limit);
439
+
440
+ const rows = (await query.toArray())
441
+ .filter((r: any) => r && typeof r.text === "string" && r.text.trim().length > 0)
442
+ .filter((r: any) => r.id && r.id !== "__schema__");
443
+
444
+ if (rows.length === 0) {
445
+ console.log("No source memories found.");
446
+ return;
447
+ }
448
+
449
+ console.log(
450
+ `Re-embedding ${rows.length} memories from ${sourceDbPath} → ${context.store.dbPath} (batchSize=${batchSize})`
451
+ );
452
+
453
+ if (dryRun) {
454
+ console.log("DRY RUN - No memories will be written");
455
+ console.log(`First example: ${rows[0].id?.slice?.(0, 8)} ${String(rows[0].text).slice(0, 80)}`);
456
+ return;
457
+ }
458
+
459
+ let processed = 0;
460
+ let imported = 0;
461
+ let skipped = 0;
462
+
463
+ for (let i = 0; i < rows.length; i += batchSize) {
464
+ const batch = rows.slice(i, i + batchSize);
465
+ const texts = batch.map((r: any) => String(r.text));
466
+ const vectors = await context.embedder.embedBatchPassage(texts);
467
+
468
+ for (let j = 0; j < batch.length; j++) {
469
+ processed++;
470
+ const row = batch[j];
471
+ const vector = vectors[j];
472
+
473
+ if (!vector || vector.length === 0) {
474
+ skipped++;
475
+ continue;
476
+ }
477
+
478
+ const id = String(row.id);
479
+ if (skipExisting) {
480
+ const exists = await context.store.hasId(id);
481
+ if (exists) {
482
+ skipped++;
483
+ continue;
484
+ }
485
+ }
486
+
487
+ const entry: MemoryEntry = {
488
+ id,
489
+ text: String(row.text),
490
+ vector,
491
+ category: (row.category as any) || "other",
492
+ scope: (row.scope as string | undefined) || "global",
493
+ importance: typeof row.importance === "number" ? row.importance : 0.7,
494
+ timestamp: typeof row.timestamp === "number" ? row.timestamp : Date.now(),
495
+ metadata: typeof row.metadata === "string" ? row.metadata : "{}",
496
+ };
497
+
498
+ await context.store.importEntry(entry);
499
+ imported++;
500
+ }
501
+
502
+ if (processed % 100 === 0 || processed === rows.length) {
503
+ console.log(`Progress: ${processed}/${rows.length} processed, ${imported} imported, ${skipped} skipped`);
504
+ }
505
+ }
506
+
507
+ console.log(`Re-embed completed: ${imported} imported, ${skipped} skipped (processed=${processed}).`);
508
+ } catch (error) {
509
+ console.error("Re-embed failed:", error);
510
+ process.exit(1);
511
+ }
512
+ });
513
+
514
+ // Migration commands
515
+ const migrate = memory
516
+ .command("migrate")
517
+ .description("Migration utilities");
518
+
519
+ migrate
520
+ .command("check")
521
+ .description("Check if migration is needed from legacy memory-lancedb")
522
+ .option("--source <path>", "Specific source database path")
523
+ .action(async (options) => {
524
+ try {
525
+ const check = await context.migrator.checkMigrationNeeded(options.source);
526
+
527
+ console.log("Migration Check Results:");
528
+ console.log(`• Legacy database found: ${check.sourceFound ? 'Yes' : 'No'}`);
529
+ if (check.sourceDbPath) {
530
+ console.log(`• Source path: ${check.sourceDbPath}`);
531
+ }
532
+ if (check.entryCount !== undefined) {
533
+ console.log(`• Entries to migrate: ${check.entryCount}`);
534
+ }
535
+ console.log(`• Migration needed: ${check.needed ? 'Yes' : 'No'}`);
536
+ } catch (error) {
537
+ console.error("Migration check failed:", error);
538
+ process.exit(1);
539
+ }
540
+ });
541
+
542
+ migrate
543
+ .command("run")
544
+ .description("Run migration from legacy memory-lancedb")
545
+ .option("--source <path>", "Specific source database path")
546
+ .option("--default-scope <scope>", "Default scope for migrated data", "global")
547
+ .option("--dry-run", "Show what would be migrated without actually migrating")
548
+ .option("--skip-existing", "Skip entries that already exist")
549
+ .action(async (options) => {
550
+ try {
551
+ const result = await context.migrator.migrate({
552
+ sourceDbPath: options.source,
553
+ defaultScope: options.defaultScope,
554
+ dryRun: options.dryRun,
555
+ skipExisting: options.skipExisting,
556
+ });
557
+
558
+ console.log("Migration Results:");
559
+ console.log(`• Status: ${result.success ? 'Success' : 'Failed'}`);
560
+ console.log(`• Migrated: ${result.migratedCount}`);
561
+ console.log(`• Skipped: ${result.skippedCount}`);
562
+ if (result.errors.length > 0) {
563
+ console.log(`• Errors: ${result.errors.length}`);
564
+ result.errors.forEach(error => console.log(` - ${error}`));
565
+ }
566
+ console.log(`• Summary: ${result.summary}`);
567
+
568
+ if (!result.success) {
569
+ process.exit(1);
570
+ }
571
+ } catch (error) {
572
+ console.error("Migration failed:", error);
573
+ process.exit(1);
574
+ }
575
+ });
576
+
577
+ migrate
578
+ .command("verify")
579
+ .description("Verify migration results")
580
+ .option("--source <path>", "Specific source database path")
581
+ .action(async (options) => {
582
+ try {
583
+ const result = await context.migrator.verifyMigration(options.source);
584
+
585
+ console.log("Migration Verification:");
586
+ console.log(`• Valid: ${result.valid ? 'Yes' : 'No'}`);
587
+ console.log(`• Source count: ${result.sourceCount}`);
588
+ console.log(`• Target count: ${result.targetCount}`);
589
+
590
+ if (result.issues.length > 0) {
591
+ console.log("• Issues:");
592
+ result.issues.forEach(issue => console.log(` - ${issue}`));
593
+ }
594
+
595
+ if (!result.valid) {
596
+ process.exit(1);
597
+ }
598
+ } catch (error) {
599
+ console.error("Verification failed:", error);
600
+ process.exit(1);
601
+ }
602
+ });
603
+ }
604
+
605
+ // ============================================================================
606
+ // Factory Function
607
+ // ============================================================================
608
+
609
+ export function createMemoryCLI(context: CLIContext) {
610
+ return (program: Command) => registerMemoryCLI(program, context);
611
+ }