Coverage for src / library_skills / cli.py: 100%

439 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-03 20:19 +0000

1import json 1Q23R45S67

2from collections import Counter 1Q23R45S67

3from dataclasses import dataclass 1Q23R45S67

4from pathlib import Path 1Q23R45S67

5from typing import Annotated 1Q23R45S67

6 

7import typer 1Q23R45S67

8from rich_toolkit import RichToolkit 1Q23R45S67

9from rich_toolkit.menu import Menu, Option 1Q23R45S67

10 

11from . import ui 1Q23R45S67

12from .deps import ( 1Q23R45S67

13 get_node_workspace_top_level_deps, 

14 get_top_level_deps, 

15 get_workspace_top_level_deps, 

16) 

17from .installer import ( 1Q23R45S67

18 TOOL_SKILL_MARKER, 

19 TOOL_SKILL_NAME, 

20 InstallError, 

21 InstallTarget, 

22 ToolSkillError, 

23 ToolSkillStatus, 

24 get_all_target_dirs, 

25 get_default_install_target_dirs, 

26 get_existing_target_dirs, 

27 get_target_dirs, 

28 inspect_tool_skill, 

29 install_skill, 

30 install_tool_skill, 

31 list_installed_skills, 

32 uninstall_skill, 

33) 

34from .python_env import ( 1Q23R45S67

35 find_node_modules, 

36 find_project_root, 

37 find_venv, 

38 get_site_packages_dir, 

39) 

40from .scanner import ( 1Q23R45S67

41 ScanResult, 

42 Skill, 

43 _normalize_package_name, 

44 scan_node_packages, 

45 scan_python_distributions, 

46) 

47from .workspace import ( 1Q23R45S67

48 NodeWorkspace, 

49 UvWorkspace, 

50 find_node_workspace, 

51 find_uv_workspace, 

52 node_workspace_dependency_files, 

53 workspace_dependency_files, 

54) 

55 

56console = ui.console 1Q23R45S67

57 

58app = typer.Typer( 1Q23R45S67

59 name="library-skills", 

60 help="Discover and reconcile agent skills from installed library packages.", 

61 invoke_without_command=True, 

62) 

63 

64 

65@dataclass(frozen=True) 1Q23R45S67

66class ProjectContext: 1Q23R45S67

67 cwd: Path 1QRS

68 project_root: Path 1QRS

69 target_environment: Path | None 1QRS

70 site_packages_dir: Path | None 1QRS

71 node_modules_dir: Path | None 1QRS

72 workspace: UvWorkspace | None = None 1Q23R45S67

73 node_workspace: NodeWorkspace | None = None 1Q23R45S67

74 workspace_dependency_files: tuple[Path, ...] = () 1Q23R45S67

75 node_workspace_dependency_files: tuple[Path, ...] = () 1Q23R45S67

76 dependency_files: tuple[Path, ...] = () 1Q23R45S67

77 

78 

79@dataclass(frozen=True) 1Q23R45S67

80class InstalledStatus: 1Q23R45S67

81 target: InstallTarget 1QRS

82 name: str 1QRS

83 type: str 1QRS

84 path: Path 1QRS

85 target_path: Path | None 1QRS

86 status: str 1QRS

87 skill: Skill | None = None 1Q23R45S67

88 

89 

90def _get_rich_toolkit() -> RichToolkit: 1Q23R45S67

91 return ui.get_toolkit(console=console) 2FeAcBcCcGeDcEcFcHeGcHcIc

92 

93 

94def _get_project_context(cwd: Path | None = None) -> ProjectContext: 1Q23R45S67

95 cwd = cwd or Path.cwd() 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb

96 workspace = find_uv_workspace(cwd) 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb

97 node_workspace = find_node_workspace(cwd) 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb

98 project_root = ( 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb

99 workspace.root 

100 if workspace is not None 

101 else node_workspace.root 

102 if node_workspace is not None 

103 else find_project_root(cwd) or cwd 

104 ) 

105 workspace_files = ( 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb

106 tuple(workspace_dependency_files(workspace)) if workspace is not None else () 

107 ) 

108 node_workspace_files = ( 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb

109 tuple(node_workspace_dependency_files(node_workspace)) 

110 if node_workspace is not None 

111 else () 

112 ) 

113 target_environment = find_venv(cwd) 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb

114 site_packages_dir = ( 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb

115 get_site_packages_dir(target_environment) if target_environment else None 

116 ) 

117 node_modules_dir = find_node_modules(cwd) 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb

118 return ProjectContext( 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb

119 cwd=cwd, 

120 project_root=project_root, 

121 target_environment=target_environment, 

122 site_packages_dir=site_packages_dir, 

123 node_modules_dir=node_modules_dir, 

124 workspace=workspace, 

125 node_workspace=node_workspace, 

126 workspace_dependency_files=workspace_files, 

127 node_workspace_dependency_files=node_workspace_files, 

128 dependency_files=workspace_files + node_workspace_files, 

129 ) 

130 

131 

132def _scan_context(context: ProjectContext) -> ScanResult: 1Q23R45S67

133 result = ScanResult(environment_path=context.target_environment) 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb

134 if context.site_packages_dir is not None: 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb

135 python_result = scan_python_distributions(context.site_packages_dir) 2W X K N Rb3bh e b k C G n bb* + , } XbYbhb] ^ LbebSbI qbrbsbIb@bzb? z T [b+bibs u v AbBbY Z L O Tb4bi f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ tbubvbJb]bCb@ A U ^b/bkbt w x DbEb0 1 M P Vb5bg d a j B E m db: ; = ab1b2blb{ | PbgbWbF wbxbybKb_bFb[ y V `b?bmbp q r GbHb

136 result.skills.extend(python_result.skills) 2W X K N Rb3bh e b k C G n bb* + , } XbYbhb] ^ LbebSbI qbrbsbIb@bzb? z T [b+bibs u v AbBbY Z L O Tb4bi f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ tbubvbJb]bCb@ A U ^b/bkbt w x DbEb0 1 M P Vb5bg d a j B E m db: ; = ab1b2blb{ | PbgbWbF wbxbybKb_bFb[ y V `b?bmbp q r GbHb

137 result.warnings.extend(python_result.warnings) 2W X K N Rb3bh e b k C G n bb* + , } XbYbhb] ^ LbebSbI qbrbsbIb@bzb? z T [b+bibs u v AbBbY Z L O Tb4bi f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ tbubvbJb]bCb@ A U ^b/bkbt w x DbEb0 1 M P Vb5bg d a j B E m db: ; = ab1b2blb{ | PbgbWbF wbxbybKb_bFb[ y V `b?bmbp q r GbHb

138 if context.workspace and context.workspace.current_member is not None: 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb

139 member_venv = context.workspace.current_member / ".venv" 2RbT ib6b7bTbU kb8b9bVbV mb!b#b

140 if member_venv.exists(): 2RbT ib6b7bTbU kb8b9bVbV mb!b#b

141 result.warnings.append( 2T 6b7bU 8b9bV !b#b

142 f"Ignoring member-local .venv in uv workspace: {member_venv}" 

143 ) 

144 if context.node_modules_dir is not None: 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb

145 node_result = scan_node_packages(context.node_modules_dir) 2Rb3b(b)b*b$bnb? [c]cTb4b,b-b.b%bob@ ^c_cVb5b:b;b=b'bpb[ `c{c

146 result.skills.extend(node_result.skills) 2Rb3b(b)b*b$bnb? [c]cTb4b,b-b.b%bob@ ^c_cVb5b:b;b=b'bpb[ `c{c

147 result.warnings.extend(node_result.warnings) 2Rb3b(b)b*b$bnb? [c]cTb4b,b-b.b%bob@ ^c_cVb5b:b;b=b'bpb[ `c{c

148 if context.site_packages_dir is None and context.node_modules_dir is None: 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb

149 result.warnings.append( 28 Mbdded9 Obfdgd! Qbhdid

150 "No target Python environment with site-packages or node_modules was " 

151 "found. Run from a project root after installing dependencies, for " 

152 "example with 'uv sync' for Python or 'npm install' for Node.js." 

153 ) 

154 result.environment_path = context.target_environment 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb

155 return result 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb

156 

157 

158def _print_warnings(warnings: list[str]) -> None: 1Q23R45S67

159 ui.print_warnings(warnings, console=console) 2W X K N 8 h e b k C G n bb* + , } XbYbhb] ^ LbSbI nbzb? Mbz T ibs u v AbBbY Z L O 9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbUbJ obCb@ ObA U kbt w x DbEb0 1 M P ! g d a j B E m db: ; = ab1b2blb{ | PbWbF pbFb[ Qby V mbp q r GbHb

160 

161 

162def _display_path(path: Path | None, project_root: Path) -> str: 1Q23R45S67

163 if path is None: 2W X K N h e b k C G %cn bb* + , } ] ^ I nbqbrbsbzb? z T ibs u v AbBbY Z L O i f c l D H 'co cb- . / ~ _ ` J obtbubvbCb@ A U kbt w x DbEb0 1 M P g d a j B E (cm db: ; = ab{ | F pbwbxbybFb[ y V mbp q r GbHb

164 return "" 2%cn s ocpc'co t qcrc(cm p sctc

165 absolute_path = path if path.is_absolute() else Path.cwd() / path 2W X K N h e b k C G %cn bb* + , } ] ^ I nbqbrbsbzb? z T ibs u v AbBbY Z L O i f c l D H 'co cb- . / ~ _ ` J obtbubvbCb@ A U kbt w x DbEb0 1 M P g d a j B E (cm db: ; = ab{ | F pbwbxbybFb[ y V mbp q r GbHb

