1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4use serde_json::{Map, Value};
5
6use crate::backend_kind::BackendKind;
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
9pub struct DaemonConfig {
10 #[serde(default)]
11 pub quiet_restart: bool,
12 #[serde(default = "default_working_dirs")]
13 pub working_dirs: Vec<String>,
14 #[serde(default)]
18 pub backend: BackendKind,
19}
20
21fn default_working_dirs() -> Vec<String> {
22 vec!["~".to_string()]
23}
24
25impl Default for DaemonConfig {
26 fn default() -> Self {
27 Self {
28 quiet_restart: false,
29 working_dirs: default_working_dirs(),
30 backend: BackendKind::Zellij,
31 }
32 }
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
36pub struct WebConfig {
37 #[serde(default = "default_web_host")]
38 pub host: String,
39 #[serde(default = "default_proxy_base_port")]
40 pub proxy_base_port: u16,
41 #[serde(default = "default_zellij_web")]
46 pub zellij_web: bool,
47 #[serde(default = "default_herdr_terminal")]
51 pub herdr_terminal: bool,
52 #[serde(default = "default_herdr_max_observers_per_session")]
54 pub herdr_terminal_max_observers_per_session: usize,
55 #[serde(default = "default_herdr_max_observers_global")]
57 pub herdr_terminal_max_observers_global: usize,
58}
59
60fn default_web_host() -> String {
61 "0.0.0.0".to_string()
62}
63
64fn default_proxy_base_port() -> u16 {
65 8800
66}
67
68fn default_zellij_web() -> bool {
69 true
70}
71
72fn default_herdr_terminal() -> bool {
73 true
74}
75
76fn default_herdr_max_observers_per_session() -> usize {
77 8
78}
79
80fn default_herdr_max_observers_global() -> usize {
81 64
82}
83
84impl Default for WebConfig {
85 fn default() -> Self {
86 Self {
87 host: default_web_host(),
88 proxy_base_port: default_proxy_base_port(),
89 zellij_web: default_zellij_web(),
90 herdr_terminal: default_herdr_terminal(),
91 herdr_terminal_max_observers_per_session: default_herdr_max_observers_per_session(),
92 herdr_terminal_max_observers_global: default_herdr_max_observers_global(),
93 }
94 }
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
100pub struct HerdrConfig {
101 #[serde(default = "default_herdr_min_version")]
102 pub min_version: String,
103 #[serde(default = "default_herdr_session")]
106 pub session: String,
107 #[serde(default)]
108 pub socket_path: Option<String>,
109}
110
111fn default_herdr_min_version() -> String {
112 "0.8.2".to_string()
113}
114
115fn default_herdr_session() -> String {
116 "default".to_string()
117}
118
119impl Default for HerdrConfig {
120 fn default() -> Self {
121 Self {
122 min_version: default_herdr_min_version(),
123 session: default_herdr_session(),
124 socket_path: None,
125 }
126 }
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
130pub struct Config {
131 #[serde(default)]
132 pub daemon: DaemonConfig,
133 #[serde(default)]
134 pub web: WebConfig,
135 #[serde(default)]
136 pub herdr: HerdrConfig,
137 #[serde(default)]
138 pub lark: LarkConfig,
139 #[serde(default, rename = "screenAnalyzer")]
140 pub screen_analyzer: ScreenAnalyzerConfig,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
144pub struct BotConfig {
145 #[serde(default)]
146 pub name: Option<String>,
147 #[serde(rename = "larkAppId")]
148 pub lark_app_id: String,
149 #[serde(rename = "larkAppSecret")]
150 pub lark_app_secret: String,
151 #[serde(rename = "cliId")]
152 pub cli_id: String,
153 #[serde(rename = "cliBin", default)]
154 pub cli_bin: Option<String>,
155 #[serde(rename = "cliArgs", default)]
156 pub cli_args: Vec<String>,
157 #[serde(default)]
160 pub backend: Option<BackendKind>,
161 #[serde(
163 rename = "cgroupSlice",
164 default,
165 skip_serializing_if = "Option::is_none"
166 )]
167 pub cgroup_slice: Option<String>,
168 #[serde(default)]
169 pub model: Option<String>,
170 #[serde(rename = "workingDir", default)]
171 pub working_dir: Option<String>,
172 #[serde(rename = "skipWorkingDirPrompt", default)]
173 pub skip_working_dir_prompt: bool,
174 #[serde(rename = "larkEncryptKey", default)]
175 pub lark_encrypt_key: Option<String>,
176 #[serde(rename = "larkVerificationToken", default)]
177 pub lark_verification_token: Option<String>,
178 #[serde(rename = "allowedUsers", default)]
179 pub allowed_users: Vec<String>,
180 #[serde(rename = "privateCard", default)]
181 pub private_card: bool,
182 #[serde(rename = "allowedChatGroups", default)]
183 pub allowed_chat_groups: Vec<String>,
184 #[serde(rename = "chatGrants", default)]
185 pub chat_grants: std::collections::HashMap<String, Vec<String>>,
186 #[serde(rename = "globalGrants", default)]
187 pub global_grants: Vec<String>,
188 #[serde(rename = "oncallChats", default)]
189 pub oncall_chats: Vec<OncallChatBinding>,
190 #[serde(rename = "restrictGrantCommands", default)]
191 pub restrict_grant_commands: bool,
192 #[serde(rename = "messageQuota", default)]
193 pub message_quota: Option<MessageQuotaConfig>,
194 #[serde(rename = "quotaState", default)]
195 pub quota_state: std::collections::HashMap<String, QuotaEntry>,
196 #[serde(rename = "customTriggers", default)]
200 pub custom_triggers: Vec<CustomTrigger>,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
204pub struct OncallChatBinding {
205 #[serde(rename = "chatId")]
206 pub chat_id: String,
207 #[serde(rename = "workingDir", default)]
208 pub working_dir: Option<String>,
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
212pub struct CustomTrigger {
213 #[serde(rename = "trigger")]
218 pub trigger: String,
219 #[serde(rename = "prompt", default)]
222 pub prompt: Option<String>,
223 #[serde(rename = "skipDirSelect", default)]
226 pub skip_dir_select: bool,
227 #[serde(rename = "workingDir", default)]
230 pub working_dir: Option<String>,
231 #[serde(rename = "ackMessage", default)]
235 pub ack_message: Option<String>,
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
239pub struct MessageQuotaConfig {
240 #[serde(rename = "defaultLimit", default)]
241 pub default_limit: Option<u32>,
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
245pub struct QuotaEntry {
246 pub limit: u32,
247 pub used: u32,
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
251pub struct LarkConfig {
252 #[serde(default)]
253 pub verification_token: Option<String>,
254 #[serde(default)]
255 pub encrypt_key: Option<String>,
256}
257
258#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
259pub struct ScreenAnalyzerConfig {
260 #[serde(default)]
261 pub enabled: bool,
262 #[serde(default)]
263 pub base_url: String,
264 #[serde(default)]
265 pub api_key: String,
266 #[serde(default)]
267 pub model: String,
268 #[serde(default = "default_screen_analyzer_interval_ms")]
269 pub interval_ms: u64,
270 #[serde(default = "default_screen_analyzer_stable_count")]
271 pub stable_count: u32,
272 #[serde(default = "default_screen_analyzer_snapshot_max_chars")]
273 pub snapshot_max_chars: usize,
274 #[serde(default)]
275 pub extra_headers: HashMap<String, String>,
276 #[serde(default)]
277 pub extra_body: Map<String, Value>,
278}
279
280fn default_screen_analyzer_interval_ms() -> u64 {
281 2_000
282}
283
284fn default_screen_analyzer_stable_count() -> u32 {
285 6
286}
287
288fn default_screen_analyzer_snapshot_max_chars() -> usize {
289 8_000
290}
291
292impl Default for ScreenAnalyzerConfig {
293 fn default() -> Self {
294 Self {
295 enabled: false,
296 base_url: String::new(),
297 api_key: String::new(),
298 model: String::new(),
299 interval_ms: default_screen_analyzer_interval_ms(),
300 stable_count: default_screen_analyzer_stable_count(),
301 snapshot_max_chars: default_screen_analyzer_snapshot_max_chars(),
302 extra_headers: HashMap::new(),
303 extra_body: Map::new(),
304 }
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::BotConfig;
311
312 #[test]
313 fn bot_config_defaults_missing_cli_args_and_skip_prompt_fields() {
314 let raw = r#"{
315 "larkAppId":"app-1",
316 "larkAppSecret":"secret",
317 "cliId":"codex"
318 }"#;
319 let bot: BotConfig = serde_json::from_str(raw).expect("deserialize bot");
320 assert!(bot.cli_args.is_empty());
321 assert!(bot.cgroup_slice.is_none());
322 assert!(!bot.skip_working_dir_prompt);
323 }
324
325 #[test]
326 fn bot_config_deserializes_cgroup_slice() {
327 let raw = r#"{
328 "larkAppId":"app-1",
329 "larkAppSecret":"secret",
330 "cliId":"grok",
331 "cgroupSlice":"cgtproxy-gateway.slice"
332 }"#;
333 let bot: BotConfig = serde_json::from_str(raw).expect("deserialize bot");
334 assert_eq!(bot.cgroup_slice.as_deref(), Some("cgtproxy-gateway.slice"));
335 }
336
337 #[test]
338 fn bot_config_ignores_legacy_cli_prefix_field() {
339 let raw = r#"{
340 "larkAppId":"app-1",
341 "larkAppSecret":"secret",
342 "cliId":"grok",
343 "cliPrefix":["systemd-run","--user"]
344 }"#;
345 let bot: BotConfig = serde_json::from_str(raw).expect("deserialize bot");
346 assert!(bot.cgroup_slice.is_none());
347 }
348
349 #[test]
350 fn bot_config_deserializes_traex_cli_args() {
351 let raw = r#"{
352 "larkAppId":"app-1",
353 "larkAppSecret":"secret",
354 "cliId":"traex",
355 "cliArgs":["-y"]
356 }"#;
357 let bot: BotConfig = serde_json::from_str(raw).expect("deserialize bot");
358 assert_eq!(bot.cli_args, vec!["-y".to_string()]);
359 }
360}