superlocalmemory 2.3.6 → 2.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.
@@ -53,21 +53,41 @@ class MemoryStoreV2:
53
53
  - Backward compatible with V1 API
54
54
  """
55
55
 
56
- def __init__(self, db_path: Optional[Path] = None):
56
+ def __init__(self, db_path: Optional[Path] = None, profile: Optional[str] = None):
57
57
  """
58
58
  Initialize MemoryStore V2.
59
59
 
60
60
  Args:
61
61
  db_path: Optional custom database path (defaults to ~/.claude-memory/memory.db)
62
+ profile: Optional profile override. If None, reads from profiles.json config.
62
63
  """
63
64
  self.db_path = db_path or DB_PATH
64
65
  self.vectors_path = VECTORS_PATH
66
+ self._profile_override = profile
65
67
  self._init_db()
66
68
  self.vectorizer = None
67
69
  self.vectors = None
68
70
  self.memory_ids = []
69
71
  self._load_vectors()
70
72
 
73
+ def _get_active_profile(self) -> str:
74
+ """
75
+ Get the currently active profile name.
76
+ Reads from profiles.json config file. Falls back to 'default'.
77
+ """
78
+ if self._profile_override:
79
+ return self._profile_override
80
+
81
+ config_file = MEMORY_DIR / "profiles.json"
82
+ if config_file.exists():
83
+ try:
84
+ with open(config_file, 'r') as f:
85
+ config = json.load(f)
86
+ return config.get('active_profile', 'default')
87
+ except (json.JSONDecodeError, IOError):
88
+ pass
89
+ return 'default'
90
+
71
91
  def _init_db(self):
72
92
  """Initialize SQLite database with V2 schema extensions."""
73
93
  conn = sqlite3.connect(self.db_path)
@@ -129,7 +149,8 @@ class MemoryStoreV2:
129
149
  'last_accessed': 'TIMESTAMP',
130
150
  'access_count': 'INTEGER DEFAULT 0',
131
151
  'content_hash': 'TEXT',
132
- 'cluster_id': 'INTEGER'
152
+ 'cluster_id': 'INTEGER',
153
+ 'profile': 'TEXT DEFAULT "default"'
133
154
  }
134
155
 
135
156
  for col_name, col_type in v2_columns.items():
@@ -190,7 +211,8 @@ class MemoryStoreV2:
190
211
  ('idx_tree_path', 'tree_path'),
191
212
  ('idx_cluster', 'cluster_id'),
192
213
  ('idx_last_accessed', 'last_accessed'),
193
- ('idx_parent_id', 'parent_id')
214
+ ('idx_parent_id', 'parent_id'),
215
+ ('idx_profile', 'profile')
194
216
  ]
195
217
 
196
218
  for idx_name, col_name in v2_indexes:
@@ -303,6 +325,7 @@ class MemoryStoreV2:
303
325
  importance = max(1, min(10, importance)) # Clamp to valid range
304
326
 
305
327
  content_hash = self._content_hash(content)
328
+ active_profile = self._get_active_profile()
306
329
 
307
330
  conn = sqlite3.connect(self.db_path)
308
331
  cursor = conn.cursor()
@@ -316,9 +339,9 @@ class MemoryStoreV2:
316
339
  content, summary, project_path, project_name, tags, category,
317
340
  parent_id, tree_path, depth,
318
341
  memory_type, importance, content_hash,
319
- last_accessed, access_count
342
+ last_accessed, access_count, profile
320
343
  )
321
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
344
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
322
345
  ''', (
323
346
  content,
324
347
  summary,
@@ -333,7 +356,8 @@ class MemoryStoreV2:
333
356
  importance,
334
357
  content_hash,
335
358
  datetime.now().isoformat(),
336
- 0
359
+ 0,
360
+ active_profile
337
361
  ))
338
362
  memory_id = cursor.lastrowid
339
363
 
@@ -350,6 +374,14 @@ class MemoryStoreV2:
350
374
  # Rebuild vectors after adding
351
375
  self._rebuild_vectors()
352
376
 
377
+ # Auto-backup check (non-blocking)
378
+ try:
379
+ from auto_backup import AutoBackup
380
+ backup = AutoBackup()
381
+ backup.check_and_backup()
382
+ except Exception:
383
+ pass # Backup failure must never break memory operations
384
+
353
385
  return memory_id
354
386
 
355
387
  except sqlite3.IntegrityError:
@@ -410,6 +442,7 @@ class MemoryStoreV2:
410
442
  List of memory dictionaries with scores
411
443
  """