166 try: 2W X K N h e b k C G %cn bb* + , } ] ^ I nbqbrbsbzb? z T ibs u v AbBbY Z L O i f c l D H 'co cb- . / ~ _ ` J obtbubvbCb@ A U kbt w x DbEb0 1 M P g d a j B E (cm db: ; = ab{ | F pbwbxbybFb[ y V mbp q r GbHb

167 return str(absolute_path.relative_to(project_root.resolve())) 2W X K N h e b k C G %cn bb* + , } ] ^ I nbqbrbsbzb? z T ibs u v AbBbY Z L O i f c l D H 'co cb- . / ~ _ ` J obtbubvbCb@ A U kbt w x DbEb0 1 M P g d a j B E (cm db: ; = ab{ | F pbwbxbybFb[ y V mbp q r GbHb

168 except ValueError: 2%c-d.d'c/d:d(c;d=d

169 return str(path) 2%c-d.d'c/d:d(c;d=d

170 

171 

172def _target_prompt_name(target: InstallTarget) -> str: 1Q23R45S67

173 if target.name == "universal": 2AcJcBcCcDcKcEcFcGcLcHcIc

174 return "Agents (.agents/skills)" 2AcJcBcCcDcKcEcFcGcLcHcIc

175 if target.name == "claude-compatible": 2AcJcBcCcDcKcEcFcGcLcHcIc

176 return "Claude Code (.claude/skills)" 2AcJcBcCcDcKcEcFcGcLcHcIc

177 return target.name 2JcpdqdKcrdsdLctdud

178 

179 

180def _target_short_name(target: InstallTarget) -> str: 1Q23R45S67

181 if target.name == "universal": 2~b1cJc2c3cac4cKc5c6cbc7cLc8c9c

182 return "Agents" 2~b1cJc2c3cac4cKc5c6cbc7cLc8c9c

183 if target.name == "claude-compatible": 2JcpdqdKcrdsdLctdud

184 return "Claude Code" 2JcpdqdKcrdsdLctdud

185 return target.name 2JcpdqdKcrdsdLctdud

186 

187 

188def _print_context(context: ProjectContext) -> None: 1Q23R45S67

189 rows = [("Project root", str(context.project_root))] 2W X K N 8 h e b k C G n I nbzb? Mbz ibs u v AbBbY Z L O 9 i f c l D H o J obCb@ ObA kbt w x DbEb0 1 M P ! g d a j B E m F pbFb[ Qby mbp q r GbHb

190 if context.workspace is not None: 2W X K N 8 h e b k C G n I nbzb? Mbz ibs u v AbBbY Z L O 9 i f c l D H o J obCb@ ObA kbt w x DbEb0 1 M P ! g d a j B E m F pbFb[ Qby mbp q r GbHb

191 rows.append(("Workspace root", str(context.workspace.root))) 2ibKdLdkbMdNdmbOdPd

192 if context.workspace.current_member is not None: 2ibKdLdkbMdNdmbOdPd

193 rows.append(("Workspace member", str(context.workspace.current_member))) 2ibKdLdkbMdNdmbOdPd

194 elif context.node_workspace is not None: 2W X K N 8 h e b k C G n I nbzb? Mbz s u v AbBbY Z L O 9 i f c l D H o J obCb@ ObA t w x DbEb0 1 M P ! g d a j B E m F pbFb[ Qby p q r GbHb

195 rows.append(("Workspace root", str(context.node_workspace.root))) 2nb|c}cob~cadpbbdcd

196 if context.node_workspace.current_member is not None: 2nb|c}cob~cadpbbdcd

197 rows.append( 2nb|c}cob~cadpbbdcd

198 ("Workspace member", str(context.node_workspace.current_member)) 

199 ) 

