2020from ..config import get_settings
2121
2222
23+ POINTS_SUPER_ADMIN_SLACK_ID = "U05QPB483K9"
24+
25+
2326@dataclass
2427class SkillResult :
2528 """Result from skill execution."""
@@ -49,6 +52,7 @@ async def execute(
4952 channel_id : Optional [str ] = None ,
5053 thread_ts : Optional [str ] = None ,
5154 thread_history : Optional [List [dict ]] = None ,
55+ param_overrides : Optional [dict ] = None ,
5256 ** kwargs
5357 ) -> SkillResult :
5458 """
@@ -70,6 +74,9 @@ async def execute(
7074 try :
7175 # Extract parameters using LLM
7276 params = await self ._extract_parameters (skill , text , user_id , thread_history )
77+ if param_overrides :
78+ params = {** params , ** param_overrides }
79+ print (f" Applied param overrides: { param_overrides } " )
7380 print (f" Extracted params: { params } " )
7481
7582 # Check for skill-specific implementation
@@ -1092,15 +1099,23 @@ async def _execute_content_factory(
10921099 # 1. New User Disclaimer & Education
10931100 # If user has no integration AND hasn't confirmed the disclaimer yet
10941101 if not integration and not params .get ("confirmed" ):
1095- # Save pending intent so we don't lose the original params (domain/topic)
1096- intent_data = json .dumps ({
1097- "skill" : "content-factory" ,
1098- "params" : params ,
1099- "text" : text ,
1100- "channel" : channel_id ,
1101- "ts" : thread_ts
1102- })
1103- await api_client .save_pending_intent (user_id , intent_data )
1102+ await self ._save_content_factory_pending_intent (
1103+ api_client ,
1104+ user_id ,
1105+ params ,
1106+ text ,
1107+ channel_id ,
1108+ thread_ts ,
1109+ )
1110+
1111+ confirm_value = json .dumps (
1112+ {
1113+ "text" : text ,
1114+ "params" : params ,
1115+ "channel_id" : channel_id ,
1116+ "thread_ts" : thread_ts ,
1117+ }
1118+ )
11041119
11051120 blocks = [
11061121 {
@@ -1137,6 +1152,7 @@ async def _execute_content_factory(
11371152 "emoji" : True
11381153 },
11391154 "action_id" : "confirm_content_factory" ,
1155+ "value" : confirm_value ,
11401156 "style" : "primary"
11411157 }
11421158 ]
@@ -2254,6 +2270,15 @@ def _resolve_points_action(self, params: dict, text: str) -> str:
22542270 text_lower = text .lower ()
22552271 explicit_points_request = "request" in text_lower and "point" in text_lower and "reward" not in text_lower
22562272
2273+ if action in ["promote_points_admin" , "set_points_admin_allowance" ]:
2274+ return action
2275+
2276+ if self ._is_points_admin_promotion_command (text ):
2277+ return "promote_points_admin"
2278+
2279+ if self ._is_points_allowance_change_command (text ):
2280+ return "set_points_admin_allowance"
2281+
22572282 if explicit_points_request :
22582283 return "request_points"
22592284
@@ -2302,6 +2327,86 @@ def _resolve_points_action(self, params: dict, text: str) -> str:
23022327 return "deduct_points"
23032328 return action
23042329
2330+ def _is_points_super_admin (self , user_id : str ) -> bool :
2331+ """Return true when the requester can manage points admins and allowances."""
2332+ return user_id == POINTS_SUPER_ADMIN_SLACK_ID
2333+
2334+ def _points_super_admin_denial (self ) -> str :
2335+ """Fixed denial response for restricted points super-admin actions."""
2336+ return (
2337+ f"Sorry mate, only <@{ POINTS_SUPER_ADMIN_SLACK_ID } > can manage "
2338+ "Points Admin access and weekly allowances. 🔒"
2339+ )
2340+
2341+ def _is_points_admin_promotion_command (self , text : str ) -> bool :
2342+ """Detect commands that promote a tagged user to Points Admin."""
2343+ return bool (
2344+ re .search (r"\b(?:promote|make)\b" , text , re .IGNORECASE )
2345+ and re .search (r"<@[A-Z0-9]+>" , text )
2346+ and re .search (r"\b(?:roo\s+)?points\s+admin\b" , text , re .IGNORECASE )
2347+ )
2348+
2349+ def _is_points_allowance_change_command (self , text : str ) -> bool :
2350+ """Detect commands that change a tagged admin's weekly allowance."""
2351+ return bool (
2352+ re .search (r"\b(?:set|change)\b" , text , re .IGNORECASE )
2353+ and re .search (r"<@[A-Z0-9]+>" , text )
2354+ and re .search (r"\bweekly\s+(?:points\s+)?allowance\b" , text , re .IGNORECASE )
2355+ )
2356+
2357+ def _extract_non_roo_mentions (self , text : str , bot_id : Optional [str ] = None ) -> list [str ]:
2358+ """Extract unique tagged Slack user IDs, excluding Roo when present."""
2359+ target_ids : list [str ] = []
2360+ seen : set [str ] = set ()
2361+
2362+ for target_id in re .findall (r"<@([A-Z0-9]+)>" , text ):
2363+ if target_id == bot_id or target_id in seen :
2364+ continue
2365+ seen .add (target_id )
2366+ target_ids .append (target_id )
2367+
2368+ return target_ids
2369+
2370+ def _extract_single_admin_target (
2371+ self ,
2372+ text : str ,
2373+ * ,
2374+ bot_id : Optional [str ],
2375+ action_label : str ,
2376+ ) -> tuple [Optional [str ], Optional [str ]]:
2377+ """Extract exactly one tagged target user for privileged admin actions."""
2378+ target_ids = self ._extract_non_roo_mentions (text , bot_id = bot_id )
2379+
2380+ if not target_ids :
2381+ return None , f"Tag exactly one user to { action_label } ."
2382+ if len (target_ids ) > 1 :
2383+ return None , f"Tag exactly one user to { action_label } . I found multiple mentions there."
2384+
2385+ return target_ids [0 ], None
2386+
2387+ def _extract_weekly_allowance (self , text : str , fallback_allowance : Any = None ) -> Optional [int ]:
2388+ """Extract the requested weekly allowance from params or text."""
2389+ if fallback_allowance not in (None , "" , "0" ):
2390+ try :
2391+ return int (fallback_allowance )
2392+ except (TypeError , ValueError ):
2393+ pass
2394+
2395+ patterns = (
2396+ r"weekly\s+(?:points\s+)?allowance\s+(?:to|of)\s+(-?\d+)\b" ,
2397+ r"allowance\s+(?:to|of)\s+(-?\d+)\b" ,
2398+ r"(?<![A-Z0-9])(-?\d+)\b" ,
2399+ )
2400+ for pattern in patterns :
2401+ match = re .search (pattern , text , re .IGNORECASE )
2402+ if match :
2403+ try :
2404+ return int (match .group (1 ))
2405+ except ValueError :
2406+ return None
2407+
2408+ return None
2409+
23052410 def _extract_points_request_reason (self , text : str , fallback_reason : str = "" ) -> str :
23062411 """Extract the reason from a natural-language points request."""
23072412 if fallback_reason :
@@ -2689,6 +2794,93 @@ async def _handle_points_action(
26892794 # =====================================================================
26902795 # Admin Actions
26912796 # =====================================================================
2797+
2798+ elif action == "promote_points_admin" :
2799+ from ..slack_client import get_bot_user_id
2800+
2801+ if not self ._is_points_super_admin (user_id ):
2802+ return self ._points_super_admin_denial ()
2803+
2804+ try :
2805+ bot_id = get_bot_user_id ()
2806+ except Exception :
2807+ bot_id = None
2808+
2809+ target_slack_id , target_error = self ._extract_single_admin_target (
2810+ text ,
2811+ bot_id = bot_id ,
2812+ action_label = "promote them to Roo Points Admin" ,
2813+ )
2814+ if target_error :
2815+ return target_error
2816+
2817+ await self ._ensure_user_exists (target_slack_id )
2818+ result = await client .promote_points_admin (user_id , target_slack_id )
2819+
2820+ error_message = str (result .get ("error" ) or result .get ("detail" ) or "" ).strip ()
2821+ promoted_target = client ._clean_slack_id (
2822+ result .get ("target_slack_id" ) or target_slack_id
2823+ )
2824+
2825+ if result .get ("already_admin" ) or (
2826+ "already" in error_message .lower () and "admin" in error_message .lower ()
2827+ ):
2828+ return f"<@{ promoted_target } > is already a Roo Points Admin."
2829+ if error_message :
2830+ return f"Couldn't promote <@{ promoted_target } > to Roo Points Admin: { error_message } "
2831+
2832+ return f"✅ <@{ promoted_target } > is now a Roo Points Admin."
2833+
2834+ elif action == "set_points_admin_allowance" :
2835+ from ..slack_client import get_bot_user_id
2836+
2837+ if not self ._is_points_super_admin (user_id ):
2838+ return self ._points_super_admin_denial ()
2839+
2840+ try :
2841+ bot_id = get_bot_user_id ()
2842+ except Exception :
2843+ bot_id = None
2844+
2845+ target_slack_id , target_error = self ._extract_single_admin_target (
2846+ text ,
2847+ bot_id = bot_id ,
2848+ action_label = "change their weekly points allowance" ,
2849+ )
2850+ if target_error :
2851+ return target_error
2852+
2853+ weekly_allowance = self ._extract_weekly_allowance (
2854+ text ,
2855+ params .get ("weekly_allowance" ),
2856+ )
2857+ if weekly_allowance is None :
2858+ return (
2859+ "What weekly allowance should I set? Give me a positive number of points, "
2860+ "like `set <@user> weekly points allowance to 150`."
2861+ )
2862+ if weekly_allowance <= 0 :
2863+ return "Weekly points allowance has to be a positive number."
2864+
2865+ result = await client .set_points_admin_weekly_allowance (
2866+ user_id ,
2867+ target_slack_id ,
2868+ weekly_allowance ,
2869+ )
2870+
2871+ error_message = str (result .get ("error" ) or result .get ("detail" ) or "" ).strip ()
2872+ if error_message :
2873+ if "not a points admin" in error_message .lower ():
2874+ return f"<@{ target_slack_id } > isn't a Points Admin yet, so I can't update their allowance."
2875+ return f"Couldn't update <@{ target_slack_id } >'s weekly allowance: { error_message } "
2876+
2877+ effective_allowance = result .get ("weekly_allowance" , result .get ("allowance" , weekly_allowance ))
2878+ updated_target = client ._clean_slack_id (result .get ("target_slack_id" ) or target_slack_id )
2879+
2880+ return (
2881+ f"✅ Set <@{ updated_target } >'s weekly points allowance to "
2882+ f"{ effective_allowance } points."
2883+ )
26922884
26932885 elif action == "create_task" :
26942886 # 1. Parameter Aliases
0 commit comments