412
444
  results = []
445
+ active_profile = self._get_active_profile()
413
446
 
414
447
  # Method 1: TF-IDF semantic search
415
448
  if SKLEARN_AVAILABLE and self.vectorizer is not None and self.vectors is not None:
@@ -432,8 +465,8 @@ class MemoryStoreV2:
432
465
  category, parent_id, tree_path, depth,
433
466
  memory_type, importance, created_at, cluster_id,
434
467
  last_accessed, access_count
435
- FROM memories WHERE id = ?
436
- ''', (memory_id,))
468
+ FROM memories WHERE id = ? AND profile = ?
469
+ ''', (memory_id, active_profile))
437
470
  row = cursor.fetchone()
438
471
 
439
472
  if row and self._apply_filters(row, project_path, memory_type,
@@ -460,10 +493,10 @@ class MemoryStoreV2:
460
493
  m.last_accessed, m.access_count
461
494
  FROM memories m
462
495
  JOIN memories_fts fts ON m.id = fts.rowid
463
- WHERE memories_fts MATCH ?
496
+ WHERE memories_fts MATCH ? AND m.profile = ?
464
497
  ORDER BY rank
465
498
  LIMIT ?
466
- ''', (fts_query, limit))
499
+ ''', (fts_query, active_profile, limit))
467
500
 
468
501
  existing_ids = {r['id'] for r in results}
469
502
 
@@ -570,6 +603,7 @@ class MemoryStoreV2:
570
603
  Returns:
571
604
  List of memories with tree structure
572
605
  """
606
+ active_profile = self._get_active_profile()
573
607
  conn = sqlite3.connect(self.db_path)
574
608
  cursor = conn.cursor()
575
609
 
@@ -580,9 +614,9 @@ class MemoryStoreV2:
580
614
  category, parent_id, tree_path, depth, memory_type, importance,
581
615
  created_at, cluster_id, last_accessed, access_count
582
616
  FROM memories
583
- WHERE parent_id IS NULL AND depth <= ?
617
+ WHERE parent_id IS NULL AND depth <= ? AND profile = ?
584
618
  ORDER BY tree_path
585
- ''', (max_depth,))
619
+ ''', (max_depth, active_profile))
586
620
  else:
587
621
  # Get subtree under specific parent
588
622
  cursor.execute('''
@@ -649,6 +683,7 @@ class MemoryStoreV2:
649
683
  Returns:
650
684
  List of memories in the cluster
651
685
  """
686
+ active_profile = self._get_active_profile()
652
687
  conn = sqlite3.connect(self.db_path)
653
688
  cursor = conn.cursor()
654
689
 
@@ -657,9 +692,9 @@ class MemoryStoreV2:
657
692
  category, parent_id, tree_path, depth, memory_type, importance,
658
693
  created_at, cluster_id, last_accessed, access_count
659
694
  FROM memories
660
- WHERE cluster_id = ?
695
+ WHERE cluster_id = ? AND profile = ?
661
696
  ORDER BY importance DESC, created_at DESC
662
- ''', (cluster_id,))
697
+ ''', (cluster_id, active_profile))
663
698
 
664
699
  results = []
665
700
  for row in cursor.fetchall():
@@ -675,10 +710,11 @@ class MemoryStoreV2:
675
710
  self._rebuild_vectors()
676
711
 
677
712
  def _rebuild_vectors(self):
678
- """Rebuild TF-IDF vectors from all memories (V1 compatible, backward compatible)."""
713
+ """Rebuild TF-IDF vectors from active profile memories (V1 compatible, backward compatible)."""
679
714
  if not SKLEARN_AVAILABLE:
680
715
  return
681
716
 
717
+ active_profile = self._get_active_profile()
682
718
  conn = sqlite3.connect(self.db_path)
683
719
  cursor = conn.cursor()
684
720
 
@@ -686,9 +722,13 @@ class MemoryStoreV2:
686
722
  cursor.execute("PRAGMA table_info(memories)")
687
723
  columns = {row[1] for row in cursor.fetchall()}
688
724
 
