@@ -32,6 +32,10 @@ class SkillInfo:
3232 description : str
3333 path : str
3434 active : bool
35+ source_type : str = "local_only"
36+ source_label : str = "local"
37+ local_exists : bool = True
38+ sandbox_exists : bool = False
3539
3640
3741def _parse_frontmatter_description (text : str ) -> str :
@@ -164,6 +168,7 @@ def _load_sandbox_skills_cache(self) -> dict:
164168 return {
165169 "version" : int (data .get ("version" , _SANDBOX_SKILLS_CACHE_VERSION )),
166170 "skills" : skills ,
171+ "updated_at" : data .get ("updated_at" ),
167172 }
168173 except Exception :
169174 return {"version" : _SANDBOX_SKILLS_CACHE_VERSION , "skills" : []}
@@ -198,6 +203,17 @@ def set_sandbox_skills_cache(self, skills: list[dict]) -> None:
198203 }
199204 self ._save_sandbox_skills_cache (cache )
200205
206+ def get_sandbox_skills_cache_status (self ) -> dict [str , object ]:
207+ cache = self ._load_sandbox_skills_cache ()
208+ skills = cache .get ("skills" , [])
209+ count = len (skills ) if isinstance (skills , list ) else 0
210+ return {
211+ "exists" : os .path .exists (self .sandbox_skills_cache_path ),
212+ "ready" : count > 0 ,
213+ "count" : count ,
214+ "updated_at" : cache .get ("updated_at" ),
215+ }
216+
201217 def list_skills (
202218 self ,
203219 * ,
@@ -217,15 +233,18 @@ def list_skills(
217233 skills_by_name : dict [str , SkillInfo ] = {}
218234
219235 sandbox_cached_paths : dict [str , str ] = {}
220- if runtime == "sandbox" :
221- cache_for_paths = self ._load_sandbox_skills_cache ()
222- for item in cache_for_paths .get ("skills" , []):
223- if not isinstance (item , dict ):
224- continue
225- name = str (item .get ("name" , "" ) or "" ).strip ()
226- path = str (item .get ("path" , "" ) or "" ).strip ().replace ("\\ " , "/" )
227- if name and path and _SKILL_NAME_RE .match (name ):
228- sandbox_cached_paths [name ] = path
236+ sandbox_cached_descriptions : dict [str , str ] = {}
237+ cache_for_paths = self ._load_sandbox_skills_cache ()
238+ for item in cache_for_paths .get ("skills" , []):
239+ if not isinstance (item , dict ):
240+ continue
241+ name = str (item .get ("name" , "" ) or "" ).strip ()
242+ path = str (item .get ("path" , "" ) or "" ).strip ().replace ("\\ " , "/" )
243+ if not name or not _SKILL_NAME_RE .match (name ):
244+ continue
245+ sandbox_cached_descriptions [name ] = str (item .get ("description" , "" ) or "" )
246+ if path :
247+ sandbox_cached_paths [name ] = path
229248
230249 for entry in sorted (Path (self .skills_root ).iterdir ()):
231250 if not entry .is_dir ():
@@ -246,6 +265,11 @@ def list_skills(
246265 description = _parse_frontmatter_description (content )
247266 except Exception :
248267 description = ""
268+ sandbox_exists = (
269+ runtime == "sandbox" and skill_name in sandbox_cached_descriptions
270+ )
271+ source_type = "both" if sandbox_exists else "local_only"
272+ source_label = "synced" if sandbox_exists else "local"
249273 if runtime == "sandbox" and show_sandbox_path :
250274 path_str = sandbox_cached_paths .get (skill_name ) or (
251275 f"{ SANDBOX_WORKSPACE_ROOT } /{ SANDBOX_SKILLS_ROOT } /{ skill_name } /SKILL.md"
@@ -258,6 +282,10 @@ def list_skills(
258282 description = description ,
259283 path = path_str ,
260284 active = active ,
285+ source_type = source_type ,
286+ source_label = source_label ,
287+ local_exists = True ,
288+ sandbox_exists = sandbox_exists ,
261289 )
262290
263291 if runtime == "sandbox" :
@@ -278,22 +306,22 @@ def list_skills(
278306 modified = True
279307 if active_only and not active :
280308 continue
281- description = str ( item .get ("description" , "" ) or "" )
309+ description = sandbox_cached_descriptions .get (skill_name , "" )
282310 if show_sandbox_path :
283- path_str = (
284- f"{ SANDBOX_WORKSPACE_ROOT } /{ SANDBOX_SKILLS_ROOT } /{ skill_name } /SKILL.md"
285- )
311+ path_str = f"{ SANDBOX_WORKSPACE_ROOT } /{ SANDBOX_SKILLS_ROOT } /{ skill_name } /SKILL.md"
286312 else :
287- path_str = str ( item .get ("path" , "" ) or "" )
313+ path_str = sandbox_cached_paths .get (skill_name , "" )
288314 if not path_str :
289- path_str = (
290- f"{ SANDBOX_WORKSPACE_ROOT } /{ SANDBOX_SKILLS_ROOT } /{ skill_name } /SKILL.md"
291- )
315+ path_str = f"{ SANDBOX_WORKSPACE_ROOT } /{ SANDBOX_SKILLS_ROOT } /{ skill_name } /SKILL.md"
292316 skills_by_name [skill_name ] = SkillInfo (
293317 name = skill_name ,
294318 description = description ,
295319 path = path_str .replace ("\\ " , "/" ),
296320 active = active ,
321+ source_type = "sandbox_only" ,
322+ source_label = "sandbox_preset" ,
323+ local_exists = False ,
324+ sandbox_exists = True ,
297325 )
298326
299327 if modified :
@@ -302,7 +330,27 @@ def list_skills(
302330
303331 return [skills_by_name [name ] for name in sorted (skills_by_name )]
304332
333+ def is_sandbox_only_skill (self , name : str ) -> bool :
334+ skill_dir = Path (self .skills_root ) / name
335+ skill_md_exists = (skill_dir / "SKILL.md" ).exists ()
336+ if skill_md_exists :
337+ return False
338+ cache = self ._load_sandbox_skills_cache ()
339+ skills = cache .get ("skills" , [])
340+ if not isinstance (skills , list ):
341+ return False
342+ for item in skills :
343+ if not isinstance (item , dict ):
344+ continue
345+ if str (item .get ("name" , "" )).strip () == name :
346+ return True
347+ return False
348+
305349 def set_skill_active (self , name : str , active : bool ) -> None :
350+ if self .is_sandbox_only_skill (name ):
351+ raise PermissionError (
352+ "Sandbox preset skill cannot be enabled/disabled from local skill management."
353+ )
306354 config = self ._load_config ()
307355 config .setdefault ("skills" , {})
308356 config ["skills" ][name ] = {"active" : bool (active )}
@@ -318,8 +366,7 @@ def _remove_skill_from_sandbox_cache(self, name: str) -> None:
318366 item
319367 for item in skills
320368 if not (
321- isinstance (item , dict )
322- and str (item .get ("name" , "" )).strip () == name
369+ isinstance (item , dict ) and str (item .get ("name" , "" )).strip () == name
323370 )
324371 ]
325372
@@ -328,6 +375,11 @@ def _remove_skill_from_sandbox_cache(self, name: str) -> None:
328375 self ._save_sandbox_skills_cache (cache )
329376
330377 def delete_skill (self , name : str ) -> None :
378+ if self .is_sandbox_only_skill (name ):
379+ raise PermissionError (
380+ "Sandbox preset skill cannot be deleted from local skill management."
381+ )
382+
331383 skill_dir = Path (self .skills_root ) / name
332384 if skill_dir .exists ():
333385 shutil .rmtree (skill_dir )
0 commit comments