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,