200 if context.target_environment: 2W X K N 8 h e b k C G n I nbzb? Mbz ibs u v AbBbY Z L O 9 i f c l D H o J obCb@ ObA kbt w x DbEb0 1 M P ! g d a j B E m F pbFb[ Qby mbp q r GbHb

201 rows.append( 2W X K N h e b k C G n I zb? z ibs u v AbBbY Z L O i f c l D H o J Cb@ A kbt w x DbEb0 1 M P g d a j B E m F Fb[ y mbp q r GbHb

202 ( 

203 "Target Python environment", 

204 _display_path(context.target_environment, context.project_root), 

205 ) 

206 ) 

207 else: 

208 rows.append(("Target Python environment", "not found")) 28 nbMb|c}c9 obOb~cad! pbQbbdcd

209 if context.site_packages_dir: 2W X K N 8 h e b k C G n I nbzb? Mbz ibs u v AbBbY Z L O 9 i f c l D H o J obCb@ ObA kbt w x DbEb0 1 M P ! g d a j B E m F pbFb[ Qby mbp q r GbHb

210 rows.append( 2W X K N h e b k C G n I zb? z ibs u v AbBbY Z L O i f c l D H o J Cb@ A kbt w x DbEb0 1 M P g d a j B E m F Fb[ y mbp q r GbHb

211 ( 

212 "Site-packages", 

213 _display_path(context.site_packages_dir, context.project_root), 

214 ) 

215 ) 

216 if context.node_modules_dir: 2W X K N 8 h e b k C G n I nbzb? Mbz ibs u v AbBbY Z L O 9 i f c l D H o J obCb@ ObA kbt w x DbEb0 1 M P ! g d a j B E m F pbFb[ Qby mbp q r GbHb

217 rows.append( 2nb? |c}cob@ ~cadpb[ bdcd

218 ( 

219 "node_modules", 

220 _display_path(context.node_modules_dir, context.project_root), 

221 ) 

222 ) 

223 ui.print_title("context", console=console) 2W X K N 8 h e b k C G n I nbzb? Mbz ibs u v AbBbY Z L O 9 i f c l D H o J obCb@ ObA kbt w x DbEb0 1 M P ! g d a j B E m F pbFb[ Qby mbp q r GbHb

224 ui.print_table(ui.context_table(rows), console=console) 2W X K N 8 h e b k C G n I nbzb? Mbz ibs u v AbBbY Z L O 9 i f c l D H o J obCb@ ObA kbt w x DbEb0 1 M P ! g d a j B E m F pbFb[ Qby mbp q r GbHb

225 

226 

227def _print_skills_table(skills: list[Skill]) -> None: 1Q23R45S67

228 """Print a formatted table of discovered skills.""" 

229 if not skills: 2Sbnbzb? MbibAbBbUbobCb@ ObkbDbEbWbpbFb[ QbmbGbHb

230 ui.print_message("No skills found in installed packages.", console=console) 2MbddedObfdgdQbhdid

231 return 2MbddedObfdgdQbhdid

232 

233 ui.print_table( 2Sbnbzb? ibAbBbUbobCb@ kbDbEbWbpbFb[ mbGbHb

234 ui.skills_table( 

235 [ 

236 ( 

237 skill.name, 

238 skill.package_name, 

239 skill.package_version, 

240 skill.description, 

241 ) 

242 for skill in skills 

243 ] 

244 ), 

245 console=console, 

246 ) 

247 

248 

249def _top_level_skills( 1Q23R45S67

250 *, 

251 context: ProjectContext, 

252 skills: list[Skill], 

253 include_all: bool, 

254) -> list[Skill]: 

255 """Filter discovered skills to top-level project dependencies by default.""" 

256 if include_all: 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbFb[ Qby V `b?bmbp q r GbHb

257 return skills 2G bb} Xb$bzb? T +bAbBbH cb~ Zb%bCb@ U /bDbEbE dbab1b'bFb[ V ?bGbHb

258 

259 top_level_deps = get_top_level_deps(context.project_root) 2W X K N Rb3b8 h e b k C n * + , Ybhb] ^ LbebSbI (b)b*b$bnbzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D o - . / 0bjb_ ` NbfbUbJ ,b-b.b%bobCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B m : ; = 2blb{ | PbgbWbF :b;b=b'bpbFb[ Qby V `b?bmbp q r GbHb

260 if context.workspace is not None: 2W X K N Rb3b8 h e b k C n * + , Ybhb] ^ LbebSbI (b)b*b$bnbzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D o - . / 0bjb_ ` NbfbUbJ ,b-b.b%bobCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B m : ; = 2blb{ | PbgbWbF :b;b=b'bpbFb[ Qby V `b?bmbp q r GbHb

261 top_level_deps = get_workspace_top_level_deps( 2Rb3bT [b+bib6b7bTb4bU ^b/bkb8b9bVb5bV `b?bmb!b#b

262 list(context.workspace_dependency_files) 

263 ) 

264 if context.node_workspace is not None: 2Rb3bT [b+bib6b7bTb4bU ^b/bkb8b9bVb5bV `b?bmb!b#b

265 node_top_level_deps = get_node_workspace_top_level_deps( 2Rb3b[c]cTb4b^c_cVb5b`c{c

266 list(context.node_workspace_dependency_files) 

267 ) 

268 if node_top_level_deps is not None: 2Rb3b[c]cTb4b^c_cVb5b`c{c

269 top_level_deps = ( 2Rb3b[c]cTb4b^c_cVb5b`c{c

270 node_top_level_deps 

271 if top_level_deps is None 

272 else top_level_deps | node_top_level_deps 

273 ) 

274 elif context.node_workspace is not None: 2W X K N 8 h e b k C n * + , Ybhb] ^ LbebSbI (b)b*b$bnbzb? Mbz s u v AbBbY Z L O 9 i f c l D o - . / 0bjb_ ` NbfbUbJ ,b-b.b%bobCb@ ObA t w x DbEb0 1 M P ! g d a j B m : ; = 2blb{ | PbgbWbF :b;b=b'bpbFb[ Qby p q r GbHb

275 top_level_deps = get_node_workspace_top_level_deps( 2(b)b*b$bnb|c}c,b-b.b%bob~cad:b;b=b'bpbbdcd

276 list(context.node_workspace_dependency_files) 

277 ) 

278 if top_level_deps is None: 2W X K N Rb3b8 h e b k C n * + , Ybhb] ^ LbebSbI (b)b*b$bnbzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D o - . / 0bjb_ ` NbfbUbJ ,b-b.b%bobCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B m : ; = 2blb{ | PbgbWbF :b;b=b'bpbFb[ Qby V `b?bmbp q r GbHb

279 return skills 28 Mbdded9 Obfdgd! Qbhdid

280 

281 return [ 2W X K N Rb3bh e b k C n * + , Ybhb] ^ LbebSbI (b)b*b$bnbzb? z T [b+bibs u v AbBbY Z L O Tb4bi f c l D o - . / 0bjb_ ` NbfbUbJ ,b-b.b%bobCb@ A U ^b/bkbt w x DbEb0 1 M P Vb5bg d a j B m : ; = 2blb{ | PbgbWbF :b;b=b'bpbFb[ y V `b?bmbp q r GbHb

282 skill 

283 for skill in skills 

284 if _normalize_package_name(skill.package_name) in top_level_deps 

285 ] 

286 

287 

288def _scan_json_payload( 1Q23R45S67

289 *, context: ProjectContext, skills: list[Skill], result: ScanResult 

290) -> dict[str, object]: 

291 return { 2Rb3beb(b)b*b$b? T [b+b6b7bTb4bfb,b-b.b%b@ U ^b/b8b9bVb5bgb:b;b=b'b[ V `b?b!b#b

292 "project_root": str(context.project_root), 

293 "workspace_root": str(_workspace_root(context)), 

294 "workspace_member": ( 

295 str(context.workspace.current_member) 

296 if context.workspace and context.workspace.current_member 

297 else str(context.node_workspace.current_member) 

298 if context.node_workspace and context.node_workspace.current_member 

299 else "" 

300 ), 

301 "dependency_files": [str(path) for path in context.dependency_files], 

302 "target_environment": str(context.target_environment or ""), 

303 "node_modules": str(context.node_modules_dir or ""), 

304 "skills": [ 

305 { 

306 "name": skill.name, 

307 "description": skill.description, 

308 "package": skill.package_name, 

309 "version": skill.package_version, 

310 "path": str(skill.skill_dir), 

311 } 

312 for skill in skills 

313 ], 

314 "warnings": result.warnings, 

315 } 

316 

317 

318def _workspace_root(context: ProjectContext) -> Path | str: 1Q23R45S67

319 if context.workspace is not None: 2Rb3beb(b)b*b$b? T [b+b6b7bTb4bfb,b-b.b%b@ U ^b/b8b9bVb5bgb:b;b=b'b[ V `b?b!b#b

320 return context.workspace.root 2Rb3bT [b+b6b7bTb4bU ^b/b8b9bVb5bV `b?b!b#b

321 if context.node_workspace is not None: 2eb(b)b*b$b? ?d@dfb,b-b.b%b@ [d]dgb:b;b=b'b[ ^d_d

322 return context.node_workspace.root 2(b)b*b$b?d@d,b-b.b%b[d]d:b;b=b'b^d_d

323 return "" 2eb? IeJefb@ KeLegb[ MeNe

324 

325 

326def _print_action_header(action: str) -> None: 1Q23R45S67

327 ui.print_action_header(action, console=console) 2~b1c2c3cac4c5c6cbc7c8c9c

328 

329 

330def _select_skills_interactive(skills: list[Skill]) -> list[Skill]: 1Q23R45S67

331 """Let the user interactively select which skills to install.""" 

332 _print_action_header("install") 21c2c3c4c5c6c7c8c9c

333 return _get_rich_toolkit().ask( 21c2c3c4c5c6c7c8c9c

334 "Select skills to install (press Space to select, Enter to confirm):", 

335 options=[ 

336 Option({"name": f"{skill.name} ({skill.package_name})", "value": skill}) 

337 for skill in skills 

338 ], 

339 allow_filtering=True, 

340 multiple=True, 

341 ) 

342 

343 

344def _select_targets_interactive( 1Q23R45S67

345 *, project_root: Path, default_targets: list[InstallTarget] 

346) -> list[InstallTarget]: 

347 """Let the user interactively select which targets to install into.""" 

348 targets = get_all_target_dirs(project_root) 2AcBcCcDcEcFcGcHcIc

349 default_names = {target.name for target in default_targets} 2AcBcCcDcEcFcGcHcIc

350 toolkit = _get_rich_toolkit() 2AcBcCcDcEcFcGcHcIc

351 menu = Menu( 2AcBcCcDcEcFcGcHcIc

352 "Select installation targets (press Space to select, Enter to confirm):", 

353 options=[ 

354 Option( 

355 { 

356 "name": _target_prompt_name(target), 

357 "value": target, 

358 } 

359 ) 

360 for target in targets 

361 ], 

362 style=toolkit.style, 

363 console=toolkit.console, 

364 multiple=True, 

365 ) 

366 menu.checked = { 2AcBcCcDcEcFcGcHcIc

367 index for index, target in enumerate(targets) if target.name in default_names 

368 } 

369 return menu.ask() 2AcBcCcDcEcFcGcHcIc

370 

371 

372def _select_install_targets( 1Q23R45S67

373 *, 

374 project_root: Path, 

375 include_claude: bool, 

376 interactive: bool, 

377) -> list[InstallTarget]: 

378 if not interactive: 2h e b G bb* + , } XbT 6b7bi f c H cb- . / ~ ZbU 8b9bg d a E db: ; = ab1bV !b#b

379 return get_target_dirs(project_root, include_claude=include_claude) 2bb} T 6b7bcb~ U 8b9bdbabV !b#b

380 return _select_targets_interactive( 2h e b G * + , Xb`d{di f c H - . / Zb|d}dg d a E : ; = 1b~dae

381 project_root=project_root, 

382 default_targets=get_default_install_target_dirs(project_root), 

383 ) 

384 

385 

386def _select_tool_skill_interactive() -> bool: 1Q23R45S67

387 toolkit = _get_rich_toolkit() 2vdwdxdydzdAdBdCdDd

388 menu = Menu( 2vdwdxdydzdAdBdCdDd

389 "Copy the Library Skills tool skill into the project so agents know how " 

390 "to update, repair, and check managed skills?", 

391 options=[ 

392 Option( 

393 { 

394 "name": "Copy Library Skills tool skill", 

395 "value": True, 

396 } 

397 ) 

398 ], 

399 style=toolkit.style, 

400 console=toolkit.console, 

401 multiple=True, 

402 ) 

403 menu.checked = {0} 2vdwdxdydzdAdBdCdDd

404 return True in menu.ask() 2vdwdxdydzdAdBdCdDd

405 

406 

407def _find_collisions(skills: list[Skill]) -> set[str]: 1Q23R45S67

408 counts = Counter(skill.name for skill in skills) 2W X K N 8 h e b k C G n !cbb* + , } XbYbI z T s u v 6b7bY Z L O 9 i f c l D H o #ccb- . / ~ Zb0bJ A U t w x 8b9b0 1 M P ! g d a j B E m $cdb: ; = ab1b2bF y V p q r !b#b

409 return {name for name, count in counts.items() if count > 1} 2W X K N 8 h e b k C G n !cbb* + , } XbYbI z T s u v 6b7bY Z L O 9 i f c l D H o #ccb- . / ~ Zb0bJ A U t w x 8b9b0 1 M P ! g d a j B E m $cdb: ; = ab1b2bF y V p q r !b#b

410 

411 

412def _deduplicate_skills(skills: list[Skill]) -> list[Skill]: 1Q23R45S67

413 seen: set[Path] = set() 2h e b icjci f c kclcg d a mcnc

414 unique: list[Skill] = [] 2h e b icjci f c kclcg d a mcnc

415 for skill in skills: 2h e b icjci f c kclcg d a mcnc

416 key = skill.skill_dir.resolve() 2h e b icjci f c kclcg d a mcnc

417 if key in seen: 2h e b icjci f c kclcg d a mcnc

418 continue 2e OePef QeRed SeTe

419 seen.add(key) 2h e b icjci f c kclcg d a mcnc

420 unique.append(skill) 2h e b icjci f c kclcg d a mcnc

421 return unique 2h e b icjci f c kclcg d a mcnc

422 

423 

424def _filter_installable_skills( 1Q23R45S67

425 skills: list[Skill], 

426 *, 

427 selected_names: list[str], 

428 include_all: bool, 

429) -> list[Skill]: 

430 collisions = _find_collisions(skills) 2h e b k C G n !cbb* + , } XbYbI z T s u v 6b7bi f c l D H o #ccb- . / ~ Zb0bJ A U t w x 8b9bg d a j B E m $cdb: ; = ab1b2bF y V p q r !b#b

431 if collisions: 2h e b k C G n !cbb* + , } XbYbI z T s u v 6b7bi f c l D H o #ccb- . / ~ Zb0bJ A U t w x 8b9bg d a j B E m $cdb: ; = ab1b2bF y V p q r !b#b

432 ui.print_warning( 2!cUeVe#cWeXe$cYeZe

433 "Skipping colliding skill names: " + ", ".join(sorted(collisions)), 

434 console=console, 

435 ) 

436 

437 installable = [skill for skill in skills if skill.name not in collisions] 2h e b k C G n !cbb* + , } XbYbI z T s u v 6b7bi f c l D H o #ccb- . / ~ Zb0bJ A U t w x 8b9bg d a j B E m $cdb: ; = ab1b2bF y V p q r !b#b

438 if selected_names: 2h e b k C G n !cbb* + , } XbYbI z T s u v 6b7bi f c l D H o #ccb- . / ~ Zb0bJ A U t w x 8b9bg d a j B E m $cdb: ; = ab1b2bF y V p q r !b#b

439 selected = set(selected_names) 2} T 6b7b~ U 8b9babV !b#b

440 return [skill for skill in installable if skill.name in selected] 2} T 6b7b~ U 8b9babV !b#b

441 if include_all: 2h e b k C G n !cbb* + , XbYbI z s u v # $ i f c l D H o #ccb- . / Zb0bJ A t w x % ' g d a j B E m $cdb: ; = 1b2bF y p q r ( )

442 return installable 2G !cbbXb0e1eH #ccbZb2e3eE $cdb1b4e5e

443 return [] 2h e b k C n * + , YbI z s u v # $ i f c l D o - . / 0bJ A t w x % ' g d a j B m : ; = 2bF y p q r ( )

444 

445 

446def _installed_statuses( 1Q23R45S67

447 *, 

448 targets: list[InstallTarget], 

449 skills: list[Skill], 

450) -> list[InstalledStatus]: 

451 skills_by_dir = {skill.skill_dir.resolve(): skill for skill in skills} 2W X K N 8 h e b k C G n {bhb] ^ LbebSbI qbrbsbIb@bz s u v # $ Y Z L O 9 i f c l D H o |bjb_ ` NbfbUbJ tbubvbJb]bA t w x % ' 0 1 M P ! g d a j B E m }blb{ | PbgbWbF wbxbybKb_by p q r ( )

452 skills_by_name = {skill.name: skill for skill in skills} 2W X K N 8 h e b k C G n {bhb] ^ LbebSbI qbrbsbIb@bz s u v # $ Y Z L O 9 i f c l D H o |bjb_ ` NbfbUbJ tbubvbJb]bA t w x % ' 0 1 M P ! g d a j B E m }blb{ | PbgbWbF wbxbybKb_by p q r ( )

453 statuses: list[InstalledStatus] = [] 2W X K N 8 h e b k C G n {bhb] ^ LbebSbI qbrbsbIb@bz s u v # $ Y Z L O 9 i f c l D H o |bjb_ ` NbfbUbJ tbubvbJb]bA t w x % ' 0 1 M P ! g d a j B E m }blb{ | PbgbWbF wbxbybKb_by p q r ( )

454 

455 for target in targets: 2W X K N 8 h e b k C G n {bhb] ^ LbebSbI qbrbsbIb@bz s u v # $ Y Z L O 9 i f c l D H o |bjb_ ` NbfbUbJ tbubvbJb]bA t w x % ' 0 1 M P ! g d a j B E m }blb{ | PbgbWbF wbxbybKb_by p q r ( )

456 for installed in list_installed_skills(target.path): 2W X K N 8 h e b k C G n {b] ^ ebI qbrbsbIbz s u v # $ Y Z L O 9 i f c l D H o |b_ ` fbJ tbubvbJbA t w x % ' 0 1 M P ! g d a j B E m }b{ | gbF wbxbybKby p q r ( )

457 if ( 

458 installed.name == TOOL_SKILL_NAME 

459 and (installed.path / TOOL_SKILL_MARKER).exists() 

460 ): 

461 continue 2z ccdcA ecfcy gchc

462 target_path = installed.target 2K k n {b] ^ ebqbrbsbIbs u v # $ L l o |b_ ` fbtbubvbJbt w x % ' M j m }b{ | gbwbxbybKbp q r ( )

463 skill = skills_by_dir.get(target_path) if target_path else None 2K k n {b] ^ ebqbrbsbIbs u v # $ L l o |b_ ` fbtbubvbJbt w x % ' M j m }b{ | gbwbxbybKbp q r ( )

464 

465 status = "hand-authored" 2K k n {b] ^ ebqbrbsbIbs u v # $ L l o |b_ ` fbtbubvbJbt w x % ' M j m }b{ | gbwbxbybKbp q r ( )

466 if installed.type == "symlink": 2K k n {b] ^ ebqbrbsbIbs u v # $ L l o |b_ ` fbtbubvbJbt w x % ' M j m }b{ | gbwbxbybKbp q r ( )

467 if target_path is None or not target_path.exists(): 2K k {b] ^ ebqbrbsbIbs u v # $ L l |b_ ` fbtbubvbJbt w x % ' M j }b{ | gbwbxbybKbp q r ( )

468 status = "broken" 2K {bs u v # $ L |bt w x % ' M }bp q r ( )

469 skill = skills_by_name.get(installed.name) 2K {bs u v # $ L |bt w x % ' M }bp q r ( )

470 elif skill: 2k {b] ^ ebqbrbsbIbs ocpcl |b_ ` fbtbubvbJbt qcrcj }b{ | gbwbxbybKbp sctc

471 status = ( 2k {b] ^ ebqbrbsbIbQdRdl |b_ ` fbtbubvbJbSdTdj }b{ | gbwbxbybKbUdVd

472 "up to date" 

473 if installed.name == skill.name 

474 else "name mismatch" 

475 ) 

476 elif installed.name in skills_by_name: 2{bs ocpc|bt qcrc}bp sctc

477 skill = skills_by_name[installed.name] 2{bbece|bdeee}bfege

478 status = "outdated" 2{bbece|bdeee}bfege

479 else: 

480 status = "orphaned" 2{bs ocpc|bt qcrc}bp sctc

481 

482 statuses.append( 2K k n {b] ^ ebqbrbsbIbs u v # $ L l o |b_ ` fbtbubvbJbt w x % ' M j m }b{ | gbwbxbybKbp q r ( )

483 InstalledStatus( 

484 target=target, 

485 name=installed.name, 

486 type=installed.type, 

487 path=installed.path, 

488 target_path=target_path, 

489 status=status, 

490 skill=skill, 

491 ) 

492 ) 

493 

494 installed_names = { 2W X K N 8 h e b k C G n {bhb] ^ LbebSbI qbrbsbIb@bz s u v # $ Y Z L O 9 i f c l D H o |bjb_ ` NbfbUbJ tbubvbJb]bA t w x % ' 0 1 M P ! g d a j B E m }blb{ | PbgbWbF wbxbybKb_by p q r ( )

495 (status.target.name, status.name) 

496 for status in statuses 

497 if status.status in {"up to date", "outdated", "broken", "name mismatch"} 

498 } 

499 for target in targets: 2W X K N 8 h e b k C G n {bhb] ^ LbebSbI qbrbsbIb@bz s u v # $ Y Z L O 9 i f c l D H o |bjb_ ` NbfbUbJ tbubvbJb]bA t w x % ' 0 1 M P ! g d a j B E m }blb{ | PbgbWbF wbxbybKb_by p q r ( )

500 for skill in skills: 2W X K N 8 h e b k C G n {b] ^ ebI qbrbsbIbz s u v # $ Y Z L O 9 i f c l D H o |b_ ` fbJ tbubvbJbA t w x % ' 0 1 M P ! g d a j B E m }b{ | gbF wbxbybKby p q r ( )

501 if (target.name, skill.name) not in installed_names: 2h e b k G {b] ^ ebqbrbsbIbu v # $ i f c l H |b_ ` fbtbubvbJbw x % ' g d a j E }b{ | gbwbxbybKbq r ( )

502 statuses.append( 1hebGuv#$ifcHwx%'gdaEqr()

503 InstalledStatus( 

504 target=target, 

505 name=skill.name, 

506 type="missing", 

507 path=target.path / skill.name, 

508 target_path=skill.skill_dir, 

509 status="new", 

510 skill=skill, 

511 ) 

512 ) 

513 return statuses 2W X K N 8 h e b k C G n {bhb] ^ LbebSbI qbrbsbIb@bz s u v # $ Y Z L O 9 i f c l D H o |bjb_ ` NbfbUbJ tbubvbJb]bA t w x % ' 0 1 M P ! g d a j B E m }blb{ | PbgbWbF wbxbybKb_by p q r ( )

514 

515 

516def _print_status_table(statuses: list[InstalledStatus], project_root: Path) -> None: 1Q23R45S67

517 ui.print_table( 1KhebkGn]^suv#$LifclHo_`twx%'MgdajEm{|pqr()

518 ui.status_table( 

519 [ 

520 ( 

521 status.target.name, 

522 status.name, 

523 status.status, 

524 _display_path(status.path, project_root), 

525 _display_path(status.target_path, project_root), 

526 ) 

527 for status in statuses 

528 ] 

529 ), 

530 console=console, 

531 ) 

532 

533 

534def _print_tool_skill_status_table( 1Q23R45S67

535 statuses: list[ToolSkillStatus], project_root: Path 

536) -> None: 

537 ui.print_table( 2N b k n z McNcO c l o A OcPcP a j m y QcRc

538 ui.tool_skill_status_table( 

539 [ 

540 ( 

541 status.target.name, 

542 status.status, 

543 _display_path(status.path, project_root), 

544 ) 

545 for status in statuses 

546 ] 

547 ), 

548 console=console, 

549 ) 

550 

551 

552def _print_installed_skills_table( 1Q23R45S67

553 statuses: list[InstalledStatus], project_root: Path 

554) -> None: 

555 installed = [status for status in statuses if status.type != "missing"] 2hb] ^ Lb)c*cjb_ ` Nb+c,clb{ | Pb-c.c

556 if not installed: 2hb] ^ Lb)c*cjb_ ` Nb+c,clb{ | Pb-c.c

557 ui.print_message("No skills installed.", console=console) 2hbLb)c*cjbNb+c,clbPb-c.c

558 return 2hbLb)c*cjbNb+c,clbPb-c.c

559 _print_status_table(installed, project_root) 2] ^ 6e7e_ ` 8e9e{ | !e#e

560 

561 

562def _select_installed_skills_interactive( 1Q23R45S67

563 statuses: list[InstalledStatus], 

564) -> list[InstalledStatus]: 

565 """Let the user interactively select which installed skills to remove.""" 

566 removable = [status for status in statuses if status.type == "symlink"] 21c2c3c4c5c6c7c8c9c

567 if not removable: 21c2c3c4c5c6c7c8c9c

568 return [] 21c2c3c4c5c6c7c8c9c

569 return _get_rich_toolkit().ask( 21c2c3c4c5c6c7c8c9c

570 "Select skills to remove (press Space to select, Enter to confirm):", 

571 options=[ 

572 Option( 

573 { 

574 "name": f"{status.name} [{_target_short_name(status.target)}]", 

575 "value": status, 

576 } 

577 ) 

578 for status in removable 

579 ], 

580 allow_filtering=True, 

581 multiple=True, 

582 ) 

583 

584 

585def _sync_target_dirs( 1Q23R45S67

586 *, project_root: Path, include_claude: bool, yes: bool, check: bool 

587) -> list[InstallTarget]: 

588 targets_by_name = { 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()

589 target.name: target for target in get_existing_target_dirs(project_root) 

590 } 

591 default_targets = ( 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()

592 get_target_dirs(project_root, include_claude=include_claude) 

593 if yes or check or include_claude 

594 else get_default_install_target_dirs(project_root) 

595 ) 

596 for target in default_targets: 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()

597 targets_by_name[target.name] = target 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()

598 return list(targets_by_name.values()) 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()

599 

600 

601def _repairable_statuses(statuses: list[InstalledStatus]) -> list[InstalledStatus]: 1Q23R45S67

602 return [ 1hebkCGnIzsuv#$ifclDHoJAtwx%'gdajBEmFypqr()

603 status 

604 for status in statuses 

605 if status.type == "symlink" 

606 and status.status in {"broken", "outdated"} 

607 and status.skill is not None 

608 ] 

609 

610 

611def _removable_statuses(statuses: list[InstalledStatus]) -> list[InstalledStatus]: 1Q23R45S67

612 return [ 1hebkCGnIzsuv#$ifclDHoJAtwx%'gdajBEmFypqr()

613 status 

614 for status in statuses 

615 if status.type == "symlink" 

616 and ( 

617 status.status == "orphaned" 

618 or (status.status == "broken" and status.skill is None) 

619 ) 

620 ] 

621 

622 

623def _select_statuses_interactive( 1Q23R45S67

624 statuses: list[InstalledStatus], *, action: str 

625) -> list[InstalledStatus]: 

626 if not statuses: 2h e b k C G n I ~bz ccdci f c l D H o J acA ecfcg d a j B E m F bcy gchc

627 return [] 2h e b k C G n I ~bz ccdci f c l D H o J acA ecfcg d a j B E m F bcy gchc

628 _print_action_header(action) 2~b/c:cac;c=cbc?c@c

629 toolkit = _get_rich_toolkit() 2~b/c:cac;c=cbc?c@c

630 menu = Menu( 2~b/c:cac;c=cbc?c@c

631 f"Select skills to {action} (press Space to select, Enter to confirm):", 

632 options=[ 

633 Option( 

634 { 

635 "name": f"{status.name} [{_target_short_name(status.target)}]", 

636 "value": status, 

637 } 

638 ) 

639 for status in statuses 

640 ], 

641 style=toolkit.style, 

642 console=toolkit.console, 

643 multiple=True, 

644 ) 

645 menu.checked = set(range(len(statuses))) 2~b/c:cac;c=cbc?c@c

646 return menu.ask() 2~b/c:cac;c=cbc?c@c

647 

648 

649def _repair_selected(*, statuses: list[InstalledStatus], project_root: Path) -> int: 1Q23R45S67

650 repaired_count = 0 2~bu v # $ acw x % ' bcq r ( )

651 for status in statuses: 2~bu v # $ acw x % ' bcq r ( )

652 if status.skill is None: 2~bu v # $ acw x % ' bcq r ( )

653 continue 2~b/c:cac;c=cbc?c@c

654 install_skill(status.skill, status.target.path) 1uv#$wx%'qr()

655 ui.print_result( 1uv#$wx%'qr()

656 "Repaired", 

657 status.name, 

658 status.target.name, 

659 _display_path(status.path, project_root), 

660 console=console, 

661 ) 

662 repaired_count += 1 1uv#$wx%'qr()

663 return repaired_count 2~bu v # $ acw x % ' bcq r ( )

664 

665 

666def _remove_selected(*, statuses: list[InstalledStatus], project_root: Path) -> int: 1Q23R45S67

667 removed_count = 0 2~bs ocpcact qcrcbcp sctc

668 for status in statuses: 2~bs ocpcact qcrcbcp sctc

669 if uninstall_skill(status.name, status.target.path): 2~bs ocpcact qcrcbcp sctc

670 ui.print_result( 2s ocpct qcrcp sctc

671 f"Removed {status.status} symlink", 

672 status.name, 

673 status.target.name, 

674 _display_path(status.path, project_root), 

675 console=console, 

676 ) 

677 removed_count += 1 2s ocpct qcrcp sctc

678 else: 

679 ui.print_message( 2~b/c:cac;c=cbc?c@c

680 f"Not found: [bold]{status.name}[/bold] ({status.target.name})", 

681 console=console, 

682 ) 

683 return removed_count 2~bs ocpcact qcrcbcp sctc

684 

685 

686def _install_selected( 1Q23R45S67

687 *, 

688 skills: list[Skill], 

689 targets: list[InstallTarget], 

690 project_root: Path, 

691 copy: bool = False, 

692) -> int: 

693 installed_count = 0 2h e b bb* + , Sc} T 6b7bi f c cb- . / Tc~ U 8b9bg d a db: ; = UcabV !b#b

694 for target in targets: 2h e b bb* + , Sc} T 6b7bi f c cb- . / Tc~ U 8b9bg d a db: ; = UcabV !b#b

695 for skill in skills: 2h e b bb* + , Sc} T 6b7bi f c cb- . / Tc~ U 8b9bg d a db: ; = UcabV !b#b

696 try: 2h e b bb* + , Sc} T 6b7bi f c cb- . / Tc~ U 8b9bg d a db: ; = UcabV !b#b

697 dest = install_skill(skill, target.path, copy=copy) 2h e b bb* + , Sc} T 6b7bi f c cb- . / Tc~ U 8b9bg d a db: ; = UcabV !b#b

698 except InstallError as e: 2ScWdXdTcYdZdUc0d1d

699 ui.print_skipped(skill.name, str(e), console=console) 2ScWdXdTcYdZdUc0d1d

700 continue 2ScWdXdTcYdZdUc0d1d

701 action = "Copied" if copy else "Installed" 2h e b bb* + , } T 6b7bi f c cb- . / ~ U 8b9bg d a db: ; = abV !b#b

702 ui.print_result( 2h e b bb* + , } T 6b7bi f c cb- . / ~ U 8b9bg d a db: ; = abV !b#b

703 action, 

704 skill.name, 

705 skill.package_name, 

706 _display_path(dest, project_root), 

707 console=console, 

708 ) 

709 installed_count += 1 2h e b bb* + , } T 6b7bi f c cb- . / ~ U 8b9bg d a db: ; = abV !b#b

710 if installed_count and not copy: 2h e b bb* + , Sc} T 6b7bi f c cb- . / Tc~ U 8b9bg d a db: ; = UcabV !b#b

711 ui.print_hint( 2e b * + , } T 6b7bf c - . / ~ U 8b9bd a : ; = abV !b#b

712 "These relative symlinks can be committed to Git when your project " 

713 "uses stable repo-local installs. They resolve after dependencies " 

714 "are installed.", 

715 console=console, 

716 ) 

717 return installed_count 2h e b bb* + , Sc} T 6b7bi f c cb- . / Tc~ U 8b9bg d a db: ; = UcabV !b#b

718 

719 

720def _sync_tool_skill( 1Q23R45S67

721 *, 

722 targets: list[InstallTarget], 

723 project_root: Path, 

724 check: bool, 

725 explicit: bool, 

726) -> tuple[int, bool]: 

727 statuses = [inspect_tool_skill(target.path) for target in targets] 2N b k n z McNcO c l o A OcPcP a j m y QcRc

728 _print_tool_skill_status_table(statuses, project_root) 2N b k n z McNcO c l o A OcPcP a j m y QcRc

729 

730 drift = [status for status in statuses if status.status != "tool skill: up to date"] 2N b k n z McNcO c l o A OcPcP a j m y QcRc

731 if check: 2N b k n z McNcO c l o A OcPcP a j m y QcRc

732 return 0, bool(drift) 2N McNcO OcPcP QcRc

733 

734 changed_count = 0 2b k n z ccdcc l o A ecfca j m y gchc

735 failed = False 2b k n z ccdcc l o A ecfca j m y gchc

736 for status in drift: 2b k n z ccdcc l o A ecfca j m y gchc

737 try: 2b k n z ccdcc l o A ecfca j m y gchc

738 install_tool_skill(status.target.path) 2b k n z ccdcc l o A ecfca j m y gchc

739 except ToolSkillError as e: 2n jdkdo ldmdm ndod

740 ui.print_skipped("tool skill", str(e), console=console) 2n jdkdo ldmdm ndod

741 failed = failed or explicit 2n jdkdo ldmdm ndod

742 continue 2n jdkdo ldmdm ndod

743 ui.print_result( 2b k z ccdcc l A ecfca j y gchc

744 "Copied", 

745 "library-skills", 

746 status.target.name, 

747 _display_path(status.path, project_root), 

748 console=console, 

749 ) 

750 changed_count += 1 2b k z ccdcc l A ecfca j y gchc

751 return changed_count, failed 2b k n z ccdcc l o A ecfca j m y gchc

752 

753 

754def _tool_skill_is_missing(targets: list[InstallTarget]) -> bool: 1Q23R45S67

755 return any( 2h e b k C heiei f c l D jekeg d a j B leme

756 inspect_tool_skill(target.path).status == "tool skill: missing" 

757 for target in targets 

758 ) 

759 

760 

761def _sync( 1Q23R45S67

762 *, 

763 include_claude: bool, 

764 yes: bool, 

765 check: bool, 

766 include_all: bool, 

767 selected_names: list[str], 

768 copy: bool, 

769 tool_skill: bool | None, 

770) -> None: 

771 context = _get_project_context() 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()

772 result = _scan_context(context) 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()

773 targets = _sync_target_dirs( 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()

774 project_root=context.project_root, 

775 include_claude=include_claude, 

776 yes=yes, 

777 check=check, 

778 ) 

779 

780 _print_context(context) 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()

781 ui.print_line(console=console) 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()

782 _print_warnings(result.warnings) 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()

783 if result.warnings: 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()

784 ui.print_line(console=console) 28 $e%e9 'e(e! )e*e

785 

786 collisions = _find_collisions(result.skills) 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()

787 candidate_skills = _top_level_skills( 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()

788 context=context, 

789 skills=result.skills, 

790 include_all=include_all or bool(selected_names), 

791 ) 

792 candidate_skill_names = {skill.name for skill in candidate_skills} 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()

793 statuses = _installed_statuses( 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()

794 targets=targets, 

795 skills=[skill for skill in result.skills if skill.name not in collisions], 

796 ) 

797 visible_statuses = [ 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()

798 status 

799 for status in statuses 

800 if status.status != "new" or status.name in candidate_skill_names 

801 ] 

802 

803 if visible_statuses: 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()

804 ui.print_title("status", console=console) 1KhebkGnsuv#$LifclHotwx%'MgdajEmpqr()

805 _print_status_table(visible_statuses, context.project_root) 1KhebkGnsuv#$LifclHotwx%'MgdajEmpqr()

806 else: 

807 ui.print_message("No installed or discovered skills found.", console=console) 2W X N 8 C I z neoeY Z O 9 D J A peqe0 1 P ! B F y rese

808 

809 drift = [ 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()

810 status 

811 for status in statuses 

812 if status.status in {"broken", "outdated", "name mismatch", "orphaned"} 

813 ] 

814 if check: 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()

815 if tool_skill: 2W X K N 8 2d3dY Z L O 9 4d5d0 1 M P ! 6d7d

816 ui.print_line(console=console) 2N McNcO OcPcP QcRc

817 _, tool_failed = _sync_tool_skill( 2N McNcO OcPcP QcRc

818 targets=get_target_dirs( 

819 context.project_root, include_claude=include_claude 

820 ), 

821 project_root=context.project_root, 

822 check=True, 

823 explicit=True, 

824 ) 

825 if tool_failed: 2N McNcO OcPcP QcRc

826 raise typer.Exit(1) 2N McNcO OcPcP QcRc

827 if drift or collisions: 2W X K 8 2d3dY Z L 9 4d5d0 1 M ! 6d7d

828 raise typer.Exit(1) 2W K 2d3dY L 4d5d0 M 6d7d

829 return 2X 8 neoeZ 9 peqe1 ! rese

830 

831 repairable = _repairable_statuses(drift) 1hebkCGnIzsuv#$ifclDHoJAtwx%'gdajBEmFypqr()

832 removable = _removable_statuses(drift) 1hebkCGnIzsuv#$ifclDHoJAtwx%'gdajBEmFypqr()

833 if drift: 1hebkCGnIzsuv#$ifclDHoJAtwx%'gdajBEmFypqr()

834 ui.print_title("attention", console=console) 1suv#$twx%'pqr()

835 ui.print_warning("Some installed skills need attention.", console=console) 1suv#$twx%'pqr()

836 ui.print_message( 1suv#$twx%'pqr()

837 "Select the skills to install, repair, or remove.", console=console 

838 ) 

839 ui.print_hint("Only managed symlinks will be changed.", console=console) 1suv#$twx%'pqr()

840 

841 selected_repairs = ( 1hebkCGnIzsuv#$ifclDHoJAtwx%'gdajBEmFypqr()

842 repairable if yes else _select_statuses_interactive(repairable, action="repair") 

843 ) 

844 selected_removals = ( 1hebkCGnIzsuv#$ifclDHoJAtwx%'gdajBEmFypqr()

845 removable if yes else _select_statuses_interactive(removable, action="remove") 

846 ) 

847 if selected_repairs: 1hebkCGnIzsuv#$ifclDHoJAtwx%'gdajBEmFypqr()

848 ui.print_line(console=console) 1uv#$wx%'qr()

849 _repair_selected(statuses=selected_repairs, project_root=context.project_root) 1uv#$wx%'qr()

850 if selected_removals: 1hebkCGnIzsuv#$ifclDHoJAtwx%'gdajBEmFypqr()

851 ui.print_line(console=console) 2s ocpct qcrcp sctc

852 _remove_selected(statuses=selected_removals, project_root=context.project_root) 2s ocpct qcrcp sctc

853 

854 selected = _filter_installable_skills( 1hebkCGnIzsuv#$ifclDHoJAtwx%'gdajBEmFypqr()

855 candidate_skills, 

856 selected_names=selected_names, 

857 include_all=include_all, 

858 ) 

859 if ( 1gdajBEmFypqr

860 not selected 

861 and not yes 

862 and not selected_names 

863 and not include_all 

864 and tool_skill is not True 

865 ): 

866 new_skills = [ 2h e b k C I 8d9di f c l D J !d#dg d a j B F $d%d

867 status.skill 

868 for status in visible_statuses 

869 if status.status == "new" and status.skill is not None 

870 ] 

871 if new_skills: 2h e b k C I 8d9di f c l D J !d#dg d a j B F $d%d

872 selected = _select_skills_interactive(_deduplicate_skills(new_skills)) 2h e b icjci f c kclcg d a mcnc

873 

874 tool_skill_changes = 0 1hebkCGnIzsuv#$ifclDHoJAtwx%'gdajBEmFypqr()

875 if selected: 1hebkCGnIzsuv#$ifclDHoJAtwx%'gdajBEmFypqr()

876 targets = _select_install_targets( 2h e b G EdFdi f c H GdHdg d a E IdJd

877 project_root=context.project_root, 

878 include_claude=include_claude, 

879 interactive=not yes and not include_claude, 

880 ) 

881 if not targets: 2h e b G EdFdi f c H GdHdg d a E IdJd

882 ui.print_message("No installation targets selected.", console=console) 2G EdFdH GdHdE IdJd

883 return 2G EdFdH GdHdE IdJd

884 ui.print_line(console=console) 2h e b icjci f c kclcg d a mcnc

885 installed_count = _install_selected( 2h e b icjci f c kclcg d a mcnc

886 skills=selected, 

887 targets=targets, 

888 project_root=context.project_root, 

889 copy=copy, 

890 ) 

891 ui.print_line(console=console) 2h e b icjci f c kclcg d a mcnc

892 ui.print_summary({"installed skill targets": installed_count}, console=console) 2h e b icjci f c kclcg d a mcnc

893 if tool_skill is True: 1hebkCnIzsuv#$ifclDoJAtwx%'gdajBmFypqr()

894 ui.print_line(console=console) 2n z ccdco A ecfcm y gchc

895 tool_skill_changes, tool_failed = _sync_tool_skill( 2n z ccdco A ecfcm y gchc

896 targets=targets, 

897 project_root=context.project_root, 

898 check=False, 

899 explicit=True, 

900 ) 

901 if tool_failed: 2n z ccdco A ecfcm y gchc

902 raise typer.Exit(1) 2n jdkdo ldmdm ndod

903 elif tool_skill is None and not yes and _tool_skill_is_missing(targets): 1hebkCIsuv#$ifclDJtwx%'gdajBFpqr()

904 if _select_tool_skill_interactive(): 2h e b k C heiei f c l D jekeg d a j B leme

905 ui.print_line(console=console) 2b k QdRdc l SdTda j UdVd

906 tool_skill_changes, _ = _sync_tool_skill( 2b k QdRdc l SdTda j UdVd

907 targets=targets, 

908 project_root=context.project_root, 

909 check=False, 

910 explicit=False, 

911 ) 

912 if ( 1gdajBFypqr

913 not selected_repairs 

914 and not selected_removals 

915 and not selected 

916 and not tool_skill_changes 

917 ): 

918 ui.print_summary({}, console=console) 2C I 8d9dD J !d#dB F $d%d

919 

920 

921@app.callback() 1Q23R45S67

922def callback( 1Q23R45S67

923 ctx: typer.Context, 

924 include_claude: Annotated[ 

925 bool, 

926 typer.Option( 

927 "--claude", 

928 help="Also install/manage skills in .claude/skills/ alongside .agents/skills/", 

929 ), 

930 ] = False, 

931 yes: Annotated[ 

932 bool, typer.Option("--yes", "-y", help="Skip confirmation prompts") 

933 ] = False, 

934 check: Annotated[ 

935 bool, typer.Option("--check", help="Validate only; exit 1 if installs drift") 

936 ] = False, 

937 include_all: Annotated[ 

938 bool, 

939 typer.Option("--all", help="Install all newly discovered unmanaged skills"), 

940 ] = False, 

941 selected_names: Annotated[ 

942 list[str] | None, 

943 typer.Option( 

944 "--skill", "-s", help="Install a specific discovered skill by name" 

945 ), 

946 ] = None, 

947 copy: Annotated[ 

948 bool, typer.Option("--copy", help="Copy files instead of creating symlinks") 

949 ] = False, 

950 tool_skill: Annotated[ 

951 bool | None, 

952 typer.Option( 

953 "--tool-skill/--no-tool-skill", 

954 help="Copy the Library Skills tool skill into the project", 

955 ), 

956 ] = None, 

957) -> None: 

958 """Discover and reconcile agent skills from installed library packages.""" 

959 if ctx.invoked_subcommand is None: 2W X K N Rb3b8 h e b k C G n bb* + , } XbYbhb] ^ LbebSbI (b)b*b$bnbqbrbsbIb@bzb? Mbz T [b+bibs u v AbBbY Z L O Tb4b9 i f c l D H o cb- . / ~ Zb0bjb_ ` NbfbUbJ ,b-b.b%bobtbubvbJb]bCb@ ObA U ^b/bkbt w x DbEb0 1 M P Vb5b! g d a j B E m db: ; = ab1b2blb{ | PbgbWbF :b;b=b'bpbwbxbybKb_bFb[ Qby V `b?bmbp q r GbHb

960 _sync( 1WXKN8hebkCGnIzsuv#$YZLO9ifclDHoJAtwx%'01MP!gdajBEmFypqr()

961 include_claude=include_claude, 

962 yes=yes, 

963 check=check, 

964 include_all=include_all, 

965 selected_names=selected_names or [], 

966 copy=copy, 

967 tool_skill=tool_skill, 

968 ) 

969 

970 

971@app.command() 1Q23R45S67

972def scan( 1Q23R45S67

973 json_output: Annotated[bool, typer.Option("--json", help="Output as JSON")] = False, 

974 include_all: Annotated[ 

975 bool, 

976 typer.Option("--all", help="Include skills from transitive dependencies"), 

977 ] = False, 

978) -> None: 

979 """Discover skills in installed packages.""" 

980 context = _get_project_context() 2Rb3b(b)b*b$bnbzb? MbT [b+bibAbBbTb4b,b-b.b%bobCb@ ObU ^b/bkbDbEbVb5b:b;b=b'bpbFb[ QbV `b?bmbGbHb

981 result = _scan_context(context) 2Rb3b(b)b*b$bnbzb? MbT [b+bibAbBbTb4b,b-b.b%bobCb@ ObU ^b/bkbDbEbVb5b:b;b=b'bpbFb[ QbV `b?bmbGbHb

982 skills = _top_level_skills( 2Rb3b(b)b*b$bnbzb? MbT [b+bibAbBbTb4b,b-b.b%bobCb@ ObU ^b/bkbDbEbVb5b:b;b=b'bpbFb[ QbV `b?bmbGbHb

983 context=context, 

984 skills=result.skills, 

985 include_all=include_all, 

986 ) 

987 if json_output: 2Rb3b(b)b*b$bnbzb? MbT [b+bibAbBbTb4b,b-b.b%bobCb@ ObU ^b/bkbDbEbVb5b:b;b=b'bpbFb[ QbV `b?bmbGbHb

988 payload = _scan_json_payload(context=context, skills=skills, result=result) 2Rb3b(b)b*b$b? T [b+b6b7bTb4b,b-b.b%b@ U ^b/b8b9bVb5b:b;b=b'b[ V `b?b!b#b

989 print(json.dumps(payload, indent=2)) 2Rb3b(b)b*b$b? T [b+b6b7bTb4b,b-b.b%b@ U ^b/b8b9bVb5b:b;b=b'b[ V `b?b!b#b

990 return 2Rb3b(b)b*b$b? T [b+b6b7bTb4b,b-b.b%b@ U ^b/b8b9bVb5b:b;b=b'b[ V `b?b!b#b

991 

992 _print_context(context) 2nbzb? MbibAbBbobCb@ ObkbDbEbpbFb[ QbmbGbHb

993 ui.print_line(console=console) 2nbzb? MbibAbBbobCb@ ObkbDbEbpbFb[ QbmbGbHb

994 _print_warnings(result.warnings) 2nbzb? MbibAbBbobCb@ ObkbDbEbpbFb[ QbmbGbHb

995 if result.warnings: 2nbzb? MbibAbBbobCb@ ObkbDbEbpbFb[ QbmbGbHb

996 ui.print_line(console=console) 2MbddedObfdgdQbhdid

997 _print_skills_table(skills) 2nbzb? MbibAbBbobCb@ ObkbDbEbpbFb[ QbmbGbHb

998 

999 

1000@app.command("list") 1Q23R45S67

1001def list_cmd( 1Q23R45S67

1002 installed: Annotated[ 

1003 bool, typer.Option("--installed", help="Only show installed skills") 

1004 ] = False, 

1005 json_output: Annotated[bool, typer.Option("--json", help="Output as JSON")] = False, 

1006 include_claude: Annotated[ 

1007 bool, 

1008 typer.Option( 

1009 "--claude", 

1010 help="Also include .claude/skills/ alongside .agents/skills/", 

1011 ), 

1012 ] = False, 

1013 include_all: Annotated[ 

1014 bool, 

1015 typer.Option("--all", help="Include skills from transitive dependencies"), 

1016 ] = False, 

1017) -> None: 

1018 """List discovered or currently installed skills.""" 

1019 context = _get_project_context() 2hb] ^ LbebSbVcWcjb_ ` NbfbUbXcYclb{ | PbgbWbZc0c

1020 result = _scan_context(context) 2hb] ^ LbebSbVcWcjb_ ` NbfbUbXcYclb{ | PbgbWbZc0c

1021 skills = _top_level_skills( 2hb] ^ LbebSbVcWcjb_ ` NbfbUbXcYclb{ | PbgbWbZc0c

1022 context=context, 

1023 skills=result.skills, 

1024 include_all=include_all, 

1025 ) 

1026 targets = get_existing_target_dirs( 2hb] ^ LbebSbVcWcjb_ ` NbfbUbXcYclb{ | PbgbWbZc0c

1027 context.project_root, include_claude=include_claude 

1028 ) 

1029 statuses = _installed_statuses(targets=targets, skills=result.skills) 2hb] ^ LbebSbVcWcjb_ ` NbfbUbXcYclb{ | PbgbWbZc0c

1030 

1031 if json_output: 2hb] ^ LbebSbVcWcjb_ ` NbfbUbXcYclb{ | PbgbWbZc0c

1032 payload = _scan_json_payload(context=context, skills=skills, result=result) 2ebVcWcfbXcYcgbZc0c

1033 payload.update( 2ebVcWcfbXcYcgbZc0c

1034 { 

1035 "installed": [ 

1036 { 

1037 "target": status.target.name, 

1038 "name": status.name, 

1039 "status": status.status, 

1040 "path": str(status.path), 

1041 "source": str(status.target_path or ""), 

1042 } 

1043 for status in statuses 

1044 if status.type != "missing" 

1045 ], 

1046 } 

1047 ) 

1048 print(json.dumps(payload, indent=2)) 2ebVcWcfbXcYcgbZc0c

1049 return 2ebVcWcfbXcYcgbZc0c

1050 

1051 _print_warnings(result.warnings) 2hb] ^ LbSb)c*cjb_ ` NbUb+c,clb{ | PbWb-c.c

1052 if installed: 2hb] ^ LbSb)c*cjb_ ` NbUb+c,clb{ | PbWb-c.c

1053 _print_installed_skills_table(statuses, context.project_root) 2hb] ^ Lb)c*cjb_ ` Nb+c,clb{ | Pb-c.c

1054 else: 

1055 _print_skills_table(skills) 2Sb+e,eUb-e.eWb/e:e

1056 

1057 

1058@app.command() 1Q23R45S67

1059def install( 1Q23R45S67

1060 include_claude: Annotated[ 

1061 bool, 

1062 typer.Option( 

1063 "--claude", 

1064 help="Also install skills in .claude/skills/ alongside .agents/skills/", 

1065 ), 

1066 ] = False, 

1067 yes: Annotated[ 

1068 bool, typer.Option("--yes", "-y", help="Skip interactive selection") 

1069 ] = False, 

1070 include_all: Annotated[ 

1071 bool, 

1072 typer.Option("--all", help="Install all newly discovered unmanaged skills"), 

1073 ] = False, 

1074 selected_names: Annotated[ 

1075 list[str] | None, 

1076 typer.Option( 

1077 "--skill", "-s", help="Install a specific discovered skill by name" 

1078 ), 

1079 ] = None, 

1080 copy: Annotated[ 

1081 bool, typer.Option("--copy", help="Copy files instead of creating symlinks") 

1082 ] = False, 

1083) -> None: 

1084 """Install skills from installed packages.""" 

1085 context = _get_project_context() 2bb* + , } XbYbT 6b7bcb- . / ~ Zb0bU 8b9bdb: ; = ab1b2bV !b#b

1086 result = _scan_context(context) 2bb* + , } XbYbT 6b7bcb- . / ~ Zb0bU 8b9bdb: ; = ab1b2bV !b#b

1087 skills = _top_level_skills( 2bb* + , } XbYbT 6b7bcb- . / ~ Zb0bU 8b9bdb: ; = ab1b2bV !b#b

1088 context=context, 

1089 skills=result.skills, 

1090 include_all=include_all or bool(selected_names), 

1091 ) 

1092 

1093 _print_warnings(result.warnings) 2bb* + , } XbYbT 6b7bcb- . / ~ Zb0bU 8b9bdb: ; = ab1b2bV !b#b

1094 selected = _filter_installable_skills( 2bb* + , } XbYbT 6b7bcb- . / ~ Zb0bU 8b9bdb: ; = ab1b2bV !b#b

1095 skills, 

1096 selected_names=selected_names or [], 

1097 include_all=include_all, 

1098 ) 

1099 if not selected and not yes: 2bb* + , } XbYbT 6b7bcb- . / ~ Zb0bU 8b9bdb: ; = ab1b2bV !b#b

1100 selected = _select_skills_interactive(skills) 2* + , `d{d- . / |d}d: ; = ~dae

1101 

1102 if not selected: 2bb* + , } XbYbT 6b7bcb- . / ~ Zb0bU 8b9bdb: ; = ab1b2bV !b#b

1103 ui.print_message("No skills selected.", console=console) 2Ybteue0bvewe2bxeye

1104 return 2Ybteue0bvewe2bxeye

1105 

1106 targets = _select_install_targets( 2bb* + , } XbT 6b7bcb- . / ~ ZbU 8b9bdb: ; = ab1bV !b#b

1107 project_root=context.project_root, 

1108 include_claude=include_claude, 

1109 interactive=not yes and not include_claude, 

1110 ) 

1111 if not targets: 2bb* + , } XbT 6b7bcb- . / ~ ZbU 8b9bdb: ; = ab1bV !b#b

1112 ui.print_message("No installation targets selected.", console=console) 2XbzeAeZbBeCe1bDeEe

1113 return 2XbzeAeZbBeCe1bDeEe

1114 

1115 installed_count = _install_selected( 2bb* + , } T 6b7bcb- . / ~ U 8b9bdb: ; = abV !b#b

1116 skills=selected, targets=targets, project_root=context.project_root, copy=copy 

1117 ) 

1118 ui.print_line(console=console) 2bb* + , } T 6b7bcb- . / ~ U 8b9bdb: ; = abV !b#b

1119 ui.print_summary({"installed skill targets": installed_count}, console=console) 2bb* + , } T 6b7bcb- . / ~ U 8b9bdb: ; = abV !b#b

1120 

1121 

1122@app.command() 1Q23R45S67

1123def remove( 1Q23R45S67

1124 skill_names: Annotated[ 

1125 list[str] | None, typer.Argument(help="Names of skills to remove") 

1126 ] = None, 

1127 include_claude: Annotated[ 

1128 bool, 

1129 typer.Option( 

1130 "--claude", 

1131 help="Also remove from .claude/skills/ alongside .agents/skills/", 

1132 ), 

1133 ] = False, 

1134 yes: Annotated[ 

1135 bool, typer.Option("--yes", "-y", help="Skip interactive selection") 

1136 ] = False, 

1137) -> None: 

1138 """Remove installed symlinked skills.""" 

1139 context = _get_project_context() 2hbqbrbsbIb@bucvcjbtbubvbJb]bwcxclbwbxbybKb_byczc

1140 result = _scan_context(context) 2hbqbrbsbIb@bucvcjbtbubvbJb]bwcxclbwbxbybKb_byczc

1141 targets = get_existing_target_dirs( 2hbqbrbsbIb@bucvcjbtbubvbJb]bwcxclbwbxbybKb_byczc

1142 context.project_root, include_claude=include_claude 

1143 ) 

1144 statuses = _installed_statuses(targets=targets, skills=result.skills) 2hbqbrbsbIb@bucvcjbtbubvbJb]bwcxclbwbxbybKb_byczc

1145 

1146 selected_statuses: list[InstalledStatus] = [] 2hbqbrbsbIb@bucvcjbtbubvbJb]bwcxclbwbxbybKb_byczc

1147 if skill_names: 2hbqbrbsbIb@bucvcjbtbubvbJb]bwcxclbwbxbybKb_byczc

