claude-memory-layer 1.0.10 → 1.0.12

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 (142) hide show
  1. package/AGENTS.md +60 -0
  2. package/README.md +166 -2
  3. package/bootstrap-kb/decisions/decisions.md +244 -0
  4. package/bootstrap-kb/glossary/glossary.md +46 -0
  5. package/bootstrap-kb/modules/.claude-plugin.md +22 -0
  6. package/bootstrap-kb/modules/agents.md.md +15 -0
  7. package/bootstrap-kb/modules/claude.md.md +15 -0
  8. package/bootstrap-kb/modules/context.md.md +15 -0
  9. package/bootstrap-kb/modules/docs.md +18 -0
  10. package/bootstrap-kb/modules/handoff.md.md +15 -0
  11. package/bootstrap-kb/modules/package-lock.json.md +15 -0
  12. package/bootstrap-kb/modules/package.json.md +15 -0
  13. package/bootstrap-kb/modules/plan.md.md +15 -0
  14. package/bootstrap-kb/modules/readme.md.md +15 -0
  15. package/bootstrap-kb/modules/scripts.md +26 -0
  16. package/bootstrap-kb/modules/spec.md.md +15 -0
  17. package/bootstrap-kb/modules/specs.md +20 -0
  18. package/bootstrap-kb/modules/src.md +51 -0
  19. package/bootstrap-kb/modules/tests.md +42 -0
  20. package/bootstrap-kb/modules/tsconfig.json.md +15 -0
  21. package/bootstrap-kb/modules/vitest.config.ts.md +15 -0
  22. package/bootstrap-kb/overview/overview.md +40 -0
  23. package/bootstrap-kb/sources/manifest.json +950 -0
  24. package/bootstrap-kb/sources/manifest.md +227 -0
  25. package/bootstrap-kb/timeline/timeline.md +57 -0
  26. package/d.sh +3 -0
  27. package/deploy.sh +3 -0
  28. package/dist/cli/index.js +3577 -389
  29. package/dist/cli/index.js.map +4 -4
  30. package/dist/core/index.js +1383 -138
  31. package/dist/core/index.js.map +4 -4
  32. package/dist/hooks/post-tool-use.js +1917 -214
  33. package/dist/hooks/post-tool-use.js.map +4 -4
  34. package/dist/hooks/session-end.js +1813 -231
  35. package/dist/hooks/session-end.js.map +4 -4
  36. package/dist/hooks/session-start.js +1802 -205
  37. package/dist/hooks/session-start.js.map +4 -4
  38. package/dist/hooks/stop.js +1909 -248
  39. package/dist/hooks/stop.js.map +4 -4
  40. package/dist/hooks/user-prompt-submit.js +1861 -206
  41. package/dist/hooks/user-prompt-submit.js.map +4 -4
  42. package/dist/server/api/index.js +2341 -217
  43. package/dist/server/api/index.js.map +4 -4
  44. package/dist/server/index.js +2350 -226
  45. package/dist/server/index.js.map +4 -4
  46. package/dist/services/memory-service.js +1805 -206
  47. package/dist/services/memory-service.js.map +4 -4
  48. package/dist/ui/app.js +1447 -55
  49. package/dist/ui/index.html +318 -147
  50. package/dist/ui/style.css +892 -0
  51. package/docs/MCP_MEMORY_SERVICE_COMPARATIVE_REVIEW.md +271 -0
  52. package/docs/MEMU_ADOPTION.md +40 -0
  53. package/docs/OPERATIONS.md +18 -0
  54. package/memory/.claude-plugin/commands/2026-02-25.md +263 -0
  55. package/memory/_index.md +405 -0
  56. package/memory/default/uncategorized/2026-02-25.md +4839 -0
  57. package/memory/specs/20260207-dashboard-upgrade/2026-02-25.md +142 -0
  58. package/memory/specs/citations-system/2026-02-25.md +1121 -0
  59. package/memory/specs/endless-mode/2026-02-25.md +1392 -0
  60. package/memory/specs/entity-edge-model/2026-02-25.md +1263 -0
  61. package/memory/specs/evidence-aligner-v2/2026-02-25.md +1028 -0
  62. package/memory/specs/mcp-desktop-integration/2026-02-25.md +1334 -0
  63. package/memory/specs/post-tool-use-hook/2026-02-25.md +1164 -0
  64. package/memory/specs/private-tags/2026-02-25.md +1057 -0
  65. package/memory/specs/progressive-disclosure/2026-02-25.md +1436 -0
  66. package/memory/specs/task-entity-system/2026-02-25.md +924 -0
  67. package/memory/specs/vector-outbox-v2/2026-02-25.md +1510 -0
  68. package/memory/specs/web-viewer-ui/2026-02-25.md +1709 -0
  69. package/package.json +9 -2
  70. package/scripts/build.ts +6 -0
  71. package/scripts/fix-sync-gap.js +32 -0
  72. package/scripts/heartbeat-memory-orchestrator.sh +28 -0
  73. package/scripts/report-sync-gap.js +26 -0
  74. package/scripts/review-queue-auto-resolve.js +21 -0
  75. package/scripts/sync-gap-auto-heal.sh +17 -0
  76. package/specs/20260207-dashboard-upgrade/context.md +38 -0
  77. package/specs/20260207-dashboard-upgrade/spec.md +96 -0
  78. package/src/cli/index.ts +391 -60
  79. package/src/core/consolidated-store.ts +63 -1
  80. package/src/core/consolidation-worker.ts +115 -6
  81. package/src/core/event-store.ts +14 -0
  82. package/src/core/index.ts +1 -0
  83. package/src/core/ingest-interceptor.ts +80 -0
  84. package/src/core/markdown-mirror.ts +70 -0
  85. package/src/core/md-mirror.ts +92 -0
  86. package/src/core/mongo-sync-config.ts +165 -0
  87. package/src/core/mongo-sync-worker.ts +381 -0
  88. package/src/core/retriever.ts +540 -150
  89. package/src/core/sqlite-event-store.ts +794 -7
  90. package/src/core/sqlite-wrapper.ts +8 -0
  91. package/src/core/tag-taxonomy.ts +51 -0
  92. package/src/core/turn-state.ts +159 -0
  93. package/src/core/types.ts +51 -8
  94. package/src/core/vector-store.ts +21 -3
  95. package/src/hooks/post-tool-use.ts +68 -23
  96. package/src/hooks/session-end.ts +8 -3
  97. package/src/hooks/stop.ts +96 -25
  98. package/src/hooks/user-prompt-submit.ts +44 -5
  99. package/src/server/api/chat.ts +244 -0
  100. package/src/server/api/citations.ts +3 -3
  101. package/src/server/api/events.ts +30 -5
  102. package/src/server/api/health.ts +53 -0
  103. package/src/server/api/index.ts +9 -1
  104. package/src/server/api/projects.ts +74 -0
  105. package/src/server/api/search.ts +3 -3
  106. package/src/server/api/sessions.ts +3 -3
  107. package/src/server/api/stats.ts +89 -8
  108. package/src/server/api/turns.ts +143 -0
  109. package/src/server/api/utils.ts +46 -0
  110. package/src/services/bootstrap-organizer.ts +443 -0
  111. package/src/services/codex-session-history-importer.ts +474 -0
  112. package/src/services/memory-service.ts +508 -71
  113. package/src/services/session-history-importer.ts +215 -51
  114. package/src/ui/app.js +1447 -55
  115. package/src/ui/index.html +318 -147
  116. package/src/ui/style.css +892 -0
  117. package/tests/bootstrap-organizer.test.ts +111 -0
  118. package/tests/consolidation-worker.test.ts +75 -0
  119. package/tests/ingest-interceptor.test.ts +38 -0
  120. package/tests/markdown-mirror.test.ts +85 -0
  121. package/tests/md-mirror.test.ts +50 -0
  122. package/tests/retriever-fallback-chain.test.ts +223 -0
  123. package/tests/retriever-strategy-scope.test.ts +97 -0
  124. package/tests/retriever.memu-adoption.test.ts +122 -0
  125. package/tests/sqlite-event-store-replication.test.ts +92 -0
  126. package/.claude/settings.local.json +0 -27
  127. package/.claude-memory/test.sqlite +0 -0
  128. package/.history/package_20260201112328.json +0 -45
  129. package/.history/package_20260201113602.json +0 -45
  130. package/.history/package_20260201113713.json +0 -45
  131. package/.history/package_20260201114110.json +0 -45
  132. package/.history/package_20260201114632.json +0 -46
  133. package/.history/package_20260201133143.json +0 -45
  134. package/.history/package_20260201134319.json +0 -45
  135. package/.history/package_20260201134326.json +0 -45
  136. package/.history/package_20260201134334.json +0 -45
  137. package/.history/package_20260201134912.json +0 -45
  138. package/.history/package_20260201142928.json +0 -46
  139. package/.history/package_20260201192048.json +0 -47
  140. package/.history/package_20260202114053.json +0 -49
  141. package/.history/package_20260202121115.json +0 -49
  142. package/test_access.js +0 -49
