superlocalmemory 2.8.2 → 2.8.5
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/ATTRIBUTION.md +1 -1
- package/CHANGELOG.md +17 -0
- package/README.md +7 -5
- package/api_server.py +5 -0
- package/bin/slm +35 -0
- package/bin/slm.bat +3 -3
- package/docs/SECURITY-QUICK-REFERENCE.md +214 -0
- package/install.ps1 +11 -11
- package/mcp_server.py +78 -10
- package/package.json +2 -2
- package/requirements-core.txt +16 -18
- package/requirements-learning.txt +8 -8
- package/requirements.txt +9 -7
- package/scripts/prepack.js +33 -0
- package/scripts/verify-v27.ps1 +301 -0
- package/src/agent_registry.py +32 -28
- package/src/auto_backup.py +12 -6
- package/src/cache_manager.py +2 -2
- package/src/compression/__init__.py +25 -0
- package/src/compression/cli.py +150 -0
- package/src/compression/cold_storage.py +217 -0
- package/src/compression/config.py +72 -0
- package/src/compression/orchestrator.py +133 -0
- package/src/compression/tier2_compressor.py +228 -0
- package/src/compression/tier3_compressor.py +153 -0
- package/src/compression/tier_classifier.py +148 -0
- package/src/db_connection_manager.py +5 -5
- package/src/event_bus.py +24 -22
- package/src/hnsw_index.py +3 -3
- package/src/learning/__init__.py +5 -4
- package/src/learning/adaptive_ranker.py +14 -265
- package/src/learning/bootstrap/__init__.py +69 -0
- package/src/learning/bootstrap/constants.py +93 -0
- package/src/learning/bootstrap/db_queries.py +316 -0
- package/src/learning/bootstrap/sampling.py +82 -0
- package/src/learning/bootstrap/text_utils.py +71 -0
- package/src/learning/cross_project_aggregator.py +58 -57
- package/src/learning/db/__init__.py +40 -0
- package/src/learning/db/constants.py +44 -0
- package/src/learning/db/schema.py +279 -0
- package/src/learning/learning_db.py +15 -234
- package/src/learning/ranking/__init__.py +33 -0
- package/src/learning/ranking/constants.py +84 -0
- package/src/learning/ranking/helpers.py +278 -0
- package/src/learning/source_quality_scorer.py +66 -65
- package/src/learning/synthetic_bootstrap.py +28 -310
- package/src/memory/__init__.py +36 -0
- package/src/memory/cli.py +205 -0
- package/src/memory/constants.py +39 -0
- package/src/memory/helpers.py +28 -0
- package/src/memory/schema.py +166 -0
- package/src/memory-profiles.py +94 -86
- package/src/memory-reset.py +187 -185
- package/src/memory_compression.py +2 -2
- package/src/memory_store_v2.py +44 -354
- package/src/migrate_v1_to_v2.py +11 -10
- package/src/patterns/analyzers.py +104 -100
- package/src/patterns/learner.py +17 -13
- package/src/patterns/scoring.py +25 -21
- package/src/patterns/store.py +40 -38
- package/src/patterns/terminology.py +53 -51
- package/src/provenance_tracker.py +2 -2
- package/src/qualixar_attribution.py +1 -1
- package/src/search/engine.py +16 -14
- package/src/search/index_loader.py +13 -11
- package/src/setup_validator.py +160 -158
- package/src/subscription_manager.py +20 -18
- package/src/tree/builder.py +66 -64
- package/src/tree/nodes.py +103 -97
- package/src/tree/queries.py +142 -137
- package/src/tree/schema.py +46 -42
- package/src/webhook_dispatcher.py +3 -3
- package/ui_server.py +7 -4
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
|
|
4
|
+
"""
|
|
5
|
+
Helper utilities for MemoryStoreV2.
|
|
6
|
+
|
|
7
|
+
This module contains standalone utility functions extracted from memory_store_v2.py
|
|
8
|
+
to reduce file size and improve maintainability.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def format_content(content: str, full: bool = False, threshold: int = 5000, preview_len: int = 2000) -> str:
|
|
13
|
+
"""
|
|
14
|
+
Smart content formatting with optional truncation.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
content: Content to format
|
|
18
|
+
full: If True, always show full content
|
|
19
|
+
threshold: Max length before truncation (default 5000)
|
|
20
|
+
preview_len: Preview length when truncating (default 2000)
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Formatted content string
|
|
24
|
+
"""
|
|
25
|
+
if full or len(content) < threshold:
|
|
26
|
+
return content
|
|
27
|
+
else:
|
|
28
|
+
return f"{content[:preview_len]}..."
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
|
|
4
|
+
"""
|
|
5
|
+
Database schema definitions for MemoryStoreV2.
|
|
6
|
+
|
|
7
|
+
This module contains SQL schema definitions and migration logic extracted from
|
|
8
|
+
memory_store_v2.py to reduce file size and improve maintainability.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Dict, List, Tuple
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# V2 column definitions for migration support
|
|
15
|
+
V2_COLUMNS: Dict[str, str] = {
|
|
16
|
+
'summary': 'TEXT',
|
|
17
|
+
'project_path': 'TEXT',
|
|
18
|
+
'project_name': 'TEXT',
|
|
19
|
+
'category': 'TEXT',
|
|
20
|
+
'parent_id': 'INTEGER',
|
|
21
|
+
'tree_path': 'TEXT',
|
|
22
|
+
'depth': 'INTEGER DEFAULT 0',
|
|
23
|
+
'memory_type': 'TEXT DEFAULT "session"',
|
|
24
|
+
'importance': 'INTEGER DEFAULT 5',
|
|
25
|
+
'updated_at': 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP',
|
|
26
|
+
'last_accessed': 'TIMESTAMP',
|
|
27
|
+
'access_count': 'INTEGER DEFAULT 0',
|
|
28
|
+
'content_hash': 'TEXT',
|
|
29
|
+
'cluster_id': 'INTEGER',
|
|
30
|
+
'profile': 'TEXT DEFAULT "default"'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# v2.8.0 schema migrations - lifecycle + access control columns
|
|
34
|
+
V28_MIGRATIONS: List[Tuple[str, str]] = [
|
|
35
|
+
("lifecycle_state", "TEXT DEFAULT 'active'"),
|
|
36
|
+
("lifecycle_updated_at", "TIMESTAMP"),
|
|
37
|
+
("lifecycle_history", "TEXT DEFAULT '[]'"),
|
|
38
|
+
("access_level", "TEXT DEFAULT 'public'"),
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
# Index definitions for V2 fields
|
|
42
|
+
V2_INDEXES: List[Tuple[str, str]] = [
|
|
43
|
+
('idx_project', 'project_path'),
|
|
44
|
+
('idx_tags', 'tags'),
|
|
45
|
+
('idx_category', 'category'),
|
|
46
|
+
('idx_tree_path', 'tree_path'),
|
|
47
|
+
('idx_cluster', 'cluster_id'),
|
|
48
|
+
('idx_last_accessed', 'last_accessed'),
|
|
49
|
+
('idx_parent_id', 'parent_id'),
|
|
50
|
+
('idx_profile', 'profile')
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_memories_table_sql() -> str:
|
|
55
|
+
"""
|
|
56
|
+
Returns the CREATE TABLE SQL for the main memories table.
|
|
57
|
+
V1 compatible + V2 extensions.
|
|
58
|
+
"""
|
|
59
|
+
return '''
|
|
60
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
61
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
62
|
+
content TEXT NOT NULL,
|
|
63
|
+
summary TEXT,
|
|
64
|
+
|
|
65
|
+
-- Organization
|
|
66
|
+
project_path TEXT,
|
|
67
|
+
project_name TEXT,
|
|
68
|
+
tags TEXT,
|
|
69
|
+
category TEXT,
|
|
70
|
+
|
|
71
|
+
-- Hierarchy (Layer 2 link)
|
|
72
|
+
parent_id INTEGER,
|
|
73
|
+
tree_path TEXT,
|
|
74
|
+
depth INTEGER DEFAULT 0,
|
|
75
|
+
|
|
76
|
+
-- Metadata
|
|
77
|
+
memory_type TEXT DEFAULT 'session',
|
|
78
|
+
importance INTEGER DEFAULT 5,
|
|
79
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
80
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
81
|
+
last_accessed TIMESTAMP,
|
|
82
|
+
access_count INTEGER DEFAULT 0,
|
|
83
|
+
|
|
84
|
+
-- Deduplication
|
|
85
|
+
content_hash TEXT UNIQUE,
|
|
86
|
+
|
|
87
|
+
-- Graph (Layer 3 link)
|
|
88
|
+
cluster_id INTEGER,
|
|
89
|
+
|
|
90
|
+
FOREIGN KEY (parent_id) REFERENCES memories(id) ON DELETE CASCADE
|
|
91
|
+
)
|
|
92
|
+
'''
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get_sessions_table_sql() -> str:
|
|
96
|
+
"""
|
|
97
|
+
Returns the CREATE TABLE SQL for the sessions table.
|
|
98
|
+
V1 compatible.
|
|
99
|
+
"""
|
|
100
|
+
return '''
|
|
101
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
102
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
103
|
+
session_id TEXT UNIQUE,
|
|
104
|
+
project_path TEXT,
|
|
105
|
+
started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
106
|
+
ended_at TIMESTAMP,
|
|
107
|
+
summary TEXT
|
|
108
|
+
)
|
|
109
|
+
'''
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def get_fts_table_sql() -> str:
|
|
113
|
+
"""
|
|
114
|
+
Returns the CREATE VIRTUAL TABLE SQL for full-text search.
|
|
115
|
+
V1 compatible.
|
|
116
|
+
"""
|
|
117
|
+
return '''
|
|
118
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts
|
|
119
|
+
USING fts5(content, summary, tags, content='memories', content_rowid='id')
|
|
120
|
+
'''
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def get_fts_trigger_insert_sql() -> str:
|
|
124
|
+
"""Returns the FTS INSERT trigger SQL."""
|
|
125
|
+
return '''
|
|
126
|
+
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
|
|
127
|
+
INSERT INTO memories_fts(rowid, content, summary, tags)
|
|
128
|
+
VALUES (new.id, new.content, new.summary, new.tags);
|
|
129
|
+
END
|
|
130
|
+
'''
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def get_fts_trigger_delete_sql() -> str:
|
|
134
|
+
"""Returns the FTS DELETE trigger SQL."""
|
|
135
|
+
return '''
|
|
136
|
+
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
|
|
137
|
+
INSERT INTO memories_fts(memories_fts, rowid, content, summary, tags)
|
|
138
|
+
VALUES('delete', old.id, old.content, old.summary, old.tags);
|
|
139
|
+
END
|
|
140
|
+
'''
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def get_fts_trigger_update_sql() -> str:
|
|
144
|
+
"""Returns the FTS UPDATE trigger SQL."""
|
|
145
|
+
return '''
|
|
146
|
+
CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
|
|
147
|
+
INSERT INTO memories_fts(memories_fts, rowid, content, summary, tags)
|
|
148
|
+
VALUES('delete', old.id, old.content, old.summary, old.tags);
|
|
149
|
+
INSERT INTO memories_fts(rowid, content, summary, tags)
|
|
150
|
+
VALUES (new.id, new.content, new.summary, new.tags);
|
|
151
|
+
END
|
|
152
|
+
'''
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def get_creator_metadata_table_sql() -> str:
|
|
156
|
+
"""
|
|
157
|
+
Returns the CREATE TABLE SQL for creator attribution metadata.
|
|
158
|
+
REQUIRED by MIT License.
|
|
159
|
+
"""
|
|
160
|
+
return '''
|
|
161
|
+
CREATE TABLE IF NOT EXISTS creator_metadata (
|
|
162
|
+
key TEXT PRIMARY KEY,
|
|
163
|
+
value TEXT NOT NULL,
|
|
164
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
165
|
+
)
|
|
166
|
+
'''
|
package/src/memory-profiles.py
CHANGED
|
@@ -128,74 +128,74 @@ class ProfileManager:
|
|
|
128
128
|
main_cursor = main_conn.cursor()
|
|
129
129
|
|
|
130
130
|
old_conn = sqlite3.connect(old_db_path)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
131
|
+
try:
|
|
132
|
+
old_cursor = old_conn.cursor()
|
|
133
|
+
|
|
134
|
+
# Get existing hashes
|
|
135
|
+
main_cursor.execute("SELECT content_hash FROM memories WHERE content_hash IS NOT NULL")
|
|
136
|
+
existing_hashes = {row[0] for row in main_cursor.fetchall()}
|
|
137
|
+
|
|
138
|
+
# Get columns from old DB
|
|
139
|
+
old_cursor.execute("PRAGMA table_info(memories)")
|
|
140
|
+
old_columns = {row[1] for row in old_cursor.fetchall()}
|
|
141
|
+
|
|
142
|
+
# Build SELECT based on available columns
|
|
143
|
+
select_cols = ['content', 'summary', 'project_path', 'project_name', 'tags',
|
|
144
|
+
'category', 'memory_type', 'importance', 'created_at', 'updated_at',
|
|
145
|
+
'content_hash']
|
|
146
|
+
available_cols = [c for c in select_cols if c in old_columns]
|
|
147
|
+
|
|
148
|
+
if 'content' not in available_cols:
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
old_cursor.execute(f"SELECT {', '.join(available_cols)} FROM memories")
|
|
152
|
+
rows = old_cursor.fetchall()
|
|
153
|
+
|
|
154
|
+
imported = 0
|
|
155
|
+
for row in rows:
|
|
156
|
+
row_dict = dict(zip(available_cols, row))
|
|
157
|
+
content = row_dict.get('content', '')
|
|
158
|
+
content_hash = row_dict.get('content_hash')
|
|
159
|
+
|
|
160
|
+
if not content:
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
# Generate hash if missing
|
|
164
|
+
if not content_hash:
|
|
165
|
+
content_hash = hashlib.sha256(content.encode()).hexdigest()[:32]
|
|
166
|
+
|
|
167
|
+
if content_hash in existing_hashes:
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
main_cursor.execute('''
|
|
172
|
+
INSERT INTO memories (content, summary, project_path, project_name, tags,
|
|
173
|
+
category, memory_type, importance, created_at, updated_at,
|
|
174
|
+
content_hash, profile)
|
|
175
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
176
|
+
''', (
|
|
177
|
+
content,
|
|
178
|
+
row_dict.get('summary'),
|
|
179
|
+
row_dict.get('project_path'),
|
|
180
|
+
row_dict.get('project_name'),
|
|
181
|
+
row_dict.get('tags'),
|
|
182
|
+
row_dict.get('category'),
|
|
183
|
+
row_dict.get('memory_type', 'session'),
|
|
184
|
+
row_dict.get('importance', 5),
|
|
185
|
+
row_dict.get('created_at'),
|
|
186
|
+
row_dict.get('updated_at'),
|
|
187
|
+
content_hash,
|
|
188
|
+
profile_name
|
|
189
|
+
))
|
|
190
|
+
imported += 1
|
|
191
|
+
existing_hashes.add(content_hash)
|
|
192
|
+
except sqlite3.IntegrityError:
|
|
193
|
+
pass
|
|
194
|
+
|
|
195
|
+
main_conn.commit()
|
|
196
|
+
finally:
|
|
148
197
|
old_conn.close()
|
|
149
198
|
main_conn.close()
|
|
150
|
-
return
|
|
151
|
-
|
|
152
|
-
old_cursor.execute(f"SELECT {', '.join(available_cols)} FROM memories")
|
|
153
|
-
rows = old_cursor.fetchall()
|
|
154
|
-
|
|
155
|
-
imported = 0
|
|
156
|
-
for row in rows:
|
|
157
|
-
row_dict = dict(zip(available_cols, row))
|
|
158
|
-
content = row_dict.get('content', '')
|
|
159
|
-
content_hash = row_dict.get('content_hash')
|
|
160
|
-
|
|
161
|
-
if not content:
|
|
162
|
-
continue
|
|
163
|
-
|
|
164
|
-
# Generate hash if missing
|
|
165
|
-
if not content_hash:
|
|
166
|
-
content_hash = hashlib.sha256(content.encode()).hexdigest()[:32]
|
|
167
|
-
|
|
168
|
-
if content_hash in existing_hashes:
|
|
169
|
-
continue
|
|
170
|
-
|
|
171
|
-
try:
|
|
172
|
-
main_cursor.execute('''
|
|
173
|
-
INSERT INTO memories (content, summary, project_path, project_name, tags,
|
|
174
|
-
category, memory_type, importance, created_at, updated_at,
|
|
175
|
-
content_hash, profile)
|
|
176
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
177
|
-
''', (
|
|
178
|
-
content,
|
|
179
|
-
row_dict.get('summary'),
|
|
180
|
-
row_dict.get('project_path'),
|
|
181
|
-
row_dict.get('project_name'),
|
|
182
|
-
row_dict.get('tags'),
|
|
183
|
-
row_dict.get('category'),
|
|
184
|
-
row_dict.get('memory_type', 'session'),
|
|
185
|
-
row_dict.get('importance', 5),
|
|
186
|
-
row_dict.get('created_at'),
|
|
187
|
-
row_dict.get('updated_at'),
|
|
188
|
-
content_hash,
|
|
189
|
-
profile_name
|
|
190
|
-
))
|
|
191
|
-
imported += 1
|
|
192
|
-
existing_hashes.add(content_hash)
|
|
193
|
-
except sqlite3.IntegrityError:
|
|
194
|
-
pass
|
|
195
|
-
|
|
196
|
-
main_conn.commit()
|
|
197
|
-
old_conn.close()
|
|
198
|
-
main_conn.close()
|
|
199
199
|
|
|
200
200
|
if imported > 0:
|
|
201
201
|
# Add profile to config if not present
|
|
@@ -257,17 +257,19 @@ class ProfileManager:
|
|
|
257
257
|
return 0
|
|
258
258
|
|
|
259
259
|
conn = sqlite3.connect(self.db_path)
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
260
|
+
try:
|
|
261
|
+
cursor = conn.cursor()
|
|
262
|
+
cursor.execute("SELECT COUNT(*) FROM memories WHERE profile = ?", (profile_name,))
|
|
263
|
+
count = cursor.fetchone()[0]
|
|
264
|
+
finally:
|
|
265
|
+
conn.close()
|
|
264
266
|
return count
|
|
265
267
|
|
|
266
|
-
def get_active_profile(self):
|
|
268
|
+
def get_active_profile(self) -> str:
|
|
267
269
|
"""Get the currently active profile name."""
|
|
268
270
|
return self.config.get('active_profile', 'default')
|
|
269
271
|
|
|
270
|
-
def list_profiles(self):
|
|
272
|
+
def list_profiles(self) -> list:
|
|
271
273
|
"""List all available profiles with memory counts."""
|
|
272
274
|
print("\n" + "=" * 60)
|
|
273
275
|
print("AVAILABLE MEMORY PROFILES")
|
|
@@ -410,11 +412,13 @@ class ProfileManager:
|
|
|
410
412
|
# Move memories to default profile
|
|
411
413
|
if self.db_path.exists() and count > 0:
|
|
412
414
|
conn = sqlite3.connect(self.db_path)
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
415
|
+
try:
|
|
416
|
+
cursor = conn.cursor()
|
|
417
|
+
cursor.execute("UPDATE memories SET profile = 'default' WHERE profile = ?", (name,))
|
|
418
|
+
moved = cursor.rowcount
|
|
419
|
+
conn.commit()
|
|
420
|
+
finally:
|
|
421
|
+
conn.close()
|
|
418
422
|
print(f" Moved {moved} memories to 'default' profile")
|
|
419
423
|
|
|
420
424
|
# Remove from config
|
|
@@ -445,10 +449,12 @@ class ProfileManager:
|
|
|
445
449
|
# Show total memories across all profiles
|
|
446
450
|
if self.db_path.exists():
|
|
447
451
|
conn = sqlite3.connect(self.db_path)
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
+
try:
|
|
453
|
+
cursor = conn.cursor()
|
|
454
|
+
cursor.execute("SELECT COUNT(*) FROM memories")
|
|
455
|
+
total = cursor.fetchone()[0]
|
|
456
|
+
finally:
|
|
457
|
+
conn.close()
|
|
452
458
|
print(f"Total memories (all profiles): {total}")
|
|
453
459
|
else:
|
|
454
460
|
print(f"Warning: Current profile '{active}' not found in config")
|
|
@@ -475,11 +481,13 @@ class ProfileManager:
|
|
|
475
481
|
# Update profile column in all memories
|
|
476
482
|
if self.db_path.exists():
|
|
477
483
|
conn = sqlite3.connect(self.db_path)
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
484
|
+
try:
|
|
485
|
+
cursor = conn.cursor()
|
|
486
|
+
cursor.execute("UPDATE memories SET profile = ? WHERE profile = ?", (new_name, old_name))
|
|
487
|
+
updated = cursor.rowcount
|
|
488
|
+
conn.commit()
|
|
489
|
+
finally:
|
|
490
|
+
conn.close()
|
|
483
491
|
print(f" Updated {updated} memories")
|
|
484
492
|
|
|
485
493
|
# Update config
|