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.
- package/CHANGELOG.md +65 -0
- package/README.md +25 -0
- package/hooks/memory-list-skill.js +13 -4
- package/hooks/memory-profile-skill.js +7 -18
- package/hooks/memory-recall-skill.js +6 -1
- 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 +131 -43
- 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/ui_server.py
CHANGED
|
@@ -124,6 +124,23 @@ UI_DIR.mkdir(exist_ok=True)
|
|
|
124
124
|
app.mount("/static", StaticFiles(directory=str(UI_DIR)), name="static")
|
|
125
125
|
|
|
126
126
|
|
|
127
|
+
# ============================================================================
|
|
128
|
+
# Profile Helper
|
|
129
|
+
# ============================================================================
|
|
130
|
+
|
|
131
|
+
def get_active_profile() -> str:
|
|
132
|
+
"""Read the active profile from profiles.json. Falls back to 'default'."""
|
|
133
|
+
config_file = MEMORY_DIR / "profiles.json"
|
|
134
|
+
if config_file.exists():
|
|
135
|
+
try:
|
|
136
|
+
with open(config_file, 'r') as f:
|
|
137
|
+
pconfig = json.load(f)
|
|
138
|
+
return pconfig.get('active_profile', 'default')
|
|
139
|
+
except (json.JSONDecodeError, IOError):
|
|
140
|
+
pass
|
|
141
|
+
return 'default'
|
|
142
|
+
|
|
143
|
+
|
|
127
144
|
# ============================================================================
|
|
128
145
|
# Request/Response Models
|
|
129
146
|
# ============================================================================
|
|
@@ -387,6 +404,8 @@ async def get_memories(
|
|
|
387
404
|
conn.row_factory = dict_factory
|
|
388
405
|
cursor = conn.cursor()
|
|
389
406
|
|
|
407
|
+
active_profile = get_active_profile()
|
|
408
|
+
|
|
390
409
|
# Build dynamic query
|
|
391
410
|
query = """
|
|
392
411
|
SELECT
|
|
@@ -394,9 +413,9 @@ async def get_memories(
|
|
|
394
413
|
importance, cluster_id, depth, access_count, parent_id,
|
|
395
414
|
created_at, updated_at, last_accessed, tags, memory_type
|
|
396
415
|
FROM memories
|
|
397
|
-
WHERE
|
|
416
|
+
WHERE profile = ?
|
|
398
417
|
"""
|
|
399
|
-
params = []
|
|
418
|
+
params = [active_profile]
|
|
400
419
|
|
|
401
420
|
if category:
|
|
402
421
|
query += " AND category = ?"
|
|
@@ -427,8 +446,8 @@ async def get_memories(
|
|
|
427
446
|
memories = cursor.fetchall()
|
|
428
447
|
|
|
429
448
|
# Get total count
|
|
430
|
-
count_query = "SELECT COUNT(*) as total FROM memories WHERE
|
|
431
|
-
count_params = []
|
|
449
|
+
count_query = "SELECT COUNT(*) as total FROM memories WHERE profile = ?"
|
|
450
|
+
count_params = [active_profile]
|
|
432
451
|
|
|
433
452
|
if category:
|
|
434
453
|
count_query += " AND category = ?"
|
|
@@ -483,6 +502,8 @@ async def get_graph(
|
|
|
483
502
|
conn.row_factory = dict_factory
|
|
484
503
|
cursor = conn.cursor()
|
|
485
504
|
|
|
505
|
+
active_profile = get_active_profile()
|
|
506
|
+
|
|
486
507
|
# Get nodes (memories with graph data)
|
|
487
508
|
cursor.execute("""
|
|
488
509
|
SELECT
|
|
@@ -492,10 +513,10 @@ async def get_graph(
|
|
|
492
513
|
gn.entities
|
|
493
514
|
FROM memories m
|
|
494
515
|
LEFT JOIN graph_nodes gn ON m.id = gn.memory_id
|
|
495
|
-
WHERE m.importance >= ?
|
|
516
|
+
WHERE m.importance >= ? AND m.profile = ?
|
|
496
517
|
ORDER BY m.importance DESC, m.updated_at DESC
|
|
497
518
|
LIMIT ?
|
|
498
|
-
""", (min_importance, max_nodes))
|
|
519
|
+
""", (min_importance, active_profile, max_nodes))
|
|
499
520
|
nodes = cursor.fetchall()
|
|
500
521
|
|
|
501
522
|
# Parse entities JSON and create previews
|
|
@@ -551,9 +572,9 @@ async def get_graph(
|
|
|
551
572
|
COUNT(*) as size,
|
|
552
573
|
AVG(importance) as avg_importance
|
|
553
574
|
FROM memories
|
|
554
|
-
WHERE cluster_id IS NOT NULL
|
|
575
|
+
WHERE cluster_id IS NOT NULL AND profile = ?
|
|
555
576
|
GROUP BY cluster_id
|
|
556
|
-
""")
|
|
577
|
+
""", (active_profile,))
|
|
557
578
|
clusters = cursor.fetchall()
|
|
558
579
|
|
|
559
580
|
conn.close()
|
|
@@ -607,6 +628,8 @@ async def get_timeline(
|
|
|
607
628
|
else: # month
|
|
608
629
|
date_group = "strftime('%Y-%m', created_at)"
|
|
609
630
|
|
|
631
|
+
active_profile = get_active_profile()
|
|
632
|
+
|
|
610
633
|
# Timeline aggregates
|
|
611
634
|
cursor.execute(f"""
|
|
612
635
|
SELECT
|
|
@@ -618,9 +641,10 @@ async def get_timeline(
|
|
|
618
641
|
GROUP_CONCAT(DISTINCT category) as categories
|
|
619
642
|
FROM memories
|
|
620
643
|
WHERE created_at >= datetime('now', '-' || ? || ' days')
|
|
644
|
+
AND profile = ?
|
|
621
645
|
GROUP BY {date_group}
|
|
622
646
|
ORDER BY period DESC
|
|
623
|
-
""", (days,))
|
|
647
|
+
""", (days, active_profile))
|
|
624
648
|
timeline = cursor.fetchall()
|
|
625
649
|
|
|
626
650
|
# Category trend over time
|
|
@@ -631,10 +655,10 @@ async def get_timeline(
|
|
|
631
655
|
COUNT(*) as count
|
|
632
656
|
FROM memories
|
|
633
657
|
WHERE created_at >= datetime('now', '-' || ? || ' days')
|
|
634
|
-
AND category IS NOT NULL
|
|
658
|
+
AND category IS NOT NULL AND profile = ?
|
|
635
659
|
GROUP BY {date_group}, category
|
|
636
660
|
ORDER BY period DESC, count DESC
|
|
637
|
-
""", (days,))
|
|
661
|
+
""", (days, active_profile))
|
|
638
662
|
category_trend = cursor.fetchall()
|
|
639
663
|
|
|
640
664
|
# Period statistics
|
|
@@ -646,7 +670,8 @@ async def get_timeline(
|
|
|
646
670
|
AVG(importance) as avg_importance
|
|
647
671
|
FROM memories
|
|
648
672
|
WHERE created_at >= datetime('now', '-' || ? || ' days')
|
|
649
|
-
|
|
673
|
+
AND profile = ?
|
|
674
|
+
""", (days, active_profile))
|
|
650
675
|
period_stats = cursor.fetchone()
|
|
651
676
|
|
|
652
677
|
conn.close()
|
|
@@ -680,6 +705,8 @@ async def get_clusters():
|
|
|
680
705
|
conn.row_factory = dict_factory
|
|
681
706
|
cursor = conn.cursor()
|
|
682
707
|
|
|
708
|
+
active_profile = get_active_profile()
|
|
709
|
+
|
|
683
710
|
# Get cluster statistics
|
|
684
711
|
cursor.execute("""
|
|
685
712
|
SELECT
|
|
@@ -693,10 +720,10 @@ async def get_clusters():
|
|
|
693
720
|
MIN(created_at) as first_memory,
|
|
694
721
|
MAX(created_at) as latest_memory
|
|
695
722
|
FROM memories
|
|
696
|
-
WHERE cluster_id IS NOT NULL
|
|
723
|
+
WHERE cluster_id IS NOT NULL AND profile = ?
|
|
697
724
|
GROUP BY cluster_id
|
|
698
725
|
ORDER BY member_count DESC
|
|
699
|
-
""")
|
|
726
|
+
""", (active_profile,))
|
|
700
727
|
clusters = cursor.fetchall()
|
|
701
728
|
|
|
702
729
|
# Get dominant entities per cluster
|
|
@@ -732,8 +759,8 @@ async def get_clusters():
|
|
|
732
759
|
cursor.execute("""
|
|
733
760
|
SELECT COUNT(*) as count
|
|
734
761
|
FROM memories
|
|
735
|
-
WHERE cluster_id IS NULL
|
|
736
|
-
""")
|
|
762
|
+
WHERE cluster_id IS NULL AND profile = ?
|
|
763
|
+
""", (active_profile,))
|
|
737
764
|
unclustered = cursor.fetchone()['count']
|
|
738
765
|
|
|
739
766
|
conn.close()
|
|
@@ -872,13 +899,16 @@ async def get_patterns():
|
|
|
872
899
|
"message": "Pattern learning not initialized. Run pattern learning first."
|
|
873
900
|
}
|
|
874
901
|
|
|
902
|
+
active_profile = get_active_profile()
|
|
903
|
+
|
|
875
904
|
cursor.execute("""
|
|
876
905
|
SELECT
|
|
877
906
|
pattern_type, key, value, confidence,
|
|
878
907
|
evidence_count, updated_at as last_updated
|
|
879
908
|
FROM identity_patterns
|
|
909
|
+
WHERE profile = ?
|
|
880
910
|
ORDER BY confidence DESC, evidence_count DESC
|
|
881
|
-
""")
|
|
911
|
+
""", (active_profile,))
|
|
882
912
|
patterns = cursor.fetchall()
|
|
883
913
|
|
|
884
914
|
# Parse value JSON
|
|
@@ -937,14 +967,16 @@ async def get_stats():
|
|
|
937
967
|
conn.row_factory = dict_factory
|
|
938
968
|
cursor = conn.cursor()
|
|
939
969
|
|
|
940
|
-
|
|
941
|
-
|
|
970
|
+
active_profile = get_active_profile()
|
|
971
|
+
|
|
972
|
+
# Basic counts (profile-filtered)
|
|
973
|
+
cursor.execute("SELECT COUNT(*) as total FROM memories WHERE profile = ?", (active_profile,))
|
|
942
974
|
total_memories = cursor.fetchone()['total']
|
|
943
975
|
|
|
944
976
|
cursor.execute("SELECT COUNT(*) as total FROM sessions")
|
|
945
977
|
total_sessions = cursor.fetchone()['total']
|
|
946
978
|
|
|
947
|
-
cursor.execute("SELECT COUNT(DISTINCT cluster_id) as total FROM memories WHERE cluster_id IS NOT NULL")
|
|
979
|
+
cursor.execute("SELECT COUNT(DISTINCT cluster_id) as total FROM memories WHERE cluster_id IS NOT NULL AND profile = ?", (active_profile,))
|
|
948
980
|
total_clusters = cursor.fetchone()['total']
|
|
949
981
|
|
|
950
982
|
cursor.execute("SELECT COUNT(*) as total FROM graph_nodes")
|
|
@@ -1115,30 +1147,40 @@ async def search_memories(request: SearchRequest):
|
|
|
1115
1147
|
@app.get("/api/profiles")
|
|
1116
1148
|
async def list_profiles():
|
|
1117
1149
|
"""
|
|
1118
|
-
List available memory profiles.
|
|
1150
|
+
List available memory profiles (column-based).
|
|
1119
1151
|
|
|
1120
1152
|
Returns:
|
|
1121
|
-
- profiles: List of
|
|
1153
|
+
- profiles: List of profiles with memory counts
|
|
1122
1154
|
- active_profile: Currently active profile
|
|
1123
1155
|
- total_profiles: Profile count
|
|
1124
1156
|
"""
|
|
1125
1157
|
try:
|
|
1126
|
-
|
|
1158
|
+
config_file = MEMORY_DIR / "profiles.json"
|
|
1159
|
+
if config_file.exists():
|
|
1160
|
+
with open(config_file, 'r') as f:
|
|
1161
|
+
config = json.load(f)
|
|
1162
|
+
else:
|
|
1163
|
+
config = {'profiles': {'default': {'name': 'default', 'description': 'Default memory profile'}}, 'active_profile': 'default'}
|
|
1127
1164
|
|
|
1165
|
+
active = config.get('active_profile', 'default')
|
|
1128
1166
|
profiles = []
|
|
1129
|
-
for profile_dir in PROFILES_DIR.iterdir():
|
|
1130
|
-
if profile_dir.is_dir():
|
|
1131
|
-
db_file = profile_dir / "memory.db"
|
|
1132
|
-
if db_file.exists():
|
|
1133
|
-
profiles.append({
|
|
1134
|
-
"name": profile_dir.name,
|
|
1135
|
-
"path": str(profile_dir),
|
|
1136
|
-
"size_mb": round(db_file.stat().st_size / (1024 * 1024), 2),
|
|
1137
|
-
"modified": datetime.fromtimestamp(db_file.stat().st_mtime).isoformat()
|
|
1138
|
-
})
|
|
1139
1167
|
|
|
1140
|
-
|
|
1141
|
-
|
|
1168
|
+
conn = get_db_connection()
|
|
1169
|
+
cursor = conn.cursor()
|
|
1170
|
+
|
|
1171
|
+
for name, info in config.get('profiles', {}).items():
|
|
1172
|
+
cursor.execute("SELECT COUNT(*) FROM memories WHERE profile = ?", (name,))
|
|
1173
|
+
count = cursor.fetchone()[0]
|
|
1174
|
+
profiles.append({
|
|
1175
|
+
"name": name,
|
|
1176
|
+
"description": info.get('description', ''),
|
|
1177
|
+
"memory_count": count,
|
|
1178
|
+
"created_at": info.get('created_at', ''),
|
|
1179
|
+
"last_used": info.get('last_used', ''),
|
|
1180
|
+
"is_active": name == active
|
|
1181
|
+
})
|
|
1182
|
+
|
|
1183
|
+
conn.close()
|
|
1142
1184
|
|
|
1143
1185
|
return {
|
|
1144
1186
|
"profiles": profiles,
|
|
@@ -1153,7 +1195,7 @@ async def list_profiles():
|
|
|
1153
1195
|
@app.post("/api/profiles/{name}/switch")
|
|
1154
1196
|
async def switch_profile(name: str):
|
|
1155
1197
|
"""
|
|
1156
|
-
Switch active memory profile.
|
|
1198
|
+
Switch active memory profile (column-based, instant).
|
|
1157
1199
|
|
|
1158
1200
|
Parameters:
|
|
1159
1201
|
- name: Profile name to switch to
|
|
@@ -1161,7 +1203,8 @@ async def switch_profile(name: str):
|
|
|
1161
1203
|
Returns:
|
|
1162
1204
|
- success: Switch status
|
|
1163
1205
|
- active_profile: New active profile
|
|
1164
|
-
-
|
|
1206
|
+
- previous_profile: Previously active profile
|
|
1207
|
+
- memory_count: Memories in new profile
|
|
1165
1208
|
"""
|
|
1166
1209
|
try:
|
|
1167
1210
|
if not validate_profile_name(name):
|
|
@@ -1170,20 +1213,48 @@ async def switch_profile(name: str):
|
|
|
1170
1213
|
detail="Invalid profile name. Use alphanumeric, underscore, or hyphen only."
|
|
1171
1214
|
)
|
|
1172
1215
|
|
|
1173
|
-
|
|
1216
|
+
config_file = MEMORY_DIR / "profiles.json"
|
|
1217
|
+
if config_file.exists():
|
|
1218
|
+
with open(config_file, 'r') as f:
|
|
1219
|
+
config = json.load(f)
|
|
1220
|
+
else:
|
|
1221
|
+
config = {'profiles': {'default': {'name': 'default', 'description': 'Default memory profile'}}, 'active_profile': 'default'}
|
|
1174
1222
|
|
|
1175
|
-
if not
|
|
1223
|
+
if name not in config.get('profiles', {}):
|
|
1176
1224
|
raise HTTPException(
|
|
1177
1225
|
status_code=404,
|
|
1178
|
-
detail=f"Profile '{name}' not found"
|
|
1226
|
+
detail=f"Profile '{name}' not found. Available: {', '.join(config.get('profiles', {}).keys())}"
|
|
1179
1227
|
)
|
|
1180
1228
|
|
|
1181
|
-
|
|
1182
|
-
|
|
1229
|
+
previous = config.get('active_profile', 'default')
|
|
1230
|
+
config['active_profile'] = name
|
|
1231
|
+
config['profiles'][name]['last_used'] = datetime.now().isoformat()
|
|
1232
|
+
|
|
1233
|
+
with open(config_file, 'w') as f:
|
|
1234
|
+
json.dump(config, f, indent=2)
|
|
1235
|
+
|
|
1236
|
+
# Get memory count for new profile
|
|
1237
|
+
conn = get_db_connection()
|
|
1238
|
+
cursor = conn.cursor()
|
|
1239
|
+
cursor.execute("SELECT COUNT(*) FROM memories WHERE profile = ?", (name,))
|
|
1240
|
+
count = cursor.fetchone()[0]
|
|
1241
|
+
conn.close()
|
|
1242
|
+
|
|
1243
|
+
# Broadcast profile switch to WebSocket clients
|
|
1244
|
+
await manager.broadcast({
|
|
1245
|
+
"type": "profile_switched",
|
|
1246
|
+
"profile": name,
|
|
1247
|
+
"previous": previous,
|
|
1248
|
+
"memory_count": count,
|
|
1249
|
+
"timestamp": datetime.now().isoformat()
|
|
1250
|
+
})
|
|
1251
|
+
|
|
1183
1252
|
return {
|
|
1184
1253
|
"success": True,
|
|
1185
1254
|
"active_profile": name,
|
|
1186
|
-
"
|
|
1255
|
+
"previous_profile": previous,
|
|
1256
|
+
"memory_count": count,
|
|
1257
|
+
"message": f"Switched to profile '{name}' ({count} memories). Changes take effect immediately."
|
|
1187
1258
|
}
|
|
1188
1259
|
|
|
1189
1260
|
except HTTPException:
|
|
@@ -1192,6 +1263,97 @@ async def switch_profile(name: str):
|
|
|
1192
1263
|
raise HTTPException(status_code=500, detail=f"Profile switch error: {str(e)}")
|
|
1193
1264
|
|
|
1194
1265
|
|
|
1266
|
+
@app.post("/api/profiles/create")
|
|
1267
|
+
async def create_profile(body: ProfileSwitch):
|
|
1268
|
+
"""
|
|
1269
|
+
Create a new memory profile.
|
|
1270
|
+
|
|
1271
|
+
Parameters:
|
|
1272
|
+
- profile_name: Name for the new profile
|
|
1273
|
+
|
|
1274
|
+
Returns:
|
|
1275
|
+
- success: Creation status
|
|
1276
|
+
- profile: Created profile name
|
|
1277
|
+
"""
|
|
1278
|
+
try:
|
|
1279
|
+
name = body.profile_name
|
|
1280
|
+
if not validate_profile_name(name):
|
|
1281
|
+
raise HTTPException(status_code=400, detail="Invalid profile name")
|
|
1282
|
+
|
|
1283
|
+
config_file = MEMORY_DIR / "profiles.json"
|
|
1284
|
+
if config_file.exists():
|
|
1285
|
+
with open(config_file, 'r') as f:
|
|
1286
|
+
config = json.load(f)
|
|
1287
|
+
else:
|
|
1288
|
+
config = {'profiles': {'default': {'name': 'default', 'description': 'Default memory profile'}}, 'active_profile': 'default'}
|
|
1289
|
+
|
|
1290
|
+
if name in config.get('profiles', {}):
|
|
1291
|
+
raise HTTPException(status_code=409, detail=f"Profile '{name}' already exists")
|
|
1292
|
+
|
|
1293
|
+
config['profiles'][name] = {
|
|
1294
|
+
'name': name,
|
|
1295
|
+
'description': f'Memory profile: {name}',
|
|
1296
|
+
'created_at': datetime.now().isoformat(),
|
|
1297
|
+
'last_used': None
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
with open(config_file, 'w') as f:
|
|
1301
|
+
json.dump(config, f, indent=2)
|
|
1302
|
+
|
|
1303
|
+
return {
|
|
1304
|
+
"success": True,
|
|
1305
|
+
"profile": name,
|
|
1306
|
+
"message": f"Profile '{name}' created"
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
except HTTPException:
|
|
1310
|
+
raise
|
|
1311
|
+
except Exception as e:
|
|
1312
|
+
raise HTTPException(status_code=500, detail=f"Profile create error: {str(e)}")
|
|
1313
|
+
|
|
1314
|
+
|
|
1315
|
+
@app.delete("/api/profiles/{name}")
|
|
1316
|
+
async def delete_profile(name: str):
|
|
1317
|
+
"""
|
|
1318
|
+
Delete a profile. Moves its memories to 'default'.
|
|
1319
|
+
"""
|
|
1320
|
+
try:
|
|
1321
|
+
if name == 'default':
|
|
1322
|
+
raise HTTPException(status_code=400, detail="Cannot delete 'default' profile")
|
|
1323
|
+
|
|
1324
|
+
config_file = MEMORY_DIR / "profiles.json"
|
|
1325
|
+
with open(config_file, 'r') as f:
|
|
1326
|
+
config = json.load(f)
|
|
1327
|
+
|
|
1328
|
+
if name not in config.get('profiles', {}):
|
|
1329
|
+
raise HTTPException(status_code=404, detail=f"Profile '{name}' not found")
|
|
1330
|
+
|
|
1331
|
+
if config.get('active_profile') == name:
|
|
1332
|
+
raise HTTPException(status_code=400, detail="Cannot delete active profile. Switch first.")
|
|
1333
|
+
|
|
1334
|
+
# Move memories to default
|
|
1335
|
+
conn = get_db_connection()
|
|
1336
|
+
cursor = conn.cursor()
|
|
1337
|
+
cursor.execute("UPDATE memories SET profile = 'default' WHERE profile = ?", (name,))
|
|
1338
|
+
moved = cursor.rowcount
|
|
1339
|
+
conn.commit()
|
|
1340
|
+
conn.close()
|
|
1341
|
+
|
|
1342
|
+
del config['profiles'][name]
|
|
1343
|
+
with open(config_file, 'w') as f:
|
|
1344
|
+
json.dump(config, f, indent=2)
|
|
1345
|
+
|
|
1346
|
+
return {
|
|
1347
|
+
"success": True,
|
|
1348
|
+
"message": f"Profile '{name}' deleted. {moved} memories moved to 'default'."
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
except HTTPException:
|
|
1352
|
+
raise
|
|
1353
|
+
except Exception as e:
|
|
1354
|
+
raise HTTPException(status_code=500, detail=f"Profile delete error: {str(e)}")
|
|
1355
|
+
|
|
1356
|
+
|
|
1195
1357
|
# ============================================================================
|
|
1196
1358
|
# API Endpoints - Import/Export
|
|
1197
1359
|
# ============================================================================
|
|
@@ -1366,6 +1528,141 @@ async def import_memories(file: UploadFile = File(...)):
|
|
|
1366
1528
|
raise HTTPException(status_code=500, detail=f"Import error: {str(e)}")
|
|
1367
1529
|
|
|
1368
1530
|
|
|
1531
|
+
# ============================================================================
|
|
1532
|
+
# API Endpoints - Backup Management
|
|
1533
|
+
# ============================================================================
|
|
1534
|
+
|
|
1535
|
+
class BackupConfigRequest(BaseModel):
|
|
1536
|
+
"""Backup configuration update request."""
|
|
1537
|
+
interval_hours: Optional[int] = Field(None, ge=1, le=8760)
|
|
1538
|
+
max_backups: Optional[int] = Field(None, ge=1, le=100)
|
|
1539
|
+
enabled: Optional[bool] = None
|
|
1540
|
+
|
|
1541
|
+
|
|
1542
|
+
@app.get("/api/backup/status")
|
|
1543
|
+
async def backup_status():
|
|
1544
|
+
"""
|
|
1545
|
+
Get auto-backup system status.
|
|
1546
|
+
|
|
1547
|
+
Returns:
|
|
1548
|
+
- enabled: Whether auto-backup is active
|
|
1549
|
+
- interval_display: Human-readable interval
|
|
1550
|
+
- last_backup: Timestamp of last backup
|
|
1551
|
+
- next_backup: When next backup is due
|
|
1552
|
+
- backup_count: Number of existing backups
|
|
1553
|
+
- total_size_mb: Total backup storage used
|
|
1554
|
+
"""
|
|
1555
|
+
try:
|
|
1556
|
+
from auto_backup import AutoBackup
|
|
1557
|
+
backup = AutoBackup()
|
|
1558
|
+
return backup.get_status()
|
|
1559
|
+
except ImportError:
|
|
1560
|
+
raise HTTPException(
|
|
1561
|
+
status_code=501,
|
|
1562
|
+
detail="Auto-backup module not installed. Update SuperLocalMemory to v2.4.0+."
|
|
1563
|
+
)
|
|
1564
|
+
except Exception as e:
|
|
1565
|
+
raise HTTPException(status_code=500, detail=f"Backup status error: {str(e)}")
|
|
1566
|
+
|
|
1567
|
+
|
|
1568
|
+
@app.post("/api/backup/create")
|
|
1569
|
+
async def backup_create():
|
|
1570
|
+
"""
|
|
1571
|
+
Create a manual backup of memory.db immediately.
|
|
1572
|
+
|
|
1573
|
+
Returns:
|
|
1574
|
+
- success: Whether backup was created
|
|
1575
|
+
- filename: Name of the backup file
|
|
1576
|
+
- status: Updated backup system status
|
|
1577
|
+
"""
|
|
1578
|
+
try:
|
|
1579
|
+
from auto_backup import AutoBackup
|
|
1580
|
+
backup = AutoBackup()
|
|
1581
|
+
filename = backup.create_backup(label='manual')
|
|
1582
|
+
|
|
1583
|
+
if filename:
|
|
1584
|
+
return {
|
|
1585
|
+
"success": True,
|
|
1586
|
+
"filename": filename,
|
|
1587
|
+
"message": f"Backup created: {filename}",
|
|
1588
|
+
"status": backup.get_status()
|
|
1589
|
+
}
|
|
1590
|
+
else:
|
|
1591
|
+
return {
|
|
1592
|
+
"success": False,
|
|
1593
|
+
"message": "Backup failed — database may not exist",
|
|
1594
|
+
"status": backup.get_status()
|
|
1595
|
+
}
|
|
1596
|
+
except ImportError:
|
|
1597
|
+
raise HTTPException(
|
|
1598
|
+
status_code=501,
|
|
1599
|
+
detail="Auto-backup module not installed. Update SuperLocalMemory to v2.4.0+."
|
|
1600
|
+
)
|
|
1601
|
+
except Exception as e:
|
|
1602
|
+
raise HTTPException(status_code=500, detail=f"Backup create error: {str(e)}")
|
|
1603
|
+
|
|
1604
|
+
|
|
1605
|
+
@app.post("/api/backup/configure")
|
|
1606
|
+
async def backup_configure(request: BackupConfigRequest):
|
|
1607
|
+
"""
|
|
1608
|
+
Update auto-backup configuration.
|
|
1609
|
+
|
|
1610
|
+
Request body (all optional):
|
|
1611
|
+
- interval_hours: Hours between backups (24=daily, 168=weekly)
|
|
1612
|
+
- max_backups: Maximum backup files to retain
|
|
1613
|
+
- enabled: Enable/disable auto-backup
|
|
1614
|
+
|
|
1615
|
+
Returns:
|
|
1616
|
+
- Updated backup status
|
|
1617
|
+
"""
|
|
1618
|
+
try:
|
|
1619
|
+
from auto_backup import AutoBackup
|
|
1620
|
+
backup = AutoBackup()
|
|
1621
|
+
result = backup.configure(
|
|
1622
|
+
interval_hours=request.interval_hours,
|
|
1623
|
+
max_backups=request.max_backups,
|
|
1624
|
+
enabled=request.enabled
|
|
1625
|
+
)
|
|
1626
|
+
return {
|
|
1627
|
+
"success": True,
|
|
1628
|
+
"message": "Backup configuration updated",
|
|
1629
|
+
"status": result
|
|
1630
|
+
}
|
|
1631
|
+
except ImportError:
|
|
1632
|
+
raise HTTPException(
|
|
1633
|
+
status_code=501,
|
|
1634
|
+
detail="Auto-backup module not installed. Update SuperLocalMemory to v2.4.0+."
|
|
1635
|
+
)
|
|
1636
|
+
except Exception as e:
|
|
1637
|
+
raise HTTPException(status_code=500, detail=f"Backup configure error: {str(e)}")
|
|
1638
|
+
|
|
1639
|
+
|
|
1640
|
+
@app.get("/api/backup/list")
|
|
1641
|
+
async def backup_list():
|
|
1642
|
+
"""
|
|
1643
|
+
List all available backups.
|
|
1644
|
+
|
|
1645
|
+
Returns:
|
|
1646
|
+
- backups: List of backup files with metadata (filename, size, age, created)
|
|
1647
|
+
- count: Total number of backups
|
|
1648
|
+
"""
|
|
1649
|
+
try:
|
|
1650
|
+
from auto_backup import AutoBackup
|
|
1651
|
+
backup = AutoBackup()
|
|
1652
|
+
backups = backup.list_backups()
|
|
1653
|
+
return {
|
|
1654
|
+
"backups": backups,
|
|
1655
|
+
"count": len(backups)
|
|
1656
|
+
}
|
|
1657
|
+
except ImportError:
|
|
1658
|
+
raise HTTPException(
|
|
1659
|
+
status_code=501,
|
|
1660
|
+
detail="Auto-backup module not installed. Update SuperLocalMemory to v2.4.0+."
|
|
1661
|
+
)
|
|
1662
|
+
except Exception as e:
|
|
1663
|
+
raise HTTPException(status_code=500, detail=f"Backup list error: {str(e)}")
|
|
1664
|
+
|
|
1665
|
+
|
|
1369
1666
|
# ============================================================================
|
|
1370
1667
|
# WebSocket Endpoint - Real-Time Updates
|
|
1371
1668
|
# ============================================================================
|