package/src/cli/index.ts CHANGED
@@ -11,10 +11,14 @@ import * as path from 'path';
11
11
  import * as os from 'os';
12
12
  import {
13
13
  getDefaultMemoryService,
14
- getMemoryServiceForProject
14
+ getMemoryServiceForProject,
15
+ getProjectStoragePath
15
16
  } from '../services/memory-service.js';
16
- import { createSessionHistoryImporter } from '../services/session-history-importer.js';
17
+ import { createSessionHistoryImporter, type ProgressEvent } from '../services/session-history-importer.js';
18
+ import { bootstrapKnowledgeBase } from '../services/bootstrap-organizer.js';
17
19
  import { startServer, stopServer, isServerRunning } from '../server/index.js';
20
+ import { SQLiteEventStore } from '../core/sqlite-event-store.js';
21
+ import { MongoSyncWorker } from '../core/mongo-sync-worker.js';
18
22
 
19
23
  // ============================================================
20
24
  // Hook Installation Utilities
@@ -107,7 +111,7 @@ const program = new Command();
107
111
  program
108
112
  .name('claude-memory-layer')
109
113
  .description('Claude Code Memory Plugin CLI')
110
- .version('1.0.0');
114
+ .version(process.env.CLAUDE_MEMORY_LAYER_VERSION || '0.0.0');
111
115
 
