Add delete_file and plugin toggle handling
Add files.delete_file and implement plugin toggle/state logic. Introduce PluginAssetFile TypedDict and a toggle_state field on PluginListItem. Change toggle filenames/patterns and implement get_plugin_meta, get_toggle_state (considers user/project/agent toggles and always_enabled), and toggle_plugin to create/remove toggle files. Update get_plugin_config to return asset dicts, adjust find_plugin_asset to return the full asset entry, and add typing updates to find_plugin_assets. Miscellaneous import and formatting tweaks.
frdel committed
Feb 23, 2026 at 10:10 UTC
420f70d7de1ee01901ca9610fcf7a887c00ab6fc
2 files changed
+92
-19
python/helpers/files.py
+3
@@ -423,6 +423,9 @@ def write_file(relative_path: str, content: str, encoding: str = "utf-8"):
423
with open(abs_path, "w", encoding=encoding) as f:
424
f.write(content)
425
426
+def delete_file(relative_path: str):
427
+ abs_path = get_abs_path(relative_path)
428
+ os.remove(abs_path)
429
430
def write_file_bin(relative_path: str, content: bytes):
431
abs_path = get_abs_path(relative_path)
python/helpers/plugins.py
+89
-19
@@ -2,7 +2,7 @@ from __future__ import annotations
2
3
import re, json
4
from pathlib import Path
5
-from typing import Any, Dict, List, Literal, Optional, TYPE_CHECKING
5
+from typing import Any, Dict, List, Literal, Optional, TYPE_CHECKING, TypedDict
6
7
from python.helpers import files, print_style
8
from pydantic import BaseModel, Field
@@ -17,14 +17,17 @@ _META_TARGET_RE = re.compile(
17
)
18
19
type ToggleState = Literal["enabled", "disabled", "advanced"]
20
-
20
+class PluginAssetFile(TypedDict):
21
+ path: str
22
+ project_name: str
23
+ agent_profile: str
24
25
META_FILE_NAME = "plugin.json"
26
CONFIG_FILE_NAME = "config.json"
27
CONFIG_DEFAULT_FILE_NAME = "config.default.json"
25
-DISABLED_FILE_NAME = ".disabled"
26
-ENABLED_FILE_NAME = ".enabled"
27
-TOGGLE_FILE_PATTERN = ".*abled"
28
+DISABLED_FILE_NAME = ".toggle-0"
29
+ENABLED_FILE_NAME = ".toggle-1"
30
+TOGGLE_FILE_PATTERN = "*.toggle-[01]"
31
32
33
class PluginMetadata(BaseModel):
@@ -49,6 +52,7 @@ class PluginListItem(BaseModel):
52
is_custom: bool = False
53
has_main_screen: bool = False
54
has_config_screen: bool = False
55
+ toggle_state: ToggleState = "disabled"
56
57
58
def get_plugin_roots() -> List[str]:
@@ -105,6 +109,7 @@ def get_enhanced_plugins_list(
109
is_custom=is_custom,
110
has_main_screen=has_main_screen,
111
has_config_screen=has_config_screen,
112
+ toggle_state=toggle_state,
113
)
114
)
115
except:
@@ -117,6 +122,15 @@ def get_enhanced_plugins_list(
122
return results
123
124
125
+def get_plugin_meta(plugin_name: str):
126
+ plugin_dir = find_plugin_dir(plugin_name)
127
+ if not plugin_dir:
128
+ return None
129
+ return PluginMetadata.model_validate(
130
+ files.read_file_json(files.get_abs_path(plugin_dir, META_FILE_NAME))
131
+ )
132
+
133
+
134
def find_plugin_dir(plugin_name: str):
135
if not plugin_name:
136
return None
@@ -180,6 +194,7 @@ def get_enabled_plugins(agent: Agent | None):
194
195
if agent:
196
from python.helpers import subagents
197
+
198
agent_paths = subagents.get_paths(
199
agent,
200
files.PLUGINS_DIR,
@@ -209,12 +224,50 @@ def get_enabled_plugins(agent: Agent | None):
224
225
226
def get_toggle_state(plugin_name: str) -> ToggleState:
212
- return "enabled"
227
+ meta = get_plugin_meta(plugin_name)
228
+ if not meta:
229
+ return "disabled"
230
+ if meta.always_enabled:
231
+ return "enabled"
232
+
233
+ state = "enabled"
234
+
235
+ # toggles inside of user directory (there should be only one, but let's make it work in any case)
236
+ usr_toggles = files.find_existing_paths_by_pattern(files.get_abs_path(files.USER_DIR, files.PLUGINS_DIR, plugin_name, TOGGLE_FILE_PATTERN))
237
+ for toggle in usr_toggles:
238
+ if toggle.endswith(ENABLED_FILE_NAME):
239
+ state = "enabled"
240
+ if toggle.endswith(DISABLED_FILE_NAME):
241
+ state = "disabled"
242
+
243
+ # if there are more toggles in project/agent directories, return advanced
244
+ if meta.per_agent_config or meta.per_project_config:
245
+ configs = find_plugin_assets(
246
+ TOGGLE_FILE_PATTERN,
247
+ plugin_name=plugin_name,
248
+ project_name="*" if meta.per_project_config else "",
249
+ agent_profile="*" if meta.per_agent_config else "",
250
+ only_first=False,
251
+ )
252
+ if len(configs) > len(usr_toggles):
253
+ state = "advanced"
254
+
255
+ return state
256
257
215
-def toggle_plugin(plugin_name: str, enabled: bool, project_name: str = "", agent_profile: str = ""):
216
- pass
217
-
258
+def toggle_plugin(
259
+ plugin_name: str, enabled: bool, project_name: str = "", agent_profile: str = ""
260
+):
261
+ enabled_file = determine_plugin_asset_path(plugin_name, project_name, agent_profile, ENABLED_FILE_NAME)
262
+ disabled_file = determine_plugin_asset_path(plugin_name, project_name, agent_profile, DISABLED_FILE_NAME)
263
+
264
+ if enabled:
265
+ files.delete_file(disabled_file)
266
+ files.write_file(enabled_file, "")
267
+ else:
268
+ files.delete_file(enabled_file)
269
+ files.write_file(disabled_file, "")
270
+
271
272
def get_webui_extensions(extension_point: str, filters: List[str] | None = None):
273
entries: List[str] = []
@@ -229,16 +282,29 @@ def get_webui_extensions(extension_point: str, filters: List[str] | None = None)
282
return entries
283
284
232
-def get_plugin_config(plugin_name: str, agent: Agent | None=None, project_name:str|None=None, agent_profile:str|None=None):
233
-
285
+def get_plugin_config(
286
+ plugin_name: str,
287
+ agent: Agent | None = None,
288
+ project_name: str | None = None,
289
+ agent_profile: str | None = None,
290
+):
291
+
292
if project_name is None and agent is not None:
293
from python.helpers import projects
294
+
295
project_name = projects.get_context_project_name(agent.context)
296
if agent_profile is None and agent is not None:
297
agent_profile = agent.config.profile
239
-
298
+
299
# find config.json in all possible places
241
- file_path = find_plugin_asset(plugin_name, CONFIG_FILE_NAME, project_name=project_name or "", agent_profile=agent_profile or "")
300
+ file = find_plugin_asset(
301
+ plugin_name,
302
+ CONFIG_FILE_NAME,
303
+ project_name=project_name or "",
304
+ agent_profile=agent_profile or "",
305
+ )
306
+ file_path = file.get("path", "") if file else ""
307
+
308
# use default config if not found
309
if not file_path:
310
file_path = files.get_abs_path(
@@ -259,15 +325,17 @@ def save_plugin_config(
325
files.write_file(file_path, json.dumps(settings))
326
327
262
-def find_plugin_asset(plugin_name: str, *subpaths: str, project_name="", agent_profile=""):
328
+def find_plugin_asset(
329
+ plugin_name: str, *subpaths: str, project_name="", agent_profile=""
330
+):
331
result = find_plugin_assets(
332
*subpaths,
333
plugin_name=plugin_name,
334
project_name=project_name,
335
agent_profile=agent_profile,
268
- only_first=True
336
+ only_first=True,
337
)
270
- return result[0]["path"] if result else None
338
+ return result[0] if result else None
339
340
341
def find_plugin_assets(
@@ -276,10 +344,10 @@ def find_plugin_assets(
344
project_name: str = "*",
345
agent_profile: str = "*",
346
only_first: bool = False,
279
-) -> list[dict]:
347
+) -> list[PluginAssetFile]:
348
from python.helpers import projects, subagents
349
282
- results: list[dict] = []
350
+ results: list[PluginAssetFile] = []
351
352
def _collect(path: str, proj: str, profile: str) -> bool:
353
matched_paths = (
@@ -301,7 +369,9 @@ def find_plugin_assets(
369
370
for matched in matched_paths:
371
inferred_proj = _after(matched, "/projects/") if need_proj else proj
304
- inferred_prof = _after(matched, "/agents/", last=True) if need_prof else profile
372
+ inferred_prof = (
373
+ _after(matched, "/agents/", last=True) if need_prof else profile
374
+ )
375
results.append(
376
{
377
"project_name": inferred_proj,