superlocalmemory 3.0.28 → 3.0.30
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 +9 -0
- package/package.json +1 -1
- package/pyproject.toml +4 -1
- package/src/superlocalmemory/cli/commands.py +12 -2
- package/src/superlocalmemory/mcp/tools_core.py +8 -0
- package/src/superlocalmemory/server/routes/helpers.py +163 -0
- package/src/superlocalmemory/server/routes/profiles.py +60 -64
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,15 @@ SuperLocalMemory V3 - Intelligent local memory system for AI coding assistants.
|
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
+
## [3.0.30] - 2026-03-21
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- Profile switching and display uses correct identifiers
|
|
23
|
+
- Profile sync across CLI, Dashboard, and MCP — all entry points now see the same profiles
|
|
24
|
+
- Profile switching now persists correctly across restarts
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
19
28
|
## [2.8.6] - 2026-03-06
|
|
20
29
|
|
|
21
30
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superlocalmemory",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.30",
|
|
4
4
|
"description": "Information-geometric agent memory with mathematical guarantees. 4-channel retrieval, Fisher-Rao similarity, zero-LLM mode, EU AI Act compliant. Works with Claude, Cursor, Windsurf, and 17+ AI tools.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-memory",
|
package/pyproject.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "superlocalmemory"
|
|
3
|
-
version = "3.0.
|
|
3
|
+
version = "3.0.30"
|
|
4
4
|
description = "Information-geometric agent memory with mathematical guarantees"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = {text = "MIT"}
|
|
@@ -23,6 +23,9 @@ dependencies = [
|
|
|
23
23
|
"rank-bm25>=0.2.2",
|
|
24
24
|
"vadersentiment>=3.3.2",
|
|
25
25
|
"einops>=0.8.2",
|
|
26
|
+
"fastapi[all]>=0.135.1",
|
|
27
|
+
"uvicorn>=0.42.0",
|
|
28
|
+
"websockets>=16.0",
|
|
26
29
|
]
|
|
27
30
|
|
|
28
31
|
[project.optional-dependencies]
|
|
@@ -699,10 +699,17 @@ def cmd_dashboard(args: Namespace) -> None:
|
|
|
699
699
|
|
|
700
700
|
|
|
701
701
|
def cmd_profile(args: Namespace) -> None:
|
|
702
|
-
"""Profile management (list, switch, create).
|
|
702
|
+
"""Profile management (list, switch, create).
|
|
703
|
+
|
|
704
|
+
Writes to BOTH SQLite and profiles.json so CLI, Dashboard, and
|
|
705
|
+
MCP all see the same profiles.
|
|
706
|
+
"""
|
|
703
707
|
from superlocalmemory.core.config import SLMConfig
|
|
704
708
|
from superlocalmemory.storage.database import DatabaseManager
|
|
705
709
|
from superlocalmemory.storage import schema
|
|
710
|
+
from superlocalmemory.server.routes.helpers import (
|
|
711
|
+
ensure_profile_in_json, set_active_profile_everywhere,
|
|
712
|
+
)
|
|
706
713
|
|
|
707
714
|
config = SLMConfig.load()
|
|
708
715
|
db = DatabaseManager(config.db_path)
|
|
@@ -721,6 +728,7 @@ def cmd_profile(args: Namespace) -> None:
|
|
|
721
728
|
{"command": "slm profile switch <name> --json", "description": "Switch profile"},
|
|
722
729
|
])
|
|
723
730
|
elif args.action == "switch":
|
|
731
|
+
set_active_profile_everywhere(args.name)
|
|
724
732
|
config.active_profile = args.name
|
|
725
733
|
config.save()
|
|
726
734
|
json_print("profile", data={"action": "switched", "profile": args.name})
|
|
@@ -729,6 +737,7 @@ def cmd_profile(args: Namespace) -> None:
|
|
|
729
737
|
"INSERT OR IGNORE INTO profiles (profile_id, name) VALUES (?, ?)",
|
|
730
738
|
(args.name, args.name),
|
|
731
739
|
)
|
|
740
|
+
ensure_profile_in_json(args.name)
|
|
732
741
|
json_print("profile", data={"action": "created", "profile": args.name},
|
|
733
742
|
next_actions=[
|
|
734
743
|
{"command": f"slm profile switch {args.name} --json",
|
|
@@ -743,13 +752,14 @@ def cmd_profile(args: Namespace) -> None:
|
|
|
743
752
|
d = dict(r)
|
|
744
753
|
print(f" - {d['profile_id']}: {d.get('name', '')}")
|
|
745
754
|
elif args.action == "switch":
|
|
755
|
+
set_active_profile_everywhere(args.name)
|
|
746
756
|
config.active_profile = args.name
|
|
747
757
|
config.save()
|
|
748
758
|
print(f"Switched to profile: {args.name}")
|
|
749
759
|
elif args.action == "create":
|
|
750
|
-
from superlocalmemory.storage.models import _new_id
|
|
751
760
|
db.execute(
|
|
752
761
|
"INSERT OR IGNORE INTO profiles (profile_id, name) VALUES (?, ?)",
|
|
753
762
|
(args.name, args.name),
|
|
754
763
|
)
|
|
764
|
+
ensure_profile_in_json(args.name)
|
|
755
765
|
print(f"Created profile: {args.name}")
|
|
@@ -201,6 +201,14 @@ def register_core_tools(server, get_engine: Callable) -> None:
|
|
|
201
201
|
engine = get_engine()
|
|
202
202
|
old = engine.profile_id
|
|
203
203
|
engine.profile_id = profile_id
|
|
204
|
+
|
|
205
|
+
# Persist to both config stores so CLI and Dashboard stay in sync
|
|
206
|
+
from superlocalmemory.server.routes.helpers import (
|
|
207
|
+
ensure_profile_in_db, set_active_profile_everywhere,
|
|
208
|
+
)
|
|
209
|
+
ensure_profile_in_db(profile_id)
|
|
210
|
+
set_active_profile_everywhere(profile_id)
|
|
211
|
+
|
|
204
212
|
return {
|
|
205
213
|
"success": True,
|
|
206
214
|
"previous_profile": old,
|
|
@@ -78,6 +78,169 @@ def validate_profile_name(name: str) -> bool:
|
|
|
78
78
|
return bool(re.match(r'^[a-zA-Z0-9_-]+$', name))
|
|
79
79
|
|
|
80
80
|
|
|
81
|
+
# ============================================================================
|
|
82
|
+
# Profile Sync — SQLite as single source of truth
|
|
83
|
+
# ============================================================================
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def ensure_profile_in_db(name: str, description: str = "") -> None:
|
|
87
|
+
"""Ensure a profile row exists in SQLite (idempotent)."""
|
|
88
|
+
if not DB_PATH.exists():
|
|
89
|
+
return
|
|
90
|
+
conn = sqlite3.connect(str(DB_PATH))
|
|
91
|
+
try:
|
|
92
|
+
conn.execute("PRAGMA foreign_keys=ON")
|
|
93
|
+
conn.execute(
|
|
94
|
+
"INSERT OR IGNORE INTO profiles (profile_id, name, description) "
|
|
95
|
+
"VALUES (?, ?, ?)",
|
|
96
|
+
(name, name, description or f"Memory profile: {name}"),
|
|
97
|
+
)
|
|
98
|
+
conn.commit()
|
|
99
|
+
finally:
|
|
100
|
+
conn.close()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def ensure_profile_in_json(name: str, description: str = "") -> None:
|
|
104
|
+
"""Ensure a profile entry exists in profiles.json (idempotent)."""
|
|
105
|
+
from datetime import datetime
|
|
106
|
+
config_file = MEMORY_DIR / "profiles.json"
|
|
107
|
+
config = _load_profiles_json()
|
|
108
|
+
profiles = config.get('profiles', {})
|
|
109
|
+
if name not in profiles:
|
|
110
|
+
profiles[name] = {
|
|
111
|
+
'name': name,
|
|
112
|
+
'description': description or f'Memory profile: {name}',
|
|
113
|
+
'created_at': datetime.now().isoformat(),
|
|
114
|
+
'last_used': None,
|
|
115
|
+
}
|
|
116
|
+
config['profiles'] = profiles
|
|
117
|
+
_save_profiles_json(config)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def sync_profiles() -> list[dict]:
|
|
121
|
+
"""Reconcile SQLite and profiles.json. Returns merged profile list.
|
|
122
|
+
|
|
123
|
+
SQLite is the source of truth. Uses ``profile_id`` (not ``name``)
|
|
124
|
+
as the canonical key because profile_id is the PK referenced by
|
|
125
|
+
every FK in the database.
|
|
126
|
+
"""
|
|
127
|
+
db_profiles = _get_db_profiles()
|
|
128
|
+
json_config = _load_profiles_json()
|
|
129
|
+
json_profiles = json_config.get('profiles', {})
|
|
130
|
+
|
|
131
|
+
# profile_id is the canonical key (PK in SQLite, FK target everywhere)
|
|
132
|
+
db_ids = {p['profile_id'] for p in db_profiles}
|
|
133
|
+
json_keys = set(json_profiles.keys())
|
|
134
|
+
|
|
135
|
+
changed = False
|
|
136
|
+
|
|
137
|
+
# JSON-only → add to SQLite (fixes Dashboard-created profiles)
|
|
138
|
+
for key in json_keys - db_ids:
|
|
139
|
+
ensure_profile_in_db(key, json_profiles[key].get('description', ''))
|
|
140
|
+
|
|
141
|
+
# SQLite-only → add to profiles.json (fixes CLI-created profiles)
|
|
142
|
+
for pid in db_ids - json_keys:
|
|
143
|
+
db_entry = next(p for p in db_profiles if p['profile_id'] == pid)
|
|
144
|
+
json_profiles[pid] = {
|
|
145
|
+
'name': pid,
|
|
146
|
+
'description': db_entry.get('description', ''),
|
|
147
|
+
'created_at': db_entry.get('created_at', ''),
|
|
148
|
+
'last_used': db_entry.get('last_used'),
|
|
149
|
+
}
|
|
150
|
+
changed = True
|
|
151
|
+
|
|
152
|
+
if changed:
|
|
153
|
+
json_config['profiles'] = json_profiles
|
|
154
|
+
_save_profiles_json(json_config)
|
|
155
|
+
|
|
156
|
+
# Return merged list from SQLite (now authoritative)
|
|
157
|
+
return _get_db_profiles()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def set_active_profile_everywhere(name: str) -> None:
|
|
161
|
+
"""Persist the active profile to BOTH profiles.json and config.json."""
|
|
162
|
+
# profiles.json
|
|
163
|
+
config = _load_profiles_json()
|
|
164
|
+
config['active_profile'] = name
|
|
165
|
+
_save_profiles_json(config)
|
|
166
|
+
|
|
167
|
+
# config.json (read by Engine/MCP on startup)
|
|
168
|
+
config_path = MEMORY_DIR / "config.json"
|
|
169
|
+
cfg = {}
|
|
170
|
+
if config_path.exists():
|
|
171
|
+
try:
|
|
172
|
+
cfg = json.loads(config_path.read_text())
|
|
173
|
+
except (json.JSONDecodeError, IOError):
|
|
174
|
+
pass
|
|
175
|
+
cfg['active_profile'] = name
|
|
176
|
+
config_path.write_text(json.dumps(cfg, indent=2))
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def delete_profile_from_db(name: str) -> None:
|
|
180
|
+
"""Delete a profile row from SQLite. ON DELETE CASCADE handles child rows."""
|
|
181
|
+
if not DB_PATH.exists():
|
|
182
|
+
return
|
|
183
|
+
conn = sqlite3.connect(str(DB_PATH))
|
|
184
|
+
try:
|
|
185
|
+
conn.execute("PRAGMA foreign_keys=ON")
|
|
186
|
+
conn.execute("DELETE FROM profiles WHERE profile_id = ?", (name,))
|
|
187
|
+
conn.commit()
|
|
188
|
+
finally:
|
|
189
|
+
conn.close()
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _get_db_profiles() -> list[dict]:
|
|
193
|
+
"""Read all profiles from SQLite."""
|
|
194
|
+
if not DB_PATH.exists():
|
|
195
|
+
return []
|
|
196
|
+
conn = sqlite3.connect(str(DB_PATH))
|
|
197
|
+
conn.row_factory = sqlite3.Row
|
|
198
|
+
try:
|
|
199
|
+
rows = conn.execute(
|
|
200
|
+
"SELECT profile_id, name, description, created_at, last_used "
|
|
201
|
+
"FROM profiles ORDER BY name"
|
|
202
|
+
).fetchall()
|
|
203
|
+
return [dict(r) for r in rows]
|
|
204
|
+
except sqlite3.OperationalError:
|
|
205
|
+
return []
|
|
206
|
+
finally:
|
|
207
|
+
conn.close()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _load_profiles_json() -> dict:
|
|
211
|
+
"""Load profiles.json config (Dashboard dict format)."""
|
|
212
|
+
config_file = MEMORY_DIR / "profiles.json"
|
|
213
|
+
if config_file.exists():
|
|
214
|
+
try:
|
|
215
|
+
with open(config_file, 'r') as f:
|
|
216
|
+
data = json.load(f)
|
|
217
|
+
# Handle ProfileManager array format → convert to dict format
|
|
218
|
+
if isinstance(data.get('profiles'), list):
|
|
219
|
+
converted = {}
|
|
220
|
+
for p in data['profiles']:
|
|
221
|
+
n = p.get('name', '')
|
|
222
|
+
if n:
|
|
223
|
+
converted[n] = p
|
|
224
|
+
data['profiles'] = converted
|
|
225
|
+
if 'active' in data and 'active_profile' not in data:
|
|
226
|
+
data['active_profile'] = data.pop('active')
|
|
227
|
+
return data
|
|
228
|
+
except (json.JSONDecodeError, IOError):
|
|
229
|
+
pass
|
|
230
|
+
return {
|
|
231
|
+
'profiles': {'default': {'name': 'default', 'description': 'Default memory profile'}},
|
|
232
|
+
'active_profile': 'default',
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _save_profiles_json(config: dict) -> None:
|
|
237
|
+
"""Save profiles.json config."""
|
|
238
|
+
MEMORY_DIR.mkdir(parents=True, exist_ok=True)
|
|
239
|
+
config_file = MEMORY_DIR / "profiles.json"
|
|
240
|
+
with open(config_file, 'w') as f:
|
|
241
|
+
json.dump(config, f, indent=2)
|
|
242
|
+
|
|
243
|
+
|
|
81
244
|
# ============================================================================
|
|
82
245
|
# Pydantic Models (shared across routes)
|
|
83
246
|
# ============================================================================
|
|
@@ -6,16 +6,21 @@
|
|
|
6
6
|
|
|
7
7
|
Routes: /api/profiles, /api/profiles/{name}/switch,
|
|
8
8
|
/api/profiles/create, DELETE /api/profiles/{name}
|
|
9
|
+
|
|
10
|
+
SQLite is the single source of truth for profiles. profiles.json
|
|
11
|
+
is kept in sync as a cache for backward compatibility.
|
|
9
12
|
"""
|
|
10
|
-
import json
|
|
11
13
|
import logging
|
|
12
14
|
from datetime import datetime
|
|
13
15
|
|
|
14
16
|
from fastapi import APIRouter, HTTPException
|
|
15
17
|
|
|
16
18
|
from .helpers import (
|
|
17
|
-
get_db_connection,
|
|
18
|
-
ProfileSwitch,
|
|
19
|
+
get_db_connection, validate_profile_name,
|
|
20
|
+
ProfileSwitch, DB_PATH,
|
|
21
|
+
sync_profiles, ensure_profile_in_db, ensure_profile_in_json,
|
|
22
|
+
set_active_profile_everywhere, delete_profile_from_db,
|
|
23
|
+
_load_profiles_json, _save_profiles_json,
|
|
19
24
|
)
|
|
20
25
|
|
|
21
26
|
logger = logging.getLogger("superlocalmemory.routes.profiles")
|
|
@@ -25,35 +30,11 @@ router = APIRouter()
|
|
|
25
30
|
ws_manager = None
|
|
26
31
|
|
|
27
32
|
|
|
28
|
-
def _load_profiles_config() -> dict:
|
|
29
|
-
"""Load profiles.json config."""
|
|
30
|
-
config_file = MEMORY_DIR / "profiles.json"
|
|
31
|
-
if config_file.exists():
|
|
32
|
-
try:
|
|
33
|
-
with open(config_file, 'r') as f:
|
|
34
|
-
return json.load(f)
|
|
35
|
-
except (json.JSONDecodeError, IOError):
|
|
36
|
-
pass
|
|
37
|
-
return {
|
|
38
|
-
'profiles': {'default': {'name': 'default', 'description': 'Default memory profile'}},
|
|
39
|
-
'active_profile': 'default',
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def _save_profiles_config(config: dict) -> None:
|
|
44
|
-
"""Save profiles.json config."""
|
|
45
|
-
MEMORY_DIR.mkdir(parents=True, exist_ok=True)
|
|
46
|
-
config_file = MEMORY_DIR / "profiles.json"
|
|
47
|
-
with open(config_file, 'w') as f:
|
|
48
|
-
json.dump(config, f, indent=2)
|
|
49
|
-
|
|
50
|
-
|
|
51
33
|
def _get_memory_count(profile: str) -> int:
|
|
52
|
-
"""Get memory count for a profile
|
|
34
|
+
"""Get memory count for a profile."""
|
|
53
35
|
try:
|
|
54
36
|
conn = get_db_connection()
|
|
55
37
|
cursor = conn.cursor()
|
|
56
|
-
# Try V3 table first
|
|
57
38
|
try:
|
|
58
39
|
cursor.execute(
|
|
59
40
|
"SELECT COUNT(*) FROM atomic_facts WHERE profile_id = ?", (profile,),
|
|
@@ -72,21 +53,24 @@ def _get_memory_count(profile: str) -> int:
|
|
|
72
53
|
|
|
73
54
|
@router.get("/api/profiles")
|
|
74
55
|
async def list_profiles():
|
|
75
|
-
"""List available memory profiles."""
|
|
56
|
+
"""List available memory profiles (synced from SQLite + profiles.json)."""
|
|
76
57
|
try:
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
58
|
+
merged = sync_profiles()
|
|
59
|
+
json_config = _load_profiles_json()
|
|
60
|
+
active = json_config.get('active_profile', 'default')
|
|
80
61
|
|
|
81
|
-
|
|
82
|
-
|
|
62
|
+
profiles = []
|
|
63
|
+
for p in merged:
|
|
64
|
+
# profile_id is the canonical key (PK, FK target, used by engine)
|
|
65
|
+
pid = p.get('profile_id', p.get('name', ''))
|
|
66
|
+
count = _get_memory_count(pid)
|
|
83
67
|
profiles.append({
|
|
84
|
-
"name":
|
|
85
|
-
"description":
|
|
68
|
+
"name": pid,
|
|
69
|
+
"description": p.get('description', ''),
|
|
86
70
|
"memory_count": count,
|
|
87
|
-
"created_at":
|
|
88
|
-
"last_used":
|
|
89
|
-
"is_active":
|
|
71
|
+
"created_at": p.get('created_at', ''),
|
|
72
|
+
"last_used": p.get('last_used', ''),
|
|
73
|
+
"is_active": pid == active,
|
|
90
74
|
})
|
|
91
75
|
|
|
92
76
|
return {
|
|
@@ -101,24 +85,29 @@ async def list_profiles():
|
|
|
101
85
|
|
|
102
86
|
@router.post("/api/profiles/{name}/switch")
|
|
103
87
|
async def switch_profile(name: str):
|
|
104
|
-
"""Switch active memory profile."""
|
|
88
|
+
"""Switch active memory profile (persists to both config stores)."""
|
|
105
89
|
try:
|
|
106
90
|
if not validate_profile_name(name):
|
|
107
91
|
raise HTTPException(status_code=400, detail="Invalid profile name.")
|
|
108
92
|
|
|
109
|
-
|
|
93
|
+
merged = sync_profiles()
|
|
94
|
+
merged_ids = {p.get('profile_id', p.get('name', '')) for p in merged}
|
|
110
95
|
|
|
111
|
-
if name not in
|
|
112
|
-
available = ', '.join(
|
|
96
|
+
if name not in merged_ids:
|
|
97
|
+
available = ', '.join(sorted(merged_ids))
|
|
113
98
|
raise HTTPException(
|
|
114
99
|
status_code=404,
|
|
115
100
|
detail=f"Profile '{name}' not found. Available: {available}",
|
|
116
101
|
)
|
|
117
102
|
|
|
118
|
-
previous =
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
103
|
+
previous = _load_profiles_json().get('active_profile', 'default')
|
|
104
|
+
set_active_profile_everywhere(name)
|
|
105
|
+
|
|
106
|
+
# Update last_used in profiles.json
|
|
107
|
+
json_config = _load_profiles_json()
|
|
108
|
+
if name in json_config.get('profiles', {}):
|
|
109
|
+
json_config['profiles'][name]['last_used'] = datetime.now().isoformat()
|
|
110
|
+
_save_profiles_json(json_config)
|
|
122
111
|
|
|
123
112
|
count = _get_memory_count(name)
|
|
124
113
|
|
|
@@ -143,22 +132,22 @@ async def switch_profile(name: str):
|
|
|
143
132
|
|
|
144
133
|
@router.post("/api/profiles/create")
|
|
145
134
|
async def create_profile(body: ProfileSwitch):
|
|
146
|
-
"""Create a new memory profile."""
|
|
135
|
+
"""Create a new memory profile (writes to BOTH SQLite and profiles.json)."""
|
|
147
136
|
try:
|
|
148
137
|
name = body.profile_name
|
|
149
138
|
if not validate_profile_name(name):
|
|
150
139
|
raise HTTPException(status_code=400, detail="Invalid profile name")
|
|
151
140
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
141
|
+
# Check both stores for duplicates
|
|
142
|
+
merged = sync_profiles()
|
|
143
|
+
merged_ids = {p.get('profile_id', p.get('name', '')) for p in merged}
|
|
144
|
+
if name in merged_ids:
|
|
155
145
|
raise HTTPException(status_code=409, detail=f"Profile '{name}' already exists")
|
|
156
146
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
_save_profiles_config(config)
|
|
147
|
+
# Write to BOTH stores atomically
|
|
148
|
+
desc = f'Memory profile: {name}'
|
|
149
|
+
ensure_profile_in_db(name, desc)
|
|
150
|
+
ensure_profile_in_json(name, desc)
|
|
162
151
|
|
|
163
152
|
return {"success": True, "profile": name, "message": f"Profile '{name}' created"}
|
|
164
153
|
|
|
@@ -175,16 +164,18 @@ async def delete_profile(name: str):
|
|
|
175
164
|
if name == 'default':
|
|
176
165
|
raise HTTPException(status_code=400, detail="Cannot delete 'default' profile")
|
|
177
166
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if name not in
|
|
167
|
+
merged = sync_profiles()
|
|
168
|
+
merged_ids = {p.get('profile_id', p.get('name', '')) for p in merged}
|
|
169
|
+
if name not in merged_ids:
|
|
181
170
|
raise HTTPException(status_code=404, detail=f"Profile '{name}' not found")
|
|
182
|
-
|
|
171
|
+
|
|
172
|
+
json_config = _load_profiles_json()
|
|
173
|
+
if json_config.get('active_profile') == name:
|
|
183
174
|
raise HTTPException(status_code=400, detail="Cannot delete active profile.")
|
|
184
175
|
|
|
176
|
+
# Move data to default before deleting (bypasses CASCADE)
|
|
185
177
|
conn = get_db_connection()
|
|
186
178
|
cursor = conn.cursor()
|
|
187
|
-
# Move memories to default (try V3 first, then V2)
|
|
188
179
|
moved = 0
|
|
189
180
|
try:
|
|
190
181
|
cursor.execute(
|
|
@@ -196,7 +187,7 @@ async def delete_profile(name: str):
|
|
|
196
187
|
pass
|
|
197
188
|
try:
|
|
198
189
|
cursor.execute(
|
|
199
|
-
"UPDATE memories SET
|
|
190
|
+
"UPDATE memories SET profile_id = 'default' WHERE profile_id = ?",
|
|
200
191
|
(name,),
|
|
201
192
|
)
|
|
202
193
|
moved += cursor.rowcount
|
|
@@ -205,8 +196,13 @@ async def delete_profile(name: str):
|
|
|
205
196
|
conn.commit()
|
|
206
197
|
conn.close()
|
|
207
198
|
|
|
208
|
-
|
|
209
|
-
|
|
199
|
+
# Delete from BOTH stores
|
|
200
|
+
delete_profile_from_db(name)
|
|
201
|
+
|
|
202
|
+
profiles = json_config.get('profiles', {})
|
|
203
|
+
profiles.pop(name, None)
|
|
204
|
+
json_config['profiles'] = profiles
|
|
205
|
+
_save_profiles_json(json_config)
|
|
210
206
|
|
|
211
207
|
return {
|
|
212
208
|
"success": True,
|