superlocalmemory 2.3.7 → 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.
@@ -12,55 +12,219 @@ Attribution must be preserved in all copies or derivatives.
12
12
  """
13
13
 
14
14
  """
15
- SuperLocalMemory V2 - Profile Management System
15
+ SuperLocalMemory V2 - Profile Management System (Column-Based)
16
16
 
17
- Allows users to maintain separate memory databases for different contexts/personalities:
17
+ v2.4.0: Rewritten to use column-based profiles in a SINGLE database.
18
+ All memories live in one memory.db with a 'profile' column.
19
+ Switching profiles = updating config. No file copying. No data loss risk.
20
+
21
+ Previous versions used separate database files per profile, which caused
22
+ data loss when switching. This version is backward compatible and will
23
+ auto-migrate old profile directories on first run.
24
+
25
+ Allows users to maintain separate memory contexts:
18
26
  - Work profile: Professional coding memories
19
27
  - Personal profile: Personal projects and learning
20
28
  - Client-specific profiles: Different clients get isolated memories
21
29
  - Experimentation profile: Testing and experiments
22
-
23
- Each profile is completely isolated with its own:
24
- - Database (memory.db)
25
- - Graph data
26
- - Learned patterns
27
- - Compressed archives
28
30
  """
29
31
 
30
32
  import os
31
33
  import sys
32
34
  import json
33
- import shutil
35
+ import sqlite3
36
+ import hashlib
34
37
  from pathlib import Path
35
38
  from datetime import datetime
36
39
  import argparse
40
+ import re
37
41
 
38
42
  MEMORY_DIR = Path.home() / ".claude-memory"
43
+ DB_PATH = MEMORY_DIR / "memory.db"
39
44
  PROFILES_DIR = MEMORY_DIR / "profiles"
40
- CURRENT_PROFILE_FILE = MEMORY_DIR / ".current_profile"
41
45
  CONFIG_FILE = MEMORY_DIR / "profiles.json"
42
46
 
43
47
 
44
48
  class ProfileManager:
49
+ """
50
+ Column-based profile manager. All memories in ONE database.
51
+ Profile = a value in the 'profile' column of the memories table.
52
+ Switching = updating which profile name is active in config.
53
+ """
54
+
45
55
  def __init__(self):
46
56
  self.memory_dir = MEMORY_DIR
47
- self.profiles_dir = PROFILES_DIR
48
- self.current_profile_file = CURRENT_PROFILE_FILE
57
+ self.db_path = DB_PATH
49
58
  self.config_file = CONFIG_FILE
50
59
 
51
- # Ensure profiles directory exists
52
- self.profiles_dir.mkdir(exist_ok=True)
60
+ # Ensure memory directory exists
61
+ self.memory_dir.mkdir(exist_ok=True)
62
+
63
+ # Ensure profile column exists in DB
64
+ self._ensure_profile_column()
53
65
 
54
66
  # Load or create config
55
67
  self.config = self._load_config()
56
68
 
69
+ # Auto-migrate old profile directories if they exist
70
+ self._migrate_old_profiles()
71
+
72
+ def _ensure_profile_column(self):
73
+ """Add 'profile' column to memories table if it doesn't exist."""
74
+ if not self.db_path.exists():
75
+ return
76
+
77
+ conn = sqlite3.connect(self.db_path)
78
+ cursor = conn.cursor()
79
+
80
+ cursor.execute("PRAGMA table_info(memories)")
81
+ columns = {row[1] for row in cursor.fetchall()}
82
+
83
+ if 'profile' not in columns:
84
+ cursor.execute("ALTER TABLE memories ADD COLUMN profile TEXT DEFAULT 'default'")
85
+ cursor.execute("UPDATE memories SET profile = 'default' WHERE profile IS NULL")
86
+ try:
87
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_profile ON memories(profile)")
88
+ except sqlite3.OperationalError:
89
+ pass
90
+ conn.commit()
91
+
92
+ conn.close()
93
+
94
+ def _migrate_old_profiles(self):
95
+ """
96
+ Backward compatibility: migrate old separate-DB profiles into the main DB.
97
+ Old profiles stored in ~/.claude-memory/profiles/<name>/memory.db
98
+ New profiles use a 'profile' column in the main memory.db
99
+ """
100
+ profiles_dir = MEMORY_DIR / "profiles"
101
+ if not profiles_dir.exists():
102
+ return
103
+
104
+ migrated_any = False
105
+ for profile_dir in profiles_dir.iterdir():
106
+ if not profile_dir.is_dir():
107
+ continue
108
+
109
+ profile_db = profile_dir / "memory.db"
110
+ if not profile_db.exists():
111
+ continue
112
+
113
+ profile_name = profile_dir.name
114
+ marker_file = profile_dir / ".migrated_to_column"
115
+
116
+ # Skip if already migrated
117
+ if marker_file.exists():
118
+ continue
119
+
120
+ # Import memories from old profile DB
121
+ try:
122
+ self._import_from_old_profile(profile_name, profile_db)
123
+ # Mark as migrated
124
+ marker_file.write_text(datetime.now().isoformat())
125
+ migrated_any = True
126
+ except Exception as e:
127
+ print(f" Warning: Could not migrate profile '{profile_name}': {e}", file=sys.stderr)
128
+
129
+ if migrated_any:
130
+ print(" Profile migration complete (old separate DBs -> column-based)", file=sys.stderr)
131
+
132
+ def _import_from_old_profile(self, profile_name, old_db_path):
133
+ """Import memories from an old separate-DB profile into main DB."""
134
+ if not self.db_path.exists():
135
+ return
136
+
137
+ main_conn = sqlite3.connect(self.db_path)
138
+ main_cursor = main_conn.cursor()
139
+
140
+ old_conn = sqlite3.connect(old_db_path)
141
+ old_cursor = old_conn.cursor()
142
+
143
+ # Get existing hashes
144
+ main_cursor.execute("SELECT content_hash FROM memories WHERE content_hash IS NOT NULL")
145
+ existing_hashes = {row[0] for row in main_cursor.fetchall()}
146
+
147
+ # Get columns from old DB
148
+ old_cursor.execute("PRAGMA table_info(memories)")
149
+ old_columns = {row[1] for row in old_cursor.fetchall()}
150
+
151
+ # Build SELECT based on available columns
152
+ select_cols = ['content', 'summary', 'project_path', 'project_name', 'tags',
153
+ 'category', 'memory_type', 'importance', 'created_at', 'updated_at',
154
+ 'content_hash']
155
+ available_cols = [c for c in select_cols if c in old_columns]
156
+
157
+ if 'content' not in available_cols:
158
+ old_conn.close()
159
+ main_conn.close()
160
+ return
161
+
162
+ old_cursor.execute(f"SELECT {', '.join(available_cols)} FROM memories")
163
+ rows = old_cursor.fetchall()
164
+
165
+ imported = 0
166
+ for row in rows:
167
+ row_dict = dict(zip(available_cols, row))
168
+ content = row_dict.get('content', '')
169
+ content_hash = row_dict.get('content_hash')
170
+
171
+ if not content:
172
+ continue
173
+
174
+ # Generate hash if missing
175
+ if not content_hash:
176
+ content_hash = hashlib.sha256(content.encode()).hexdigest()[:32]
177
+
178
+ if content_hash in existing_hashes:
179
+ continue
180
+
181
+ try:
182
+ main_cursor.execute('''
183
+ INSERT INTO memories (content, summary, project_path, project_name, tags,
184
+ category, memory_type, importance, created_at, updated_at,
185
+ content_hash, profile)
186
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
187
+ ''', (
188
+ content,
189
+ row_dict.get('summary'),
190
+ row_dict.get('project_path'),
191
+ row_dict.get('project_name'),
192
+ row_dict.get('tags'),
193
+ row_dict.get('category'),
194
+ row_dict.get('memory_type', 'session'),
195
+ row_dict.get('importance', 5),
196
+ row_dict.get('created_at'),
197
+ row_dict.get('updated_at'),
198
+ content_hash,
199
+ profile_name
200
+ ))
201
+ imported += 1
202
+ existing_hashes.add(content_hash)
203
+ except sqlite3.IntegrityError:
204
+ pass
205
+
206
+ main_conn.commit()
207
+ old_conn.close()
208
+ main_conn.close()
209
+
210
+ if imported > 0:
211
+ # Add profile to config if not present
212
+ config = self._load_config()
213
+ if profile_name not in config.get('profiles', {}):
214
+ config['profiles'][profile_name] = {
215
+ 'name': profile_name,
216
+ 'description': f'Memory profile: {profile_name} (migrated)',
217
+ 'created_at': datetime.now().isoformat(),
218
+ 'last_used': None
219
+ }
220
+ self._save_config(config)
221
+
57
222
  def _load_config(self):
