1111logger = structlog .get_logger (__name__ )
1212
1313
14+ def _find_similar_plugin_names (target_name : str , available_names : list [str ], max_suggestions : int = 3 ) -> list [str ]:
15+ """Find similar plugin names using simple string similarity."""
16+ if not available_names :
17+ return []
18+
19+ suggestions = []
20+
21+ # 1. Exact match (case-insensitive)
22+ for name in available_names :
23+ if name .lower () == target_name .lower ():
24+ return [name ] # Exact match found
25+
26+ # 2. Simple character replacement (- <-> _)
27+ target_normalized = target_name .replace ("_" , "-" )
28+ for name in available_names :
29+ name_normalized = name .replace ("_" , "-" )
30+ if target_normalized .lower () == name_normalized .lower ():
31+ suggestions .append (name )
32+
33+ # 3. Basic edit distance for other close matches
34+ if len (suggestions ) < max_suggestions :
35+
36+ def simple_edit_distance (s1 : str , s2 : str ) -> int :
37+ """Simple Levenshtein distance calculation."""
38+ if len (s1 ) < len (s2 ):
39+ return simple_edit_distance (s2 , s1 )
40+ if len (s2 ) == 0 :
41+ return len (s1 )
42+
43+ previous_row = list (range (len (s2 ) + 1 ))
44+ for i , c1 in enumerate (s1 ):
45+ current_row = [i + 1 ]
46+ for j , c2 in enumerate (s2 ):
47+ insertions = previous_row [j + 1 ] + 1
48+ deletions = current_row [j ] + 1
49+ substitutions = previous_row [j ] + (c1 != c2 )
50+ current_row .append (min (insertions , deletions , substitutions ))
51+ previous_row = current_row
52+ return previous_row [- 1 ]
53+
54+ # Calculate similarity for remaining names
55+ similarities = []
56+ for name in available_names :
57+ if name not in suggestions :
58+ distance = simple_edit_distance (target_name .lower (), name .lower ())
59+ # Only suggest if distance is reasonable (less than half the length)
60+ if distance <= min (len (target_name ), len (name )) // 2 :
61+ similarities .append ((name , distance ))
62+
63+ # Sort by distance and add to suggestions
64+ similarities .sort (key = lambda x : x [1 ])
65+ suggestions .extend ([name for name , _ in similarities [: max_suggestions - len (suggestions )]])
66+
67+ return suggestions [:max_suggestions ]
68+
69+
1470@click .command ()
1571@click .option ("--dry-run" , is_flag = True , help = "Show what would be changed without making changes" )
1672@click .pass_context
@@ -86,8 +142,8 @@ def sync(ctx, dry_run: bool):
86142 fg = "yellow" ,
87143 )
88144
89- installed_plugins [plugin_info [ "package" ] ] = {
90- "plugin_name " : plugin_name ,
145+ installed_plugins [plugin_name ] = {
146+ "package_name " : plugin_info [ "package" ] ,
91147 "version" : plugin_info ["version" ],
92148 "capabilities" : capabilities ,
93149 }
@@ -98,46 +154,41 @@ def sync(ctx, dry_run: bool):
98154 click .secho (f"Failed to discover installed plugins: { e } " , fg = "red" )
99155 ctx .exit (1 )
100156
101- # Calculate changes needed - use package names for consistent comparison
102- installed_package_names = set (installed_plugins .keys ())
157+ # Calculate changes needed - use plugin (entry point) names for comparison
158+ installed_plugin_names = set (installed_plugins .keys ())
103159
104160 # Plugins to add (installed but not in config)
105- packages_to_add = installed_package_names - current_plugins
161+ plugins_to_add = installed_plugin_names - current_plugins
106162
107163 # Plugins to remove (in config but not installed)
108- packages_to_remove = current_plugins - installed_package_names
164+ plugins_to_remove = current_plugins - installed_plugin_names
109165
110- # Get packages to add (package names that need to be added)
111- plugins_to_add = []
112- for package_name , info in installed_plugins .items ():
113- if package_name in packages_to_add :
114- plugins_to_add .append ((info ["plugin_name" ], package_name ))
115-
116- # Keep package names for removals since that's how they're stored in config
117- packages_to_remove_list = list (packages_to_remove )
166+ # Convert to list for processing
167+ plugins_to_add_list = list (plugins_to_add )
168+ plugins_to_remove_list = list (plugins_to_remove )
118169
119170 # Show summary of changes
120- if plugins_to_add :
121- click .secho (f"\n Plugins to add ({ len (plugins_to_add )} ):" , fg = "green" )
122- for plugin_name , package_name in sorted (plugins_to_add ):
123- package_info = installed_plugins [package_name ]
171+ if plugins_to_add_list :
172+ click .secho (f"\n Plugins to add ({ len (plugins_to_add_list )} ):" , fg = "green" )
173+ for plugin_name in sorted (plugins_to_add_list ):
174+ plugin_info = installed_plugins [plugin_name ]
124175 click .secho (
125- f" + { package_name } (plugin : { plugin_name } , v{ package_info ['version' ]} )" ,
176+ f" + { plugin_name } (package : { plugin_info [ 'package_name' ] } , v{ plugin_info ['version' ]} )" ,
126177 fg = "green" ,
127178 )
128179
129- if packages_to_remove_list :
130- click .secho (f"\n Plugins to remove ({ len (packages_to_remove_list )} ):" , fg = "red" )
131- for package_name in sorted (packages_to_remove_list ):
132- click .secho (f" - { package_name } (package no longer installed)" , fg = "red" )
180+ if plugins_to_remove_list :
181+ click .secho (f"\n Plugins to remove ({ len (plugins_to_remove_list )} ):" , fg = "red" )
182+ for plugin_name in sorted (plugins_to_remove_list ):
183+ click .secho (f" - { plugin_name } (plugin no longer installed)" , fg = "red" )
133184
134- if not plugins_to_add and not packages_to_remove_list :
185+ if not plugins_to_add_list and not plugins_to_remove_list :
135186 click .secho ("\n ✓ agentup.yml is already in sync with installed plugins" , fg = "green" )
136187 return
137188
138189 if dry_run :
139190 click .secho (
140- f"\n Would add { len (plugins_to_add )} and remove { len (packages_to_remove_list )} plugins" ,
191+ f"\n Would add { len (plugins_to_add_list )} and remove { len (plugins_to_remove_list )} plugins" ,
141192 fg = "cyan" ,
142193 )
143194 click .secho ("Run without --dry-run to apply changes" , fg = "cyan" )
@@ -147,11 +198,11 @@ def sync(ctx, dry_run: bool):
147198 changes_made = False
148199
149200 # Add new plugins with discovered capabilities
150- for _plugin_name , package_name in plugins_to_add :
201+ for plugin_name in plugins_to_add_list :
151202 try :
152- plugin_info = installed_plugins [package_name ]
203+ plugin_info = installed_plugins [plugin_name ]
153204
154- # Create plugin override with package name and capabilities
205+ # Create plugin override with capabilities
155206 from agent .config .intent import CapabilityOverride , PluginOverride
156207
157208 capability_overrides = {}
@@ -161,45 +212,45 @@ def sync(ctx, dry_run: bool):
161212 enabled = cap ["enabled" ], required_scopes = cap ["required_scopes" ]
162213 )
163214
164- # Create plugin override (package name is the key)
215+ # Create plugin override (plugin name is the key)
165216 plugin_override = PluginOverride (
166217 enabled = True ,
167218 capabilities = capability_overrides ,
168219 )
169- # Use package name as key for intuitive user experience
170- intent_config .add_plugin (package_name , plugin_override )
220+ # Use plugin (entry point) name as key to match plugin loading logic
221+ intent_config .add_plugin (plugin_name , plugin_override )
171222
172223 if plugin_info ["capabilities" ]:
173224 click .secho (
174- f" ✓ Added { package_name } with { len (plugin_info ['capabilities' ])} capabilities" ,
225+ f" ✓ Added { plugin_name } (from { plugin_info [ ' package_name' ] } ) with { len (plugin_info ['capabilities' ])} capabilities" ,
175226 fg = "green" ,
176227 )
177228 else :
178229 click .secho (
179- f" ✓ Added { package_name } with no capabilities" ,
230+ f" ✓ Added { plugin_name } (from { plugin_info [ ' package_name' ] } ) with no capabilities" ,
180231 fg = "green" ,
181232 )
182233
183234 changes_made = True
184235 except (KeyError , AttributeError , ValueError , TypeError ) as e :
185- click .secho (f" ✗ Failed to add { package_name } : { e } " , fg = "red" )
236+ click .secho (f" ✗ Failed to add { plugin_name } : { e } " , fg = "red" )
186237
187238 # Remove plugins no longer installed
188- for package_name in packages_to_remove_list :
239+ for plugin_name in plugins_to_remove_list :
189240 try :
190- if package_name in intent_config .plugins :
191- del intent_config .plugins [package_name ]
192- click .secho (f" ✓ Removed { package_name } " , fg = "green" )
241+ if plugin_name in intent_config .plugins :
242+ del intent_config .plugins [plugin_name ]
243+ click .secho (f" ✓ Removed { plugin_name } " , fg = "green" )
193244 changes_made = True
194245 except (KeyError , AttributeError ) as e :
195- click .secho (f" ✗ Failed to remove { package_name } : { e } " , fg = "red" )
246+ click .secho (f" ✗ Failed to remove { plugin_name } : { e } " , fg = "red" )
196247
197248 # Save updated configuration
198249 if changes_made :
199250 try :
200251 save_intent_config (intent_config , str (intent_config_path ))
201252 click .secho (
202- f"\n ✓ Updated agentup.yml with { len (plugins_to_add )} additions and { len (packages_to_remove_list )} removals" ,
253+ f"\n ✓ Updated agentup.yml with { len (plugins_to_add_list )} additions and { len (plugins_to_remove_list )} removals" ,
203254 fg = "green" ,
204255 )
205256 except (FileNotFoundError , yaml .YAMLError , PermissionError , OSError ) as e :
@@ -274,6 +325,22 @@ def add(ctx, plugin_name: str):
274325 if not plugin_found :
275326 click .secho (f"Plugin '{ plugin_name } ' is not installed or is not an AgentUp plugin" , fg = "red" )
276327 click .secho (f"Install it first with: uv add { plugin_name } " , fg = "cyan" )
328+
329+ # Suggest similar plugin names from available installed plugins
330+ try :
331+ from agent .plugins .manager import PluginRegistry
332+
333+ registry = PluginRegistry ()
334+ available_plugins_info = registry .discover_all_available_plugins ()
335+ available_plugins = [p ["name" ] for p in available_plugins_info ]
336+ suggestions = _find_similar_plugin_names (plugin_name , available_plugins )
337+ if suggestions :
338+ if len (suggestions ) == 1 :
339+ click .secho (f"Did you mean: { suggestions [0 ]} ?" , fg = "cyan" )
340+ else :
341+ click .secho (f"Did you mean one of: { ', ' .join (suggestions )} ?" , fg = "cyan" )
342+ except Exception :
343+ pass # nosec
277344 return
278345
279346 click .secho (
@@ -285,16 +352,62 @@ def add(ctx, plugin_name: str):
285352 click .secho (f"Failed to verify plugin installation: { e } " , fg = "red" )
286353 ctx .exit (1 )
287354
288- # Add plugin to configuration (package name is used as the key)
355+ # Add plugin to configuration with capability discovery
289356 try :
290- from agent .config .intent import PluginOverride
357+ from agent .config .intent import CapabilityOverride , PluginOverride
358+
359+ # Discover capabilities from the plugin (same logic as sync)
360+ capability_overrides = {}
361+
362+ try :
363+ # Load the plugin and discover capabilities
364+ import importlib .metadata as metadata
365+
366+ entry_points = metadata .entry_points ()
367+ if hasattr (entry_points , "select" ):
368+ plugin_entries = entry_points .select (group = "agentup.plugins" )
369+ else :
370+ plugin_entries = entry_points .get ("agentup.plugins" , [])
371+
372+ for entry_point in plugin_entries :
373+ if entry_point .name == actual_plugin_name :
374+ plugin_class = entry_point .load ()
375+ plugin_instance = plugin_class ()
376+
377+ # Get capability definitions if available
378+ if hasattr (plugin_instance , "get_capability_definitions" ):
379+ cap_definitions = plugin_instance .get_capability_definitions ()
380+
381+ for cap_def in cap_definitions :
382+ capability_overrides [cap_def .id ] = CapabilityOverride (
383+ enabled = True , required_scopes = cap_def .required_scopes
384+ )
385+ break
386+ except (ImportError , AttributeError , TypeError , ValueError ) as e :
387+ click .secho (
388+ f" Warning: Could not discover capabilities for { actual_plugin_name } : { e } " ,
389+ fg = "yellow" ,
390+ )
291391
292- plugin_override = PluginOverride (enabled = True )
392+ plugin_override = PluginOverride (
393+ enabled = True ,
394+ capabilities = capability_overrides ,
395+ )
293396
294- # Use package name as key for consistent user experience
295- intent_config .add_plugin (package_name , plugin_override )
397+ # Use plugin (entry point) name as key to match plugin loading logic
398+ intent_config .add_plugin (actual_plugin_name , plugin_override )
296399 save_intent_config (intent_config , str (intent_config_path ))
297- click .secho (f"✓ Added { package_name } to agentup.yml" , fg = "green" )
400+
401+ if capability_overrides :
402+ click .secho (
403+ f"✓ Added { actual_plugin_name } (from package { package_name } ) with { len (capability_overrides )} capabilities to agentup.yml" ,
404+ fg = "green" ,
405+ )
406+ else :
407+ click .secho (
408+ f"✓ Added { actual_plugin_name } (from package { package_name } ) with no capabilities to agentup.yml" ,
409+ fg = "green" ,
410+ )
298411
299412 except (
300413 KeyError ,
@@ -330,6 +443,17 @@ def remove(ctx, plugin_name: str):
330443 # Check if plugin is configured
331444 if not intent_config .plugins or plugin_name not in intent_config .plugins :
332445 click .secho (f"Plugin '{ plugin_name } ' is not configured in agentup.yml" , fg = "yellow" )
446+
447+ # Suggest similar plugin names from configured plugins
448+ if intent_config .plugins :
449+ configured_plugins = list (intent_config .plugins .keys ())
450+ suggestions = _find_similar_plugin_names (plugin_name , configured_plugins )
451+ if suggestions :
452+ if len (suggestions ) == 1 :
453+ click .secho (f"Did you mean: { suggestions [0 ]} ?" , fg = "cyan" )
454+ else :
455+ click .secho (f"Did you mean one of: { ', ' .join (suggestions )} ?" , fg = "cyan" )
456+
333457 return
334458
335459 # Remove the plugin
@@ -365,15 +489,9 @@ def reload(plugin_name: str):
365489 click .secho (f"Plugin '{ plugin_name } ' not found" , fg = "yellow" )
366490 return
367491
368- click .secho (f"Reloading plugin '{ plugin_name } '..." , fg = "cyan" )
369-
370- if manager .reload_plugin (plugin_name ):
371- click .secho (f"✓ Successfully reloaded { plugin_name } " , fg = "green" )
372- else :
373- click .secho (f"✗ Failed to reload { plugin_name } " , fg = "red" )
374- click .secho ("[dim]Note: Entry point plugins cannot be reloaded[/dim]" )
492+ click .secho ("Restart the agent to reload plugin changes." , fg = "cyan" )
375493
376494 except ImportError :
377495 click .secho ("Plugin system not available." , fg = "red" )
378496 except (AttributeError , KeyError , RuntimeError ) as e :
379- click .secho (f"Error reloading plugin: { e } " , fg = "red" )
497+ click .secho (f"Error accessing plugin: { e } " , fg = "red" )
0 commit comments