112
116
  // ============================================================
113
117
  // Install / Uninstall Commands
@@ -427,6 +431,356 @@ program
427
431
  }
428
432
  });
429
433
 
434
+ /**
435
+ * Mongo Sync command - sync local SQLite events with a shared MongoDB database (optional)
436
+ */
437
+ program
438
+ .command('mongo-sync')
439
+ .description('Sync events with MongoDB for multi-server collaboration (optional)')
440
+ .option('-p, --project <path>', 'Project path (defaults to cwd)')
441
+ .option('--mongo-uri <uri>', 'MongoDB connection URI (env: CLAUDE_MEMORY_MONGO_URI)')
442
+ .option('--mongo-db <name>', 'MongoDB database name (env: CLAUDE_MEMORY_MONGO_DB)')
443
+ .option('--mongo-project <key>', 'Remote project key (env: CLAUDE_MEMORY_MONGO_PROJECT, default: basename(projectPath))')
444
+ .option('--direction <dir>', 'push|pull|both', 'both')
445
+ .option('--batch-size <n>', 'Batch size', '500')
446
+ .option('--interval <ms>', 'Watch interval ms', '30000')
447
+ .option('--watch', 'Run continuously')
448
+ .action(async (options) => {
449
+ const projectPath = options.project || process.cwd();
450
+ const mongoUri = options.mongoUri || process.env.CLAUDE_MEMORY_MONGO_URI;
451
+ const mongoDb = options.mongoDb || process.env.CLAUDE_MEMORY_MONGO_DB;
452
+ const projectKey = options.mongoProject || process.env.CLAUDE_MEMORY_MONGO_PROJECT || path.basename(projectPath);
453
+ const direction = String(options.direction || 'both').toLowerCase();
454
+
455
+ if (!mongoUri || !mongoDb) {
456
+ console.error('\n❌ MongoDB sync is not configured.');
457
+ console.error(' Set --mongo-uri/--mongo-db or env CLAUDE_MEMORY_MONGO_URI/CLAUDE_MEMORY_MONGO_DB.\n');
458
+ process.exit(1);
459
+ }
460
+
461
+ if (!['push', 'pull', 'both'].includes(direction)) {
462
+ console.error('\n❌ Invalid --direction. Use: push | pull | both\n');
463
+ process.exit(1);
464
+ }
465
+
466
+ const storagePath = getProjectStoragePath(projectPath);
467
+ if (!fs.existsSync(storagePath)) {
468
+ fs.mkdirSync(storagePath, { recursive: true });
469
+ }
470
+
471
+ const batchSizeParsed = parseInt(options.batchSize, 10);
472
+ const intervalParsed = parseInt(options.interval, 10);
473
+ const batchSize = (Number.isFinite(batchSizeParsed) && batchSizeParsed > 0) ? batchSizeParsed : 500;
474
+ const intervalMs = (Number.isFinite(intervalParsed) && intervalParsed > 0) ? intervalParsed : 30000;
475
+
476
+ const sqliteStore = new SQLiteEventStore(path.join(storagePath, 'events.sqlite'));
477
+ const worker = new MongoSyncWorker(sqliteStore, {
478
+ uri: mongoUri,
479
+ dbName: mongoDb,
480
+ projectKey,
481
+ direction,
482
+ batchSize,
483
+ intervalMs
484
+ });
485
+
486
+ const runOnce = async () => {
487
+ const { pushed, pulled } = await worker.syncNow();
488
+ const ts = new Date().toISOString();
489
+ process.stdout.write(`[mongo-sync] ${ts} project=${projectKey} pushed=${pushed} pulled=${pulled}\n`);
490
+ };
491
+
492
+ try {
493
+ if (!options.watch) {
494
+ await runOnce();
495
+ await worker.shutdown();
496
+ sqliteStore.close();
497
+ return;
498
+ }
499
+
500
+ console.log(`[mongo-sync] Watch mode started (interval=${intervalMs}ms, project=${projectKey})`);
501
+
502
+ const handle = setInterval(() => {
503
+ runOnce().catch((err) => {
504
+ console.error('[mongo-sync] Sync failed:', err);
505
+ });
506
+ }, intervalMs);
507
+
508
+ const shutdown = async () => {
509
+ clearInterval(handle);
510
+ console.log('\n[mongo-sync] Shutting down...');
511
+ try {
512
+ await worker.shutdown();
513
+ } finally {
514
+ sqliteStore.close();
515
+ }
516
+ process.exit(0);
517
+ };
518
+
519
+ process.on('SIGINT', () => { void shutdown(); });
520
+ process.on('SIGTERM', () => { void shutdown(); });
521
+
522
+ // Run immediately, then keep alive
523
+ await runOnce();
524
+ await new Promise(() => {});
525
+ } catch (error) {
526
+ console.error('[mongo-sync] Failed:', error);
527
+ process.exit(1);
528
+ }
529
+ });
530
+
531
+ /**
532
+ * Render import progress to terminal
533
+ */
534
+ function renderProgress(event: ProgressEvent): void {
535
+ switch (event.phase) {
536
+ case 'scan':
537
+ console.log(` 🔍 ${event.message}`);
538
+ break;
539
+ case 'session-start': {
540
+ const pct = Math.round(((event.sessionIndex) / event.totalSessions) * 100);
541
+ const sessionName = path.basename(event.filePath, '.jsonl').slice(0, 8);
542
+ process.stdout.write(
543
+ `\r 📄 [${event.sessionIndex + 1}/${event.totalSessions}] ${pct}% | Session ${sessionName}... `
544
+ );
545
+ break;
546
+ }
547
+ case 'session-progress': {
548
+ process.stdout.write(
549
+ `\r 📄 [${event.sessionIndex + 1}/...] ${event.messagesProcessed} msgs | +${event.imported} imported, ~${event.skipped} skipped `
550
+ );
551
+ break;
552
+ }
553
+ case 'session-done': {
554
+ const imported = event.importedPrompts + event.importedResponses;
555
+ if (imported > 0) {
556
+ process.stdout.write(
557
+ `\r ✅ [${event.sessionIndex + 1}] +${event.importedPrompts} prompts, +${event.importedResponses} responses${event.skipped > 0 ? `, ~${event.skipped} skipped` : ''} \n`
558
+ );
559
+ } else if (event.skipped > 0) {
560
+ process.stdout.write(
561
+ `\r ⏭️ [${event.sessionIndex + 1}] All ${event.skipped} already imported \n`
562
+ );
563
+ } else {
564
+ process.stdout.write(
565
+ `\r ⏭️ [${event.sessionIndex + 1}] Empty session \n`
566
+ );
567
+ }
568
+ break;
569
+ }
570
+ case 'embedding':
571
+ process.stdout.write(
572
+ `\r 🧠 Embeddings: ${event.processed}/${event.total} processed `
573
+ );
574
+ if (event.processed >= event.total) {
575
+ process.stdout.write('\n');
576
+ }
577
+ break;
578
+ case 'done':
579
+ break;
580
+ }
581
+ }
582
+
583
+ function printImportSummary(result: import('../services/session-history-importer.js').ImportResult, embedCount: number): void {
584
+ console.log('\n┌─────────────────────────────────┐');
585
+ console.log('│ ✅ Import Complete │');
586
+ console.log('├─────────────────────────────────┤');
587
+ console.log(`│ Sessions processed: ${String(result.totalSessions).padStart(8)} │`);
588
+ console.log(`│ Total messages: ${String(result.totalMessages).padStart(8)} │`);
589
+ console.log(`│ Imported prompts: ${String(result.importedPrompts).padStart(8)} │`);
590
+ console.log(`│ Imported responses: ${String(result.importedResponses).padStart(8)} │`);
591
+ console.log(`│ Skipped duplicates: ${String(result.skippedDuplicates).padStart(8)} │`);
592
+ console.log(`│ Embeddings queued: ${String(embedCount).padStart(8)} │`);
593
+ console.log('└─────────────────────────────────┘');
594
+
595
+ if (result.errors.length > 0) {
596
+ console.log(`\n⚠️ Errors (${result.errors.length}):`);
597
+ for (const error of result.errors.slice(0, 5)) {
598
+ console.log(` - ${error}`);
599
+ }
600
+ if (result.errors.length > 5) {
601
+ console.log(` ... and ${result.errors.length - 5} more`);
602
+ }
603
+ }
604
+ }
605
+
606
+ function sanitizeSegment(input: string | undefined, fallback: string): string {
607
+ const v = (input || '').trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
608
+ return v || fallback;
609
+ }
610
+
611
+ async function listMarkdownFiles(root: string): Promise<string[]> {
612
+ const out: string[] = [];
613
+ const stack = [root];
614
+
615
+ while (stack.length > 0) {
616
+ const dir = stack.pop()!;
617
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
618
+ for (const e of entries) {
619
+ const full = path.join(dir, e.name);
620
+ if (e.isDirectory()) stack.push(full);
621
+ else if (e.isFile() && e.name.endsWith('.md') && e.name !== '_index.md') out.push(full);
622
+ }
623
+ }
624
+
625
+ return out.sort();
626
+ }
627
+
628
+ function deriveNamespaceCategory(sourceRoot: string, filePath: string): { namespace: string; categoryPath: string[] } {
629
+ const rel = path.relative(sourceRoot, filePath);
630
+ const dirSeg = path.dirname(rel).split(path.sep).filter(Boolean);
631
+
632
+ if (dirSeg.length >= 2) {
633
+ const namespace = sanitizeSegment(dirSeg[0], 'default');
634
+ const categoryPath = dirSeg.slice(1).map((s) => sanitizeSegment(s, 'uncategorized'));
635
+ return { namespace, categoryPath: categoryPath.length > 0 ? categoryPath : ['uncategorized'] };
636
+ }
637
+
638
+ return { namespace: 'default', categoryPath: ['uncategorized'] };
639
+ }
640
+
641
+ function extractImportEvidence(markdown: string): { confidence?: string; sources: string[] } {
642
+ const confidenceMatch = markdown.match(/^-\s*confidence:\s*([^\n]+)/m);
643
+ const sources = markdown
644
+ .split(/\r?\n/)
645
+ .map((line) => line.trim())
646
+ .filter((line) => line.startsWith('- source:'))
647
+ .map((line) => line.replace(/^-\s*source:\s*/i, '').trim())
648
+ .filter(Boolean)
649
+ .slice(0, 30);
650
+
651
+ return {
652
+ confidence: confidenceMatch ? confidenceMatch[1].trim() : undefined,
653
+ sources
654
+ };
655
+ }
656
+
657
+ /**
658
+ * Organize-import command - import legacy markdown memories into structured mirror
659
+ */
660
+ program
661
+ .command('organize-import [sourceDir]')
662
+ .description('Import existing markdown memory files, or bootstrap knowledge docs from codebase/git when markdown is missing')
663
+ .option('-p, --project <path>', 'Project path (defaults to cwd)')
664
+ .option('--session <id>', 'Session id for imported events (default: import:organized)')
665
+ .option('--limit <n>', 'Limit number of files to import')
666
+ .option('--dry-run', 'Preview mapping without writing')
667
+ .option('--bootstrap', 'Force-generate structured markdown from codebase + git history before import')
668
+ .option('--bootstrap-if-empty', 'Auto-bootstrap when source has no markdown files (default: true)', true)
669
+ .option('--no-bootstrap-if-empty', 'Disable auto-bootstrap when source has no markdown files')
670
+ .option('--force-bootstrap', 'Run bootstrap even when markdown files exist')
671
+ .option('--repo <path>', 'Repository root for bootstrap analysis (default: project path)')
672
+ .option('--out <path>', 'Output directory for generated bootstrap markdown (default: <sourceDir>/bootstrap-kb)')
673
+ .option('--since <range>', 'Git history range for bootstrap (default: "180 days ago")')
674
+ .option('--max-commits <n>', 'Max commits to analyze for bootstrap (default: 1000)')
675
+ .option('--incremental', 'Use previous bootstrap manifest as baseline for incremental updates (default: true)', true)
676
+ .option('--no-incremental', 'Disable incremental bootstrap; regenerate full snapshot')
677
+ .action(async (sourceDir: string | undefined, options) => {
678
+ const projectPath = options.project || process.cwd();
679
+ const sessionId = options.session || 'import:organized';
680
+ const sourceRoot = path.resolve(sourceDir || options.out || projectPath);
681
+ const repoPath = path.resolve(options.repo || projectPath);
682
+
683
+ if (!fs.existsSync(sourceRoot)) {
684
+ fs.mkdirSync(sourceRoot, { recursive: true });
685
+ }
686
+
687
+ const service = getMemoryServiceForProject(projectPath);
688
+
689
+ try {
690
+ let activeSourceRoot = sourceRoot;
691
+ let importRoot = sourceRoot;
692
+ let files = await listMarkdownFiles(importRoot);
693
+ const hasMarkdown = files.length > 0;
694
+ const shouldBootstrap = Boolean(options.forceBootstrap || options.bootstrap || (!hasMarkdown && options.bootstrapIfEmpty));
695
+
696
+ if (shouldBootstrap) {
697
+ const outDir = path.resolve(options.out || path.join(sourceRoot, 'bootstrap-kb'));
698
+ const since = options.since || '180 days ago';
699
+ const maxCommits = options.maxCommits ? Math.max(1, parseInt(options.maxCommits, 10)) : 1000;
700
+
701
+ console.log('\n🧠 Bootstrapping markdown knowledge base...');
702
+ const bootstrap = await bootstrapKnowledgeBase({
703
+ repoPath,
704
+ outDir,
705
+ since,
706
+ maxCommits,
707
+ incremental: options.incremental
708
+ });
709
+ console.log(` Repo: ${repoPath}`);
710
+ console.log(` Output: ${bootstrap.outDir}`);
711
+ console.log(` Files analyzed: ${bootstrap.fileCount}`);
712
+ console.log(` Commits analyzed: ${bootstrap.commitCount}`);
713
+ console.log(` Modules: ${bootstrap.moduleCount}`);
714
+
715
+ activeSourceRoot = outDir;
716
+ importRoot = outDir;
717
+ files = await listMarkdownFiles(importRoot);
718
+ }
719
+
720
+ if (files.length === 0) {
721
+ console.error('\n❌ organize-import found no markdown files to import.\n');
722
+ process.exit(1);
723
+ }
724
+
725
+ const limit = options.limit ? Math.max(1, parseInt(options.limit, 10)) : files.length;
726
+ const targets = files.slice(0, limit);
727
+
728
+ console.log(`\n📦 organize-import`);
729
+ console.log(` Source: ${activeSourceRoot}`);
730
+ console.log(` Project: ${projectPath}`);
731
+ console.log(` Files: ${targets.length}${targets.length < files.length ? `/${files.length}` : ''}`);
732
+ console.log(` Dry-run: ${options.dryRun ? 'yes' : 'no'}\n`);
733
+
734
+ if (!options.dryRun) {
735
+ await service.initialize();
736
+ }
737
+
738
+ let imported = 0;
739
+ let skipped = 0;
740
+
741
+ for (const file of targets) {
742
+ const text = await fs.promises.readFile(file, 'utf8');
743
+ if (!text.trim()) {
744
+ skipped += 1;
745
+ continue;
746
+ }
747
+
748
+ const { namespace, categoryPath } = deriveNamespaceCategory(activeSourceRoot, file);
749
+ const rel = path.relative(activeSourceRoot, file);
750
+ const evidence = extractImportEvidence(text);
751
+
752
+ if (options.dryRun) {
753
+ console.log(`- ${rel} -> namespace=${namespace} category=${categoryPath.join('/')} confidence=${evidence.confidence || 'n/a'} sources=${evidence.sources.length}`);
754
+ continue;
755
+ }
756
+
757
+ await service.storeSessionSummary(sessionId, text, {
758
+ namespace,
759
+ categoryPath,
760
+ confidence: evidence.confidence,
761
+ sources: evidence.sources,
762
+ import: {
763
+ sourceFile: rel,
764
+ importedAt: new Date().toISOString(),
765
+ bootstrap: shouldBootstrap === true
766
+ }
767
+ });
768
+ imported += 1;
769
+ }
770
+
771
+ if (!options.dryRun) {
772
+ const embed = await service.processPendingEmbeddings();
773
+ await service.shutdown();
774
+ console.log(`\n✅ Imported: ${imported}, skipped-empty: ${skipped}, embeddings: ${embed}\n`);
775
+ } else {
776
+ console.log(`\n✅ Dry-run complete (planned imports: ${targets.length - skipped}, skipped-empty: ${skipped})\n`);
777
+ }
778
+ } catch (error) {
779
+ console.error('\n❌ organize-import failed:', error);
780
+ process.exit(1);
781
+ }
782
+ });
783
+
430
784
  /**
431
785
  * Import command - import existing Claude Code sessions
432
786
  */