58
223
  """Load profiles configuration."""
59
224
  if self.config_file.exists():
60
225
  with open(self.config_file, 'r') as f:
61
226
  return json.load(f)
62
227
  else:
63
- # Default config
64
228
  config = {
65
229
  'profiles': {
66
230
  'default': {
@@ -83,11 +247,8 @@ class ProfileManager:
83
247
  with open(self.config_file, 'w') as f:
84
248
  json.dump(config, f, indent=2)
85
249
 
86
- def _get_profile_path(self, profile_name):
87
- """Get directory path for a profile with security validation."""
88
- import re
89
-
90
- # SECURITY: Validate profile name to prevent path traversal
250
+ def _validate_profile_name(self, profile_name):
251
+ """Validate profile name for security."""
91
252
  if not profile_name:
92
253
  raise ValueError("Profile name cannot be empty")
93
254
 
@@ -97,327 +258,239 @@ class ProfileManager:
97
258
  if len(profile_name) > 50:
98
259
  raise ValueError("Profile name too long (max 50 characters)")
99
260
 
100
- if profile_name in ['.', '..', 'default']:
261
+ if profile_name in ['.', '..']:
101
262
  raise ValueError(f"Reserved profile name: {profile_name}")
102
263
 
103
- path = (self.profiles_dir / profile_name).resolve()
104
-
105
- # SECURITY: Ensure path stays within profiles directory
106
- if not str(path).startswith(str(self.profiles_dir.resolve())):
107
- raise ValueError("Invalid profile path - path traversal detected")
264
+ def _get_memory_count(self, profile_name):
265
+ """Get memory count for a specific profile."""
266
+ if not self.db_path.exists():
267
+ return 0
108
268
 
109
- return path
269
+ conn = sqlite3.connect(self.db_path)
270
+ cursor = conn.cursor()
271
+ cursor.execute("SELECT COUNT(*) FROM memories WHERE profile = ?", (profile_name,))
272
+ count = cursor.fetchone()[0]
273
+ conn.close()
274
+ return count
110
275
 
111
- def _get_main_files(self):
112
- """Get list of main memory system files to copy."""
113
- return [
114
- 'memory.db',
115
- 'config.json',
116
- 'vectors'
117
- ]
276
+ def get_active_profile(self):
277
+ """Get the currently active profile name."""
278
+ return self.config.get('active_profile', 'default')
118
279
 
119
280
  def list_profiles(self):
120
- """List all available profiles."""
121
- print("\n" + "="*60)
281
+ """List all available profiles with memory counts."""
282
+ print("\n" + "=" * 60)
122
283
  print("AVAILABLE MEMORY PROFILES")
123
- print("="*60)
284
+ print("=" * 60)
124
285
 
125
286
  active = self.config.get('active_profile', 'default')
126
287
 
127
- if not self.config['profiles']:
288
+ if not self.config.get('profiles'):
128
289
  print("\n No profiles found. Create one with: create <name>")
129
290
  return
130
291
 
131
- print(f"\n{'Profile':20s} {'Description':35s} {'Status':10s}")
132
- print("-" * 70)
292
+ print(f"\n{'Profile':20s} {'Description':30s} {'Memories':10s} {'Status':10s}")
293
+ print("-" * 75)
133
294
 
134
295
  for name, info in self.config['profiles'].items():
135
296
  status = "ACTIVE" if name == active else ""
136
- desc = info.get('description', 'No description')[:35]
137
- marker = " " if name == active else " "
138
- print(f"{marker}{name:18s} {desc:35s} {status:10s}")
297
+ desc = info.get('description', 'No description')[:30]
298
+ marker = "-> " if name == active else " "
299
+ count = self._get_memory_count(name)
300
+ print(f"{marker}{name:17s} {desc:30s} {count:<10d} {status:10s}")
139
301
 
140
302
  print(f"\nTotal profiles: {len(self.config['profiles'])}")
141
303
  print(f"Active profile: {active}")
142
304
 
143
305
  def create_profile(self, name, description=None, from_current=False):
144
- """Create a new profile."""
306
+ """
307
+ Create a new profile.
308
+ Column-based: just adds to config. If from_current, copies memories.
309
+ """
145
310
  print(f"\nCreating profile: {name}")
146
311
 
147
- # Check if profile exists
312
+ self._validate_profile_name(name)
313
+
148
314
  if name in self.config['profiles']:
149
- print(f"Error: Profile '{name}' already exists")
315
+ print(f"Error: Profile '{name}' already exists")
150
316
  return False
151
317
 
152
- # Create profile directory
153
- profile_path = self._get_profile_path(name)
154
- profile_path.mkdir(exist_ok=True)
155
-
156
318
  if from_current:
157
- # Copy current memory system to new profile
158
- print(" Copying current memory system...")
159
-
160
- for file in self._get_main_files():
161
- src = self.memory_dir / file
162
- dst = profile_path / file
163
-
164
- if src.exists():
165
- if src.is_dir():
166
- shutil.copytree(src, dst, dirs_exist_ok=True)
167
- else:
168
- shutil.copy2(src, dst)
169
- print(f" ✓ Copied {file}")
319
+ # Copy current profile's memories to new profile
320
+ active = self.config.get('active_profile', 'default')
321
+ if self.db_path.exists():
322
+ conn = sqlite3.connect(self.db_path)
323
+ cursor = conn.cursor()
324
+ cursor.execute("""
325
+ INSERT INTO memories (content, summary, project_path, project_name, tags,
326
+ category, parent_id, tree_path, depth, memory_type,
327
+ importance, created_at, updated_at, last_accessed,
328
+ access_count, content_hash, cluster_id, profile)
329
+ SELECT content, summary, project_path, project_name, tags,
330
+ category, parent_id, tree_path, depth, memory_type,
331
+ importance, created_at, updated_at, last_accessed,
332
+ access_count, NULL, cluster_id, ?
333
+ FROM memories WHERE profile = ?
334
+ """, (name, active))
335
+ copied = cursor.rowcount
336
+ conn.commit()
337
+
338
+ # Generate new content hashes for copied memories
339
+ cursor.execute("SELECT id, content FROM memories WHERE profile = ? AND content_hash IS NULL", (name,))
340
+ for row in cursor.fetchall():
341
+ new_hash = hashlib.sha256(row[1].encode()).hexdigest()[:32]
342
+ try:
343
+ cursor.execute("UPDATE memories SET content_hash = ? WHERE id = ?", (new_hash + f"_{name}", row[0]))
344
+ except sqlite3.IntegrityError:
345
+ pass
346
+ conn.commit()
347
+ conn.close()
348
+
349
+ print(f" Copied {copied} memories from '{active}' profile")
350
+ else:
351
+ print(" No database found, creating empty profile")
170
352
  else:
171
- # Initialize empty profile with V2 schema
172
- print(" Initializing empty profile with V2 schema...")
173
- self._initialize_empty_profile(profile_path)
353
+ print(f" Empty profile created (memories will be saved here when active)")
174
354
 
175
355
  # Add to config
176
356
  self.config['profiles'][name] = {
177
357
  'name': name,
178
- 'description': description or f"Memory profile: {name}",
358
+ 'description': description or f'Memory profile: {name}',
179
359
  'created_at': datetime.now().isoformat(),
180
360
  'last_used': None,
181
361
  'created_from': 'current' if from_current else 'empty'
182
362
  }
183
363
  self._save_config()
184
364
 
185
- print(f"Profile '{name}' created successfully")
365
+ print(f"Profile '{name}' created successfully")
186
366
  return True
187
367
 
188
- def _initialize_empty_profile(self, profile_path):
189
- """Initialize empty profile with V2 schema."""
190
- # Import the migration script's initialization logic
191
- import sqlite3
192
-
193
- db_path = profile_path / "memory.db"
194
- conn = sqlite3.connect(db_path)
195
- cursor = conn.cursor()
196
-
197
- # Create basic V2 schema (simplified version)
198
- cursor.execute('''
199
- CREATE TABLE IF NOT EXISTS memories (
200
- id INTEGER PRIMARY KEY AUTOINCREMENT,
201
- content TEXT NOT NULL,
202
- summary TEXT,
203
- project_path TEXT,
204
- project_name TEXT,
205
- tags TEXT,
206
- category TEXT,
207
- memory_type TEXT DEFAULT 'session',
208
- importance INTEGER DEFAULT 5,
209
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
210
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
211
- last_accessed TIMESTAMP,
212
- access_count INTEGER DEFAULT 0,
213
- content_hash TEXT UNIQUE,
214
- parent_id INTEGER,
215
- tree_path TEXT,
216
- depth INTEGER DEFAULT 0,
217
- cluster_id INTEGER,
218
- tier INTEGER DEFAULT 1
219
- )
220
- ''')
221
-
222
- # Add other essential tables
223
- tables = [
224
- 'memory_tree', 'graph_nodes', 'graph_edges',
225
- 'graph_clusters', 'identity_patterns',
226
- 'pattern_examples', 'memory_archive'
227
- ]
228
-
229
- for table in tables:
230
- cursor.execute(f'CREATE TABLE IF NOT EXISTS {table} (id INTEGER PRIMARY KEY)')
231
-
232
- conn.commit()
233
- conn.close()
234
-
235
- # Create basic config
236
- config = {
237
- "version": "2.0.0",
238
- "embedding_model": "local-tfidf",
239
- "max_context_tokens": 4000
240
- }
241
-
242
- with open(profile_path / "config.json", 'w') as f:
243
- json.dump(config, f, indent=2)
244
-
245
- print(" ✓ Initialized V2 schema")
246
-
247
368
  def switch_profile(self, name):
248
- """Switch to a different profile."""
369
+ """
370
+ Switch to a different profile.
371
+ Column-based: just updates the active_profile in config. Instant. No data risk.
372
+ """
249
373
  if name not in self.config['profiles']:
250
- print(f"Error: Profile '{name}' not found")
374
+ print(f"Error: Profile '{name}' not found")
251
375
  print(f" Available: {', '.join(self.config['profiles'].keys())}")
252
376
  return False
253
377
 
254
- current = self.config.get('active_profile')
378
+ current = self.config.get('active_profile', 'default')
255
379
 
256
380
  if current == name:
257
381
  print(f"Already using profile: {name}")
258
382
  return True
259
383
 
260
- print(f"\nSwitching from '{current}' to '{name}'...")
261
-
262
- # Save current profile (skip for 'default' — it uses main DB directly)
263
- if current and current != 'default' and current in self.config['profiles']:
264
- self._save_current_to_profile(current)
265
-
266
- # Load new profile
267
- self._load_profile_to_main(name)
268
-
269
- # Update config
384
+ # Column-based switch: just update config
270
385
  self.config['active_profile'] = name
271
386
  self.config['profiles'][name]['last_used'] = datetime.now().isoformat()
272
387
  self._save_config()
273
388
 
274
- print(f"✅ Switched to profile: {name}")
275
- print(f"\n⚠️ IMPORTANT: Restart Claude CLI to use new profile!")
389
+ count = self._get_memory_count(name)
390
+ print(f"\nSwitched to profile: {name} ({count} memories)")
391
+ print(f"Previous profile: {current}")
276
392
  return True
277
393
 
278
- def _save_current_to_profile(self, profile_name):
279
- """Save current main memory system to profile."""
280
- print(f" Saving current state to profile '{profile_name}'...")
281
-
282
- profile_path = self._get_profile_path(profile_name)
283
- profile_path.mkdir(exist_ok=True)
284
-
285
- for file in self._get_main_files():
286
- src = self.memory_dir / file
287
- dst = profile_path / file
288
-
289
- if src.exists():
290
- # Remove old version
291
- if dst.exists():
292
- if dst.is_dir():
293
- shutil.rmtree(dst)
294
- else:
295
- dst.unlink()
296
-
297
- # Copy current
298
- if src.is_dir():
299
- shutil.copytree(src, dst)
300
- else:
301
- shutil.copy2(src, dst)
302
-
303
- print(f" ✓ Saved to {profile_path}")
304
-
305
- def _load_profile_to_main(self, profile_name):
306
- """Load profile to main memory system."""
307
- print(f" Loading profile '{profile_name}'...")
308
-
309
- profile_path = self._get_profile_path(profile_name)
310
-
311
- if not profile_path.exists():
312
- print(f" ⚠️ Profile directory not found, will create on switch")
313
- return
314
-
315
- for file in self._get_main_files():
316
- src = profile_path / file
317
- dst = self.memory_dir / file
318
-
319
- if src.exists():
320
- # Remove old version
321
- if dst.exists():
322
- if dst.is_dir():
323
- shutil.rmtree(dst)
324
- else:
325
- dst.unlink()
326
-
327
- # Copy profile
328
- if src.is_dir():
329
- shutil.copytree(src, dst)
330
- else:
331
- shutil.copy2(src, dst)
332
-
333
- print(f" ✓ Loaded from {profile_path}")
334
-
335
394
  def delete_profile(self, name, force=False):
336
- """Delete a profile."""
395
+ """Delete a profile. Moves memories to 'default' or deletes them."""
337
396
  if name not in self.config['profiles']:
338
- print(f"Error: Profile '{name}' not found")
397
+ print(f"Error: Profile '{name}' not found")
339
398
  return False
340
399
 
341
400
  if name == 'default':
342
- print(f"Error: Cannot delete 'default' profile")
401
+ print(f"Error: Cannot delete 'default' profile")
343
402
  return False
344
403
 
345
404
  if self.config.get('active_profile') == name:
346
- print(f"Error: Cannot delete active profile")
347
- print(f" Switch to another profile first")
405
+ print(f"Error: Cannot delete active profile")
406
+ print(f" Switch to another profile first: slm profile switch default")
348
407
  return False
349
408
 
409
+ count = self._get_memory_count(name)
410
+
350
411
  if not force:
351
- print(f"\n⚠️ WARNING: This will permanently delete profile '{name}'")
412
+ print(f"\nWARNING: This will delete profile '{name}' ({count} memories)")
413
+ print(f"Memories will be moved to 'default' profile before deletion.")
352
414
  response = input(f"Type profile name '{name}' to confirm: ")
353
415
 
354
416
  if response != name:
355
417
  print("Cancelled.")
356
418
  return False
357
419
 
358
- # Delete profile directory
359
- profile_path = self._get_profile_path(name)
360
- if profile_path.exists():
361
- shutil.rmtree(profile_path)
362
- print(f" Deleted profile directory")
420
+ # Move memories to default profile
421
+ if self.db_path.exists() and count > 0:
422
+ conn = sqlite3.connect(self.db_path)
423
+ cursor = conn.cursor()
424
+ cursor.execute("UPDATE memories SET profile = 'default' WHERE profile = ?", (name,))
425
+ moved = cursor.rowcount
426
+ conn.commit()
427
+ conn.close()
428
+ print(f" Moved {moved} memories to 'default' profile")
363
429
 
364
430
  # Remove from config
365
431
  del self.config['profiles'][name]
366
432
  self._save_config()
367
433
 
368
- print(f"Profile '{name}' deleted")
434
+ print(f"Profile '{name}' deleted")
369
435
  return True
370
436
 
371
437
  def show_current(self):
372
- """Show current active profile."""
438
+ """Show current active profile with stats."""
373
439
  active = self.config.get('active_profile', 'default')
374
440
 
375
441
  if active in self.config['profiles']:
376
442
  info = self.config['profiles'][active]
377
443
 
378
- print("\n" + "="*60)
444
+ print("\n" + "=" * 60)
379
445
  print("CURRENT ACTIVE PROFILE")
380
- print("="*60)
446
+ print("=" * 60)
381
447
  print(f"\nProfile: {active}")
382
448
  print(f"Description: {info.get('description', 'N/A')}")
383
449
  print(f"Created: {info.get('created_at', 'N/A')}")
384
450
  print(f"Last used: {info.get('last_used', 'N/A')}")
385
451
 
386
- # Show stats if database exists
387
- db_path = self.memory_dir / "memory.db"
388
- if db_path.exists():
389
- import sqlite3
390
- conn = sqlite3.connect(db_path)
391
- cursor = conn.cursor()
392
-
393
- cursor.execute('SELECT COUNT(*) FROM memories')
394
- memory_count = cursor.fetchone()[0]
452
+ count = self._get_memory_count(active)
453
+ print(f"\nMemories in this profile: {count}")
395
454
 
396
- print(f"\nMemories: {memory_count}")
455
+ # Show total memories across all profiles
456
+ if self.db_path.exists():
457
+ conn = sqlite3.connect(self.db_path)
458
+ cursor = conn.cursor()
459
+ cursor.execute("SELECT COUNT(*) FROM memories")
460
+ total = cursor.fetchone()[0]
397
461
  conn.close()
462
+ print(f"Total memories (all profiles): {total}")
398
463
  else:
399
- print(f"⚠️ Current profile '{active}' not found in config")
464
+ print(f"Warning: Current profile '{active}' not found in config")
465
+ print("Resetting to 'default' profile...")
466
+ self.config['active_profile'] = 'default'
467
+ self._save_config()
400
468
 
401
469
  def rename_profile(self, old_name, new_name):
402
- """Rename a profile."""
470
+ """Rename a profile. Updates the column value in all memories."""
403
471
  if old_name not in self.config['profiles']:
404
- print(f"Error: Profile '{old_name}' not found")
472
+ print(f"Error: Profile '{old_name}' not found")
405
473
  return False
406
474
 
407
475
  if new_name in self.config['profiles']:
408
- print(f"Error: Profile '{new_name}' already exists")
476
+ print(f"Error: Profile '{new_name}' already exists")
409
477
  return False
410
478
 
411
479
  if old_name == 'default':
412
- print(f"Error: Cannot rename 'default' profile")
480
+ print(f"Error: Cannot rename 'default' profile")
413
481
  return False
414
482
 
415
- # Rename directory
416
- old_path = self._get_profile_path(old_name)
417
- new_path = self._get_profile_path(new_name)
483
+ self._validate_profile_name(new_name)
418
484
 
419
- if old_path.exists():
420
- old_path.rename(new_path)
485
+ # Update profile column in all memories
486
+ if self.db_path.exists():
487
+ conn = sqlite3.connect(self.db_path)
488
+ cursor = conn.cursor()
489
+ cursor.execute("UPDATE memories SET profile = ? WHERE profile = ?", (new_name, old_name))
490
+ updated = cursor.rowcount
491
+ conn.commit()
492
+ conn.close()
493
+ print(f" Updated {updated} memories")
421
494
 
422
495
  # Update config
423
496
  self.config['profiles'][new_name] = self.config['profiles'][old_name]
@@ -429,13 +502,13 @@ class ProfileManager:
429
502
 
430
503
  self._save_config()
431
504
 
432
- print(f"Profile renamed: '{old_name}' '{new_name}'")
505
+ print(f"Profile renamed: '{old_name}' -> '{new_name}'")
433
506
  return True
434
507
 
435
508
 
436
509
  def main():
437
510
  parser = argparse.ArgumentParser(
438
- description='SuperLocalMemory V2 - Profile Management',
511
+ description='SuperLocalMemory V2 - Profile Management (Column-Based)',
439
512
  formatter_class=argparse.RawDescriptionHelpFormatter,
440
513
  epilog='''
441
514
  Examples:
@@ -451,14 +524,19 @@ Examples:
451
524
  # Create profile from current memories
452
525
  python memory-profiles.py create personal --from-current
453
526
 
454
- # Switch to different profile
527
+ # Switch to different profile (instant, no restart needed)
455
528
  python memory-profiles.py switch work
456
529
 
457
- # Delete a profile
530
+ # Delete a profile (memories moved to default)
458
531
  python memory-profiles.py delete old-profile
459
532
 
460
533
  # Rename profile
461
534
  python memory-profiles.py rename old-name new-name
535
+
536
+ Architecture (v2.4.0):
537
+ All profiles share ONE database (memory.db).
538
+ Each memory has a 'profile' column.
539
+ Switching profiles = changing config. Instant. Safe.
462
540
  '''
463
541
  )
464
542
 
@@ -469,7 +547,7 @@ Examples:
469
547
  parser.add_argument('name2', nargs='?', help='Second name (for rename)')
470
548
  parser.add_argument('--description', help='Profile description')
471
549
  parser.add_argument('--from-current', action='store_true',
472
- help='Create from current memory system')
550
+ help='Create from current profile memories')
473
551
  parser.add_argument('--force', action='store_true',
474
552
  help='Force operation without confirmation')
475
553
 
@@ -485,7 +563,7 @@ Examples:
485
563
 
486
564
  elif args.command == 'create':
487
565
  if not args.name:
488
- print("Error: Profile name required")
566
+ print("Error: Profile name required")
489
567
  print(" Usage: python memory-profiles.py create <name>")
490
568
  sys.exit(1)
491
569
 
@@ -493,21 +571,21 @@ Examples:
493
571
 
494
572
  elif args.command == 'switch':
495
573
  if not args.name:
496
- print("Error: Profile name required")
574
+ print("Error: Profile name required")
497
575
  sys.exit(1)
498
576
 
499
577
  manager.switch_profile(args.name)
500
578
 
501
579
  elif args.command == 'delete':
502
580
  if not args.name:
503
- print("Error: Profile name required")
581
+ print("Error: Profile name required")
504
582
  sys.exit(1)
505
583
 
506
584
  manager.delete_profile(args.name, args.force)
507
585
 
508
586
  elif args.command == 'rename':
509
587
  if not args.name or not args.name2:
510
- print("Error: Both old and new names required")
588
+ print("Error: Both old and new names required")
511
589
  print(" Usage: python memory-profiles.py rename <old> <new>")
512
590
  sys.exit(1)
513
591