superlocalmemory 3.0.28 → 3.0.29
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 +8 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -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 +162 -0
- package/src/superlocalmemory/server/routes/profiles.py +56 -61
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,14 @@ SuperLocalMemory V3 - Intelligent local memory system for AI coding assistants.
|
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
+
## [3.0.29] - 2026-03-21
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- Profile sync across CLI, Dashboard, and MCP — all entry points now see the same profiles
|
|
23
|
+
- Profile switching now persists correctly across restarts
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
19
27
|
## [2.8.6] - 2026-03-06
|
|
20
28
|
|
|
21
29
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superlocalmemory",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.29",
|
|
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
|
@@ -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,168 @@ 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. Any profile in profiles.json
|
|
124
|
+
that's missing from SQLite is added to SQLite. Any profile in
|
|
125
|
+
SQLite that's missing from profiles.json is added to profiles.json.
|
|
126
|
+
"""
|
|
127
|
+
db_profiles = _get_db_profiles()
|
|
128
|
+
json_config = _load_profiles_json()
|
|
129
|
+
json_profiles = json_config.get('profiles', {})
|
|
130
|
+
|
|
131
|
+
db_names = {p['name'] for p in db_profiles}
|
|
132
|
+
json_names = set(json_profiles.keys())
|
|
133
|
+
|
|
134
|
+
changed = False
|
|
135
|
+
|
|
136
|
+
# JSON-only → add to SQLite (fixes Dashboard-created profiles)
|
|
137
|
+
for name in json_names - db_names:
|
|
138
|
+
ensure_profile_in_db(name, json_profiles[name].get('description', ''))
|
|
139
|
+
|
|
140
|
+
# SQLite-only → add to profiles.json (fixes CLI-created profiles)
|
|
141
|
+
for name in db_names - json_names:
|
|
142
|
+
db_entry = next(p for p in db_profiles if p['name'] == name)
|
|
143
|
+
json_profiles[name] = {
|
|
144
|
+
'name': name,
|
|
145
|
+
'description': db_entry.get('description', ''),
|
|
146
|
+
'created_at': db_entry.get('created_at', ''),
|
|
147
|
+
'last_used': db_entry.get('last_used'),
|
|
148
|
+
}
|
|
149
|
+
changed = True
|
|
150
|
+
|
|
151
|
+
if changed:
|
|
152
|
+
json_config['profiles'] = json_profiles
|
|
153
|
+
_save_profiles_json(json_config)
|
|
154
|
+
|
|
155
|
+
# Return merged list from SQLite (now authoritative)
|
|
156
|
+
return _get_db_profiles()
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def set_active_profile_everywhere(name: str) -> None:
|
|
160
|
+
"""Persist the active profile to BOTH profiles.json and config.json."""
|
|
161
|
+
# profiles.json
|
|
162
|
+
config = _load_profiles_json()
|
|
163
|
+
config['active_profile'] = name
|
|
164
|
+
_save_profiles_json(config)
|
|
165
|
+
|
|
166
|
+
# config.json (read by Engine/MCP on startup)
|
|
167
|
+
config_path = MEMORY_DIR / "config.json"
|
|
168
|
+
cfg = {}
|
|
169
|
+
if config_path.exists():
|
|
170
|
+
try:
|
|
171
|
+
cfg = json.loads(config_path.read_text())
|
|
172
|
+
except (json.JSONDecodeError, IOError):
|
|
173
|
+
pass
|
|
174
|
+
cfg['active_profile'] = name
|
|
175
|
+
config_path.write_text(json.dumps(cfg, indent=2))
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def delete_profile_from_db(name: str) -> None:
|
|
179
|
+
"""Delete a profile row from SQLite. ON DELETE CASCADE handles child rows."""
|
|
180
|
+
if not DB_PATH.exists():
|
|
181
|
+
return
|
|
182
|
+
conn = sqlite3.connect(str(DB_PATH))
|
|
183
|
+
try:
|
|
184
|
+
conn.execute("PRAGMA foreign_keys=ON")
|
|
185
|
+
conn.execute("DELETE FROM profiles WHERE profile_id = ?", (name,))
|
|
186
|
+
conn.commit()
|
|
187
|
+
finally:
|
|
188
|
+
conn.close()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _get_db_profiles() -> list[dict]:
|
|
192
|
+
"""Read all profiles from SQLite."""
|
|
193
|
+
if not DB_PATH.exists():
|
|
194
|
+
return []
|
|
195
|
+
conn = sqlite3.connect(str(DB_PATH))
|
|
196
|
+
conn.row_factory = sqlite3.Row
|
|
197
|
+
try:
|
|
198
|
+
rows = conn.execute(
|
|
199
|
+
"SELECT profile_id, name, description, created_at, last_used "
|
|
200
|
+
"FROM profiles ORDER BY name"
|
|
201
|
+
).fetchall()
|
|
202
|
+
return [dict(r) for r in rows]
|
|
203
|
+
except sqlite3.OperationalError:
|
|
204
|
+
return []
|
|
205
|
+
finally:
|
|
206
|
+
conn.close()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _load_profiles_json() -> dict:
|
|
210
|
+
"""Load profiles.json config (Dashboard dict format)."""
|
|
211
|
+
config_file = MEMORY_DIR / "profiles.json"
|
|
212
|
+
if config_file.exists():
|
|
213
|
+
try:
|
|
214
|
+
with open(config_file, 'r') as f:
|
|
215
|
+
data = json.load(f)
|
|
216
|
+
# Handle ProfileManager array format → convert to dict format
|
|
217
|
+
if isinstance(data.get('profiles'), list):
|
|
218
|
+
converted = {}
|
|
219
|
+
for p in data['profiles']:
|
|
220
|
+
n = p.get('name', '')
|
|
221
|
+
if n:
|
|
222
|
+
converted[n] = p
|
|
223
|
+
data['profiles'] = converted
|
|
224
|
+
if 'active' in data and 'active_profile' not in data:
|
|
225
|
+
data['active_profile'] = data.pop('active')
|
|
226
|
+
return data
|
|
227
|
+
except (json.JSONDecodeError, IOError):
|
|
228
|
+
pass
|
|
229
|
+
return {
|
|
230
|
+
'profiles': {'default': {'name': 'default', 'description': 'Default memory profile'}},
|
|
231
|
+
'active_profile': 'default',
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _save_profiles_json(config: dict) -> None:
|
|
236
|
+
"""Save profiles.json config."""
|
|
237
|
+
MEMORY_DIR.mkdir(parents=True, exist_ok=True)
|
|
238
|
+
config_file = MEMORY_DIR / "profiles.json"
|
|
239
|
+
with open(config_file, 'w') as f:
|
|
240
|
+
json.dump(config, f, indent=2)
|
|
241
|
+
|
|
242
|
+
|
|
81
243
|
# ============================================================================
|
|
82
244
|
# Pydantic Models (shared across routes)
|
|
83
245
|
# ============================================================================
|
|
@@ -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,20 +53,22 @@ 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
|
-
|
|
62
|
+
profiles = []
|
|
63
|
+
for p in merged:
|
|
64
|
+
name = p.get('name', p.get('profile_id', ''))
|
|
82
65
|
count = _get_memory_count(name)
|
|
83
66
|
profiles.append({
|
|
84
67
|
"name": name,
|
|
85
|
-
"description":
|
|
68
|
+
"description": p.get('description', ''),
|
|
86
69
|
"memory_count": count,
|
|
87
|
-
"created_at":
|
|
88
|
-
"last_used":
|
|
70
|
+
"created_at": p.get('created_at', ''),
|
|
71
|
+
"last_used": p.get('last_used', ''),
|
|
89
72
|
"is_active": name == active,
|
|
90
73
|
})
|
|
91
74
|
|
|
@@ -101,24 +84,29 @@ async def list_profiles():
|
|
|
101
84
|
|
|
102
85
|
@router.post("/api/profiles/{name}/switch")
|
|
103
86
|
async def switch_profile(name: str):
|
|
104
|
-
"""Switch active memory profile."""
|
|
87
|
+
"""Switch active memory profile (persists to both config stores)."""
|
|
105
88
|
try:
|
|
106
89
|
if not validate_profile_name(name):
|
|
107
90
|
raise HTTPException(status_code=400, detail="Invalid profile name.")
|
|
108
91
|
|
|
109
|
-
|
|
92
|
+
merged = sync_profiles()
|
|
93
|
+
merged_names = {p.get('name', p.get('profile_id', '')) for p in merged}
|
|
110
94
|
|
|
111
|
-
if name not in
|
|
112
|
-
available = ', '.join(
|
|
95
|
+
if name not in merged_names:
|
|
96
|
+
available = ', '.join(sorted(merged_names))
|
|
113
97
|
raise HTTPException(
|
|
114
98
|
status_code=404,
|
|
115
99
|
detail=f"Profile '{name}' not found. Available: {available}",
|
|
116
100
|
)
|
|
117
101
|
|
|
118
|
-
previous =
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
102
|
+
previous = _load_profiles_json().get('active_profile', 'default')
|
|
103
|
+
set_active_profile_everywhere(name)
|
|
104
|
+
|
|
105
|
+
# Update last_used in profiles.json
|
|
106
|
+
json_config = _load_profiles_json()
|
|
107
|
+
if name in json_config.get('profiles', {}):
|
|
108
|
+
json_config['profiles'][name]['last_used'] = datetime.now().isoformat()
|
|
109
|
+
_save_profiles_json(json_config)
|
|
122
110
|
|
|
123
111
|
count = _get_memory_count(name)
|
|
124
112
|
|
|
@@ -143,22 +131,22 @@ async def switch_profile(name: str):
|
|
|
143
131
|
|
|
144
132
|
@router.post("/api/profiles/create")
|
|
145
133
|
async def create_profile(body: ProfileSwitch):
|
|
146
|
-
"""Create a new memory profile."""
|
|
134
|
+
"""Create a new memory profile (writes to BOTH SQLite and profiles.json)."""
|
|
147
135
|
try:
|
|
148
136
|
name = body.profile_name
|
|
149
137
|
if not validate_profile_name(name):
|
|
150
138
|
raise HTTPException(status_code=400, detail="Invalid profile name")
|
|
151
139
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
140
|
+
# Check both stores for duplicates
|
|
141
|
+
merged = sync_profiles()
|
|
142
|
+
merged_names = {p.get('name', p.get('profile_id', '')) for p in merged}
|
|
143
|
+
if name in merged_names:
|
|
155
144
|
raise HTTPException(status_code=409, detail=f"Profile '{name}' already exists")
|
|
156
145
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
_save_profiles_config(config)
|
|
146
|
+
# Write to BOTH stores atomically
|
|
147
|
+
desc = f'Memory profile: {name}'
|
|
148
|
+
ensure_profile_in_db(name, desc)
|
|
149
|
+
ensure_profile_in_json(name, desc)
|
|
162
150
|
|
|
163
151
|
return {"success": True, "profile": name, "message": f"Profile '{name}' created"}
|
|
164
152
|
|
|
@@ -175,16 +163,18 @@ async def delete_profile(name: str):
|
|
|
175
163
|
if name == 'default':
|
|
176
164
|
raise HTTPException(status_code=400, detail="Cannot delete 'default' profile")
|
|
177
165
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if name not in
|
|
166
|
+
merged = sync_profiles()
|
|
167
|
+
merged_names = {p.get('name', p.get('profile_id', '')) for p in merged}
|
|
168
|
+
if name not in merged_names:
|
|
181
169
|
raise HTTPException(status_code=404, detail=f"Profile '{name}' not found")
|
|
182
|
-
|
|
170
|
+
|
|
171
|
+
json_config = _load_profiles_json()
|
|
172
|
+
if json_config.get('active_profile') == name:
|
|
183
173
|
raise HTTPException(status_code=400, detail="Cannot delete active profile.")
|
|
184
174
|
|
|
175
|
+
# Move data to default before deleting (bypasses CASCADE)
|
|
185
176
|
conn = get_db_connection()
|
|
186
177
|
cursor = conn.cursor()
|
|
187
|
-
# Move memories to default (try V3 first, then V2)
|
|
188
178
|
moved = 0
|
|
189
179
|
try:
|
|
190
180
|
cursor.execute(
|
|
@@ -196,7 +186,7 @@ async def delete_profile(name: str):
|
|
|
196
186
|
pass
|
|
197
187
|
try:
|
|
198
188
|
cursor.execute(
|
|
199
|
-
"UPDATE memories SET
|
|
189
|
+
"UPDATE memories SET profile_id = 'default' WHERE profile_id = ?",
|
|
200
190
|
(name,),
|
|
201
191
|
)
|
|
202
192
|
moved += cursor.rowcount
|
|
@@ -205,8 +195,13 @@ async def delete_profile(name: str):
|
|
|
205
195
|
conn.commit()
|
|
206
196
|
conn.close()
|
|
207
197
|
|
|
208
|
-
|
|
209
|
-
|
|
198
|
+
# Delete from BOTH stores
|
|
199
|
+
delete_profile_from_db(name)
|
|
200
|
+
|
|
201
|
+
profiles = json_config.get('profiles', {})
|
|
202
|
+
profiles.pop(name, None)
|
|
203
|
+
json_config['profiles'] = profiles
|
|
204
|
+
_save_profiles_json(json_config)
|
|
210
205
|
|
|
211
206
|
return {
|
|
212
207
|
"success": True,
|