@@ -437,8 +791,11 @@ program
437
791
  .option('-s, --session <file>', 'Import specific session file (JSONL)')
438
792
  .option('-a, --all', 'Import all sessions from all projects')
439
793
  .option('-l, --limit <number>', 'Limit messages per session')
794
+ .option('-f, --force', 'Force reimport: delete existing events and reimport with turn_id grouping')
440
795
  .option('-v, --verbose', 'Show detailed progress')
441
796
  .action(async (options) => {
797
+ const startTime = Date.now();
798
+
442
799
  // Determine target project path for storage
443
800
  const targetProjectPath = options.project || process.cwd();
444
801
 
@@ -446,102 +803,76 @@ program
446
803
  const service = getMemoryServiceForProject(targetProjectPath);
447
804
  const importer = createSessionHistoryImporter(service);
448
805
 
806
+ const importOpts = {
807
+ limit: options.limit ? parseInt(options.limit) : undefined,
808
+ force: options.force,
809
+ verbose: options.verbose,
810
+ onProgress: renderProgress
811
+ };
812
+
449
813
  try {
814
+ console.log('\n⏳ Initializing memory service...');
450
815
  await service.initialize();
816
+ console.log(' ✅ Ready\n');
817
+
818
+ if (options.force) {
819
+ console.log('🔄 Force mode: existing events will be deleted and reimported with turn_id grouping\n');
820
+ }
451
821
 
452
822
  let result;
453
823
 
454
824
  if (options.session) {
455
825
  // Import specific session file
456
- console.log(`\n📥 Importing session: ${options.session}`);
457
- console.log(` Target project: ${targetProjectPath}\n`);
826
+ console.log(`📥 Importing session: ${options.session}`);
827
+ console.log(` Target: ${targetProjectPath}\n`);
458
828
  result = await importer.importSessionFile(options.session, {
829
+ ...importOpts,
459
830
  projectPath: targetProjectPath,
460
- limit: options.limit ? parseInt(options.limit) : undefined,
461
- verbose: options.verbose
462
831
  });
463
832
  } else if (options.project) {
464
833
  // Import all sessions from a project
465
- console.log(`\n📥 Importing project: ${options.project}\n`);
466
- result = await importer.importProject(options.project, {
467
- limit: options.limit ? parseInt(options.limit) : undefined,
468
- verbose: options.verbose
469
- });
834
+ console.log(`📥 Importing project: ${options.project}\n`);
835
+ result = await importer.importProject(options.project, importOpts);
470
836
  } else if (options.all) {
471
837
  // Import all sessions from all projects
472
- // Note: --all imports to global storage for backward compatibility
473
- console.log('\n📥 Importing all sessions from all projects');
838
+ console.log('📥 Importing all sessions from all projects');
474
839
  console.log(' ⚠️ Using global storage (use -p for project-specific)\n');
475
840
  const globalService = getDefaultMemoryService();
476
841
  const globalImporter = createSessionHistoryImporter(globalService);
477
842
  await globalService.initialize();
478
- result = await globalImporter.importAll({
479
- limit: options.limit ? parseInt(options.limit) : undefined,
480
- verbose: options.verbose
481
- });
843
+ result = await globalImporter.importAll(importOpts);
482
844
 
483
845
  // Process embeddings
484
- console.log('\n Processing embeddings...');
846
+ console.log('\n🧠 Processing embeddings...');
485
847
  const embedCount = await globalService.processPendingEmbeddings();
486
848
 
487
- // Show results
488
- console.log('\n✅ Import Complete\n');
489
- console.log(`Sessions processed: ${result.totalSessions}`);
490
- console.log(`Total messages: ${result.totalMessages}`);
491
- console.log(`Imported prompts: ${result.importedPrompts}`);
492
- console.log(`Imported responses: ${result.importedResponses}`);
493
- console.log(`Skipped duplicates: ${result.skippedDuplicates}`);
494
- console.log(`Embeddings processed: ${embedCount}`);
495
-
496
- if (result.errors.length > 0) {
497
- console.log(`\n⚠️ Errors (${result.errors.length}):`);
498
- for (const error of result.errors.slice(0, 5)) {
499
- console.log(` - ${error}`);
500
- }
501
- if (result.errors.length > 5) {
502
- console.log(` ... and ${result.errors.length - 5} more`);
503
- }
504
- }
849
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
850
+ printImportSummary(result, embedCount);
851
+ console.log(`\n⏱️ Completed in ${elapsed}s`);
505
852
 
506
853
  await globalService.shutdown();
507
854
  return;
508
855
  } else {
509
856
  // Default: import current project
510
857
  const cwd = process.cwd();
511
- console.log(`\n📥 Importing sessions for current project: ${cwd}\n`);
858
+ console.log(`📥 Importing sessions for: ${cwd}\n`);
512
859
  result = await importer.importProject(cwd, {
860
+ ...importOpts,
513
861
  projectPath: cwd,
514
- limit: options.limit ? parseInt(options.limit) : undefined,
515
- verbose: options.verbose
516
862
  });
517
863
  }
518
864
 
519
865
  // Process embeddings
520
- console.log('\n Processing embeddings...');
866
+ console.log('\n🧠 Processing embeddings...');
521
867
  const embedCount = await service.processPendingEmbeddings();
522
868
 
523
- // Show results
524
- console.log('\n✅ Import Complete\n');
525
- console.log(`Sessions processed: ${result.totalSessions}`);
526
- console.log(`Total messages: ${result.totalMessages}`);
527
- console.log(`Imported prompts: ${result.importedPrompts}`);
528
- console.log(`Imported responses: ${result.importedResponses}`);
529
- console.log(`Skipped duplicates: ${result.skippedDuplicates}`);
530
- console.log(`Embeddings processed: ${embedCount}`);
531
-
532
- if (result.errors.length > 0) {
533
- console.log(`\n⚠️ Errors (${result.errors.length}):`);
534
- for (const error of result.errors.slice(0, 5)) {
535
- console.log(` - ${error}`);
536
- }
537
- if (result.errors.length > 5) {
538
- console.log(` ... and ${result.errors.length - 5} more`);
539
- }
540
- }
869
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
870
+ printImportSummary(result, embedCount);
871
+ console.log(`\n⏱️ Completed in ${elapsed}s`);
541
872
 
542
873
  await service.shutdown();
543
874
  } catch (error) {
544
- console.error('Import failed:', error);
875
+ console.error('\n❌ Import failed:', error);
545
876
  process.exit(1);
546
877
  }
547
878
  });