689
- # Build SELECT query based on available columns
725
+ # Build SELECT query based on available columns, filtered by profile
726
+ has_profile = 'profile' in columns
690
727
  if 'summary' in columns:
691
- cursor.execute('SELECT id, content, summary FROM memories')
728
+ if has_profile:
729
+ cursor.execute('SELECT id, content, summary FROM memories WHERE profile = ?', (active_profile,))
730
+ else:
731
+ cursor.execute('SELECT id, content, summary FROM memories')
692
732
  rows = cursor.fetchall()
693
733
  texts = [f"{row[1]} {row[2] or ''}" for row in rows]
694
734
  else:
@@ -720,7 +760,8 @@ class MemoryStoreV2:
720
760
  json.dump(self.memory_ids, f)
721
761
 
722
762
  def get_recent(self, limit: int = 10, project_path: Optional[str] = None) -> List[Dict[str, Any]]:
723
- """Get most recent memories (V1 compatible)."""
763
+ """Get most recent memories (V1 compatible, profile-aware)."""
764
+ active_profile = self._get_active_profile()
724
765
  conn = sqlite3.connect(self.db_path)
725
766
  cursor = conn.cursor()
726
767
 
@@ -730,19 +771,20 @@ class MemoryStoreV2:
730
771
  category, parent_id, tree_path, depth, memory_type, importance,
731
772
  created_at, cluster_id, last_accessed, access_count
732
773
  FROM memories
733
- WHERE project_path = ?
774
+ WHERE project_path = ? AND profile = ?
734
775
  ORDER BY created_at DESC
735
776
  LIMIT ?
736
- ''', (project_path, limit))
777
+ ''', (project_path, active_profile, limit))
737
778
  else:
738
779
  cursor.execute('''
739
780
  SELECT id, content, summary, project_path, project_name, tags,
740
781
  category, parent_id, tree_path, depth, memory_type, importance,
741
782
  created_at, cluster_id, last_accessed, access_count
742
783
  FROM memories
784
+ WHERE profile = ?
743
785
  ORDER BY created_at DESC
744
786
  LIMIT ?
745
- ''', (limit,))
787
+ ''', (active_profile, limit))
746
788
 
747
789
  results = []
748
790
  for row in cursor.fetchall():
@@ -789,7 +831,8 @@ class MemoryStoreV2:
789
831
  return deleted
790
832
 
791
833
  def list_all(self, limit: int = 50) -> List[Dict[str, Any]]:
792
- """List all memories with short previews (V1 compatible)."""
834
+ """List all memories with short previews (V1 compatible, profile-aware)."""
835
+ active_profile = self._get_active_profile()
793
836
  conn = sqlite3.connect(self.db_path)
794
837
  cursor = conn.cursor()
795
838
 
@@ -798,9 +841,10 @@ class MemoryStoreV2:
798
841
  category, parent_id, tree_path, depth, memory_type, importance,
799
842
  created_at, cluster_id, last_accessed, access_count
800
843
  FROM memories
844
+ WHERE profile = ?
801
845
  ORDER BY created_at DESC
802
846
  LIMIT ?
803
- ''', (limit,))
847
+ ''', (active_profile, limit))
804
848
 
805
849
  results = []
806
850
  for row in cursor.fetchall():
@@ -817,35 +861,42 @@ class MemoryStoreV2:
817
861
  return results
818
862
 
819
863
  def get_stats(self) -> Dict[str, Any]:
820
- """Get memory store statistics (V1 compatible with V2 extensions)."""
864
+ """Get memory store statistics (V1 compatible with V2 extensions, profile-aware)."""
865
+ active_profile = self._get_active_profile()
821
866
  conn = sqlite3.connect(self.db_path)
822
867
  cursor = conn.cursor()
823
868
 
824
- cursor.execute('SELECT COUNT(*) FROM memories')
869
+ cursor.execute('SELECT COUNT(*) FROM memories WHERE profile = ?', (active_profile,))
825
870
  total_memories = cursor.fetchone()[0]
826
871
 
827
- cursor.execute('SELECT COUNT(DISTINCT project_path) FROM memories WHERE project_path IS NOT NULL')
872
+ cursor.execute('SELECT COUNT(DISTINCT project_path) FROM memories WHERE project_path IS NOT NULL AND profile = ?', (active_profile,))
828
873
  total_projects = cursor.fetchone()[0]
829
874
 
830
- cursor.execute('SELECT memory_type, COUNT(*) FROM memories GROUP BY memory_type')
875
+ cursor.execute('SELECT memory_type, COUNT(*) FROM memories WHERE profile = ? GROUP BY memory_type', (active_profile,))
831
876
  by_type = dict(cursor.fetchall())
832
877
 
833
- cursor.execute('SELECT category, COUNT(*) FROM memories WHERE category IS NOT NULL GROUP BY category')
878
+ cursor.execute('SELECT category, COUNT(*) FROM memories WHERE category IS NOT NULL AND profile = ? GROUP BY category', (active_profile,))
834
879
  by_category = dict(cursor.fetchall())
835
880
 
836
- cursor.execute('SELECT MIN(created_at), MAX(created_at) FROM memories')
881
+ cursor.execute('SELECT MIN(created_at), MAX(created_at) FROM memories WHERE profile = ?', (active_profile,))
837
882
  date_range = cursor.fetchone()
838
883
 
839
- cursor.execute('SELECT COUNT(DISTINCT cluster_id) FROM memories WHERE cluster_id IS NOT NULL')
884
+ cursor.execute('SELECT COUNT(DISTINCT cluster_id) FROM memories WHERE cluster_id IS NOT NULL AND profile = ?', (active_profile,))
840
885
  total_clusters = cursor.fetchone()[0]
841
886
 
842
- cursor.execute('SELECT MAX(depth) FROM memories')
887
+ cursor.execute('SELECT MAX(depth) FROM memories WHERE profile = ?', (active_profile,))
843
888
  max_depth = cursor.fetchone()[0] or 0
844
889
 
890
+ # Total across all profiles
891
+ cursor.execute('SELECT COUNT(*) FROM memories')
892
+ total_all_profiles = cursor.fetchone()[0]
893
+
845
894
  conn.close()
846
895
 
847
896
  return {
848
897
  'total_memories': total_memories,
898
+ 'total_all_profiles': total_all_profiles,
899
+ 'active_profile': active_profile,
849
900
  'total_projects': total_projects,
850
901
  'total_clusters': total_clusters,
851
902
  'max_tree_depth': max_depth,
@@ -915,6 +966,25 @@ class MemoryStoreV2:
915
966
  return ''.join(output)
916
967
 
917
968
 
969
+ def format_content(content: str, full: bool = False, threshold: int = 5000, preview_len: int = 2000) -> str:
970
+ """
971
+ Smart content formatting with optional truncation.
972
+
973
+ Args:
974
+ content: Content to format
975
+ full: If True, always show full content
976
+ threshold: Max length before truncation (default 5000)
977
+ preview_len: Preview length when truncating (default 2000)
978
+
979
+ Returns:
980
+ Formatted content string
981
+ """
982
+ if full or len(content) < threshold:
983
+ return content
984
+ else:
985
+ return f"{content[:preview_len]}..."
986
+
987
+
918
988
  # CLI interface (V1 compatible + V2 extensions)
919
989
  if __name__ == "__main__":
920
990
  import sys
@@ -925,16 +995,18 @@ if __name__ == "__main__":
925
995
  print("MemoryStore V2 CLI")
926
996
  print("\nV1 Compatible Commands:")
927
997
  print(" python memory_store_v2.py add <content> [--project <path>] [--tags tag1,tag2]")
928
- print(" python memory_store_v2.py search <query>")
929
- print(" python memory_store_v2.py list [limit]")
998
+ print(" python memory_store_v2.py search <query> [--full]")
999
+ print(" python memory_store_v2.py list [limit] [--full]")
930
1000
  print(" python memory_store_v2.py get <id>")
931
- print(" python memory_store_v2.py recent [limit]")
1001
+ print(" python memory_store_v2.py recent [limit] [--full]")
932
1002
  print(" python memory_store_v2.py stats")
933
1003
  print(" python memory_store_v2.py context <query>")
934
1004
  print(" python memory_store_v2.py delete <id>")
935
1005
  print("\nV2 Extensions:")
936
1006
  print(" python memory_store_v2.py tree [parent_id]")
937
- print(" python memory_store_v2.py cluster <cluster_id>")
1007
+ print(" python memory_store_v2.py cluster <cluster_id> [--full]")
1008
+ print("\nOptions:")
1009
+ print(" --full Show complete content (default: smart truncation at 5000 chars)")
938
1010
  sys.exit(0)
939
1011
 
940
1012
  command = sys.argv[1]
@@ -954,6 +1026,7 @@ if __name__ == "__main__":
954
1026
 
955
1027
  elif command == "cluster" and len(sys.argv) >= 3:
956
1028
  cluster_id = int(sys.argv[2])
1029
+ show_full = '--full' in sys.argv
957
1030
  results = store.get_by_cluster(cluster_id)
958
1031
 
959
1032
  if not results:
@@ -962,7 +1035,7 @@ if __name__ == "__main__":
962
1035
  print(f"Cluster {cluster_id} - {len(results)} memories:")
963
1036
  for r in results:
964
1037
  print(f"\n[{r['id']}] Importance: {r['importance']}")
965
- print(f" {r['content'][:200]}...")
1038
+ print(f" {format_content(r['content'], full=show_full)}")
966
1039
 
967
1040
  elif command == "stats":
968
1041
  stats = store.get_stats()
@@ -996,10 +1069,11 @@ if __name__ == "__main__":
996
1069
  elif command == "search":
997
1070
  if len(sys.argv) < 3:
998
1071
  print("Error: Search query required")
999
- print("Usage: python memory_store_v2.py search <query>")
1072
+ print("Usage: python memory_store_v2.py search <query> [--full]")
1000
1073
  sys.exit(1)
1001
1074
 
1002
1075
  query = sys.argv[2]
1076
+ show_full = '--full' in sys.argv
1003
1077
  results = store.search(query, limit=5)
1004
1078
 
1005
1079
  if not results:
@@ -1011,11 +1085,18 @@ if __name__ == "__main__":
1011
1085
  print(f"Project: {r['project_name']}")
1012
1086
  if r.get('tags'):
1013
1087
  print(f"Tags: {', '.join(r['tags'])}")
1014
- print(f"Content: {r['content'][:200]}...")
1088
+ print(f"Content: {format_content(r['content'], full=show_full)}")
1015
1089
  print(f"Created: {r['created_at']}")
1016
1090
 
1017
1091
  elif command == "recent":
1018
- limit = int(sys.argv[2]) if len(sys.argv) > 2 else 10
1092
+ show_full = '--full' in sys.argv
1093
+ # Parse limit (skip --full flag)
1094
+ limit = 10
1095
+ for i, arg in enumerate(sys.argv[2:], start=2):
1096
+ if arg != '--full' and arg.isdigit():
1097
+ limit = int(arg)
1098
+ break
1099
+
1019
1100
  results = store.get_recent(limit)
1020
1101
 
1021
1102
  if not results:
@@ -1027,17 +1108,24 @@ if __name__ == "__main__":
1027
1108
  print(f"Project: {r['project_name']}")
1028
1109
  if r.get('tags'):
1029
1110
  print(f"Tags: {', '.join(r['tags'])}")
1030
- print(f"Content: {r['content'][:200]}...")
1111
+ print(f"Content: {format_content(r['content'], full=show_full)}")
1031
1112
 
1032
1113
  elif command == "list":
1033
- limit = int(sys.argv[2]) if len(sys.argv) > 2 else 10
1114
+ show_full = '--full' in sys.argv
1115
+ # Parse limit (skip --full flag)
1116
+ limit = 10
1117
+ for i, arg in enumerate(sys.argv[2:], start=2):
1118
+ if arg != '--full' and arg.isdigit():
1119
+ limit = int(arg)
1120
+ break
1121
+
1034
1122
  results = store.get_recent(limit)
1035
1123
 
1036
1124
  if not results:
1037
1125
  print("No memories found.")
1038
1126
  else:
1039
1127
  for r in results:
1040
- print(f"[{r['id']}] {r['content'][:100]}...")
1128
+ print(f"[{r['id']}] {format_content(r['content'], full=show_full)}")
1041
1129
 
1042
1130
  elif command == "get":
1043
1131
  if len(sys.argv) < 3:
@@ -1046,7 +1134,7 @@ if __name__ == "__main__":
1046
1134
  sys.exit(1)
1047
1135
 
1048
1136
  mem_id = int(sys.argv[2])
1049
- memory = store.get_memory(mem_id)
1137
+ memory = store.get_by_id(mem_id)
1050
1138
 
1051
1139
  if not memory:
1052
1140
  print(f"Memory {mem_id} not found.")