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.
- package/CHANGELOG.md +41 -0
- package/README.md +25 -0
- package/hooks/memory-profile-skill.js +7 -18
- package/mcp_server.py +74 -12
- package/package.json +1 -1
- package/src/__pycache__/auto_backup.cpython-312.pyc +0 -0
- package/src/__pycache__/memory_store_v2.cpython-312.pyc +0 -0
- package/src/__pycache__/pattern_learner.cpython-312.pyc +0 -0
- package/src/auto_backup.py +424 -0
- package/src/graph_engine.py +126 -39
- package/src/memory-profiles.py +321 -243
- package/src/memory_store_v2.py +82 -31
- package/src/pattern_learner.py +126 -44
- package/ui/app.js +526 -17
- package/ui/index.html +182 -1
- package/ui_server.py +340 -43
package/src/memory-profiles.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
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
|
|
52
|
-
self.
|
|
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
|
|
87
|
-
"""
|
|
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 ['.', '..'
|
|
261
|
+
if profile_name in ['.', '..']:
|
|
101
262
|
raise ValueError(f"Reserved profile name: {profile_name}")
|
|
102
263
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
|
112
|
-
"""Get
|
|
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
|
|
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':
|
|
132
|
-
print("-" *
|
|
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')[:
|
|
137
|
-
marker = "
|
|
138
|
-
|
|
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
|
-
"""
|
|
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
|
-
|
|
312
|
+
self._validate_profile_name(name)
|
|
313
|
+
|
|
148
314
|
if name in self.config['profiles']:
|
|
149
|
-
print(f"
|
|
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
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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
|
|
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"
|
|
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
|
-
"""
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
275
|
-
print(f"\
|
|
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"
|
|
397
|
+
print(f"Error: Profile '{name}' not found")
|
|
339
398
|
return False
|
|
340
399
|
|
|
341
400
|
if name == 'default':
|
|
342
|
-
print(f"
|
|
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"
|
|
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"\
|
|
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
|
-
#
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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"
|
|
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
|
-
|
|
387
|
-
|
|
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
|
-
|
|
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"
|
|
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"
|
|
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"
|
|
476
|
+
print(f"Error: Profile '{new_name}' already exists")
|
|
409
477
|
return False
|
|
410
478
|
|
|
411
479
|
if old_name == 'default':
|
|
412
|
-
print(f"
|
|
480
|
+
print(f"Error: Cannot rename 'default' profile")
|
|
413
481
|
return False
|
|
414
482
|
|
|
415
|
-
|
|
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
|
-
|
|
420
|
-
|
|
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"
|
|
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
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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
|
|