1148 selected = set(skill_names) 2qbsbIbucvctbvbJbwcxcwbybKbyczc

1149 selected_statuses = [ 2qbsbIbucvctbvbJbwcxcwbybKbyczc

1150 status 

1151 for status in statuses 

1152 if status.name in selected and status.type == "symlink" 

1153 ] 

1154 elif not yes: 2hbrb@b'd(djbub]b)d*dlbxb_b+d,d

1155 selected_statuses = _select_installed_skills_interactive(statuses) 2rb;e=eub?e@exb[e]e

1156 

1157 if not selected_statuses: 2hbqbrbsbIb@bucvcjbtbubvbJb]bwcxclbwbxbybKb_byczc

1158 ui.print_message("No skills selected.", console=console) 2hb@b'd(djb]b)d*dlb_b+d,d

1159 return 2hb@b'd(djb]b)d*dlb_b+d,d

1160 

1161 for status in selected_statuses: 2qbrbsbIbucvctbubvbJbwcxcwbxbybKbyczc

1162 if uninstall_skill(status.name, status.target.path): 2qbrbsbIbucvctbubvbJbwcxcwbxbybKbyczc

1163 ui.print_result( 2qbrbsbucvctbubvbwcxcwbxbybyczc

1164 "Removed", 

1165 status.name, 

1166 status.target.name, 

1167 _display_path(status.path, context.project_root), 

1168 console=console, 

1169 ) 

1170 else: 

1171 ui.print_message( 2Ib^e_eJb`e{eKb|e}e

1172 f"Not found: [bold]{status.name}[/bold] ({status.target.name})", 

1173 console=console, 

1174 ) 

1175 

1176 

1177def main() -> None: 1Q23R45S67

1178 """CLI entry point.""" 

1179 app() 2~eafbfcfdfefffgfhf