@@ -8,7 +8,9 @@ import { randomUUID } from 'crypto';
8
8
  import { dbRun, dbAll, toDate, type Database } from './db-wrapper.js';
9
9
  import type {
10
10
  ConsolidatedMemory,
11
- ConsolidatedMemoryInput
11
+ ConsolidatedMemoryInput,
12
+ ConsolidationRule,
13
+ ConsolidationRuleInput
12
14
  } from './types.js';
13
15
  import { EventStore } from './event-store.js';
14
16
 
@@ -170,6 +172,66 @@ export class ConsolidatedStore {
170
172
  );
171
173
  }
172
174
 
175
+ /**
176
+ * Create a long-term rule promoted from stable summaries
177
+ */
178
+ async createRule(input: ConsolidationRuleInput): Promise<string> {
179
+ const ruleId = randomUUID();
180
+
181
+ await dbRun(
182
+ this.db,
183
+ `INSERT INTO consolidated_rules
184
+ (rule_id, rule, topics, source_memory_ids, source_events, confidence, created_at)
185
+ VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
186
+ [
187
+ ruleId,
188
+ input.rule,
189
+ JSON.stringify(input.topics),
190
+ JSON.stringify(input.sourceMemoryIds),
191
+ JSON.stringify(input.sourceEvents),
192
+ input.confidence
193
+ ]
194
+ );
195
+
196
+ return ruleId;
197
+ }
198
+
199
+ async getRules(options?: { limit?: number }): Promise<ConsolidationRule[]> {
200
+ const limit = options?.limit || 100;
201
+ const rows = await dbAll<Record<string, unknown>>(
202
+ this.db,
203
+ `SELECT * FROM consolidated_rules ORDER BY confidence DESC, created_at DESC LIMIT ?`,
204
+ [limit]
205
+ );
206
+
207
+ return rows.map((row) => ({
208
+ ruleId: row.rule_id as string,
209
+ rule: row.rule as string,
210
+ topics: JSON.parse((row.topics as string) || '[]'),
211
+ sourceMemoryIds: JSON.parse((row.source_memory_ids as string) || '[]'),
212
+ sourceEvents: JSON.parse((row.source_events as string) || '[]'),
213
+ confidence: Number(row.confidence ?? 0.5),
214
+ createdAt: toDate(row.created_at) || new Date()
215
+ }));
216
+ }
217
+
218
+ async countRules(): Promise<number> {
219
+ const result = await dbAll<{ count: number }>(
220
+ this.db,
221
+ `SELECT COUNT(*) as count FROM consolidated_rules`
222
+ );
223
+ return result[0]?.count || 0;
224
+ }
225
+
226
+ async hasRuleForSourceMemory(memoryId: string): Promise<boolean> {
227
+ const rows = await dbAll<{ count: number }>(
228
+ this.db,
229
+ `SELECT COUNT(*) as count FROM consolidated_rules WHERE source_memory_ids LIKE ?`,
230
+ [`%"${memoryId}"%`]
231
+ );
232
+ return (rows[0]?.count || 0) > 0;
233
+ }
234
+
173
235
  /**
174
236
  * Get count of consolidated memories
175
237
  */