66from pathlib import Path
77from typing import Optional
88
9+
10+ @dataclass
11+ class LocalCommand :
12+ """Local command configuration."""
13+ type : str # "static" or "handler"
14+ response : str = "" # for static type
15+ handler : str = "" # for handler type
16+
917logger = logging .getLogger (__name__ )
1018DEFAULT_CONFIG_PATH = Path (__file__ ).with_name ("config.toml" )
1119
@@ -28,7 +36,46 @@ def extract_command(text: Optional[str]) -> Optional[str]:
2836 return command
2937
3038
31- def _load_config (config_path : Path = DEFAULT_CONFIG_PATH ) -> tuple [list [str ], dict [str , str ], list [int | str ]]:
39+ def _parse_local_command (name : str , value ) -> tuple [str , LocalCommand | None ]:
40+ """Parse a single local command entry.
41+
42+ Args:
43+ name: Command name (with or without leading slash)
44+ value: Command value (string for legacy, dict for new format)
45+
46+ Returns:
47+ Tuple of (normalized_cmd, LocalCommand) or (normalized_cmd, None) if invalid
48+ """
49+ cmd = f"/{ name .lstrip ('/' )} " if not name .startswith ('/' ) else name
50+
51+ # Legacy format: string value = static response
52+ if isinstance (value , str ):
53+ return cmd , LocalCommand (type = "static" , response = value )
54+
55+ # New format: dict with type field
56+ if isinstance (value , dict ):
57+ cmd_type = value .get ('type' , '' )
58+ if cmd_type == 'static' :
59+ response = value .get ('response' , '' )
60+ if not response :
61+ logger .warning (f"Local command { cmd } has no response; skipping" )
62+ return cmd , None
63+ return cmd , LocalCommand (type = "static" , response = response )
64+ elif cmd_type == 'handler' :
65+ handler = value .get ('handler' , '' )
66+ if not handler :
67+ logger .warning (f"Local command { cmd } has no handler; skipping" )
68+ return cmd , None
69+ return cmd , LocalCommand (type = "handler" , handler = handler )
70+ else :
71+ logger .warning (f"Local command { cmd } has unknown type: { cmd_type } ; skipping" )
72+ return cmd , None
73+
74+ logger .warning (f"Local command { cmd } has invalid value type; skipping" )
75+ return cmd , None
76+
77+
78+ def _load_config (config_path : Path = DEFAULT_CONFIG_PATH ) -> tuple [list [str ], dict [str , LocalCommand ], list [int | str ]]:
3279 """Load commands and security config from TOML config file.
3380
3481 Returns:
@@ -48,16 +95,18 @@ def _load_config(config_path: Path = DEFAULT_CONFIG_PATH) -> tuple[list[str], di
4895 agent_commands = []
4996 agent_commands = [cmd for cmd in agent_commands if isinstance (cmd , str )]
5097
51- # Load local commands
98+ # Load local commands (supports both legacy string and new dict format)
5299 local_commands_raw = data .get ('local_commands' , {})
53100 if not isinstance (local_commands_raw , dict ):
54101 logger .warning ("Local commands config is not a table; ignoring configuration" )
55102 local_commands_raw = {}
56- local_commands = {
57- f"/{ name .lstrip ('/' )} " if not name .startswith ('/' ) else name : str (value )
58- for name , value in local_commands_raw .items ()
59- if isinstance (name , str ) and isinstance (value , str )
60- }
103+ local_commands : dict [str , LocalCommand ] = {}
104+ for name , value in local_commands_raw .items ():
105+ if not isinstance (name , str ):
106+ continue
107+ cmd , parsed = _parse_local_command (name , value )
108+ if parsed :
109+ local_commands [cmd ] = parsed
61110
62111 # Load security whitelist
63112 security = data .get ('security' , {})
@@ -92,8 +141,9 @@ class Config:
92141 auth_token : str
93142 queue_url : str
94143 agent_commands : list [str ]
95- local_commands : dict [str , str ]
144+ local_commands : dict [str , LocalCommand ]
96145 user_whitelist : list [int | str ]
146+ telegram_webhook_secret : str = ""
97147
98148 @classmethod
99149 def from_env (cls , config_path : Optional [Path ] = None ) -> 'Config' :
@@ -107,6 +157,7 @@ def from_env(cls, config_path: Optional[Path] = None) -> 'Config':
107157 agent_commands = agent_cmds ,
108158 local_commands = local_cmds ,
109159 user_whitelist = whitelist ,
160+ telegram_webhook_secret = os .getenv ('TELEGRAM_WEBHOOK_SECRET' , '' ),
110161 )
111162
112163 def get_command (self , text : Optional [str ]) -> Optional [str ]:
@@ -118,8 +169,16 @@ def is_agent_command(self, cmd: Optional[str]) -> bool:
118169 def is_local_command (self , cmd : Optional [str ]) -> bool :
119170 return bool (cmd ) and cmd in self .local_commands
120171
172+ def get_local_command (self , cmd : str ) -> LocalCommand | None :
173+ """Get local command config by command name."""
174+ return self .local_commands .get (cmd )
175+
121176 def local_response (self , cmd : str ) -> str :
122- return self .local_commands .get (cmd , "Unsupported command." )
177+ """Get static response for a local command (legacy compatibility)."""
178+ local_cmd = self .local_commands .get (cmd )
179+ if local_cmd and local_cmd .type == "static" :
180+ return local_cmd .response
181+ return "Unsupported command."
123182
124183 def unknown_command_message (self ) -> str :
125184 parts = []
0 commit comments