Enhanced code review and frontend validation with intelligent triggers: Code Review Agent Enhancement: - Added Sequential Thinking MCP integration for complex issues - Triggers on 2+ rejections or 3+ critical issues - New escalation format with root cause analysis - Comprehensive solution strategies with trade-off evaluation - Educational feedback to break rejection cycles - Files: .claude/agents/code-review.md (+308 lines) - Docs: CODE_REVIEW_ST_ENHANCEMENT.md, CODE_REVIEW_ST_TESTING.md Frontend Design Skill Enhancement: - Automatic invocation for ANY UI change - Comprehensive validation checklist (200+ checkpoints) - 8 validation categories (visual, interactive, responsive, a11y, etc.) - 3 validation levels (quick, standard, comprehensive) - Integration with code review workflow - Files: .claude/skills/frontend-design/SKILL.md (+120 lines) - Docs: UI_VALIDATION_CHECKLIST.md (462 lines), AUTOMATIC_VALIDATION_ENHANCEMENT.md (587 lines) Settings Optimization: - Repaired .claude/settings.local.json (fixed m365 pattern) - Reduced permissions from 49 to 33 (33% reduction) - Removed duplicates, sorted alphabetically - Created SETTINGS_PERMISSIONS.md documentation Checkpoint Command Enhancement: - Dual checkpoint system (git + database) - Saves session context to API for cross-machine recall - Includes git metadata in database context - Files: .claude/commands/checkpoint.md (+139 lines) Decision Rationale: - Sequential Thinking MCP breaks rejection cycles by identifying root causes - Automatic frontend validation catches UI issues before code review - Dual checkpoints enable complete project memory across machines - Settings optimization improves maintainability Total: 1,200+ lines of documentation and enhancements Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
465 lines
18 KiB
Plaintext
465 lines
18 KiB
Plaintext
1→//! Agent configuration management
|
|
2→//!
|
|
3→//! Supports three configuration sources (in priority order):
|
|
4→//! 1. Embedded config (magic bytes appended to executable)
|
|
5→//! 2. Config file (guruconnect.toml or %ProgramData%\GuruConnect\agent.toml)
|
|
6→//! 3. Environment variables (fallback)
|
|
7→
|
|
8→use anyhow::{anyhow, Context, Result};
|
|
9→use serde::{Deserialize, Serialize};
|
|
10→use std::io::{Read, Seek, SeekFrom};
|
|
11→use std::path::PathBuf;
|
|
12→use tracing::{info, warn};
|
|
13→use uuid::Uuid;
|
|
14→
|
|
15→/// Magic marker for embedded configuration (10 bytes)
|
|
16→const MAGIC_MARKER: &[u8] = b"GURUCONFIG";
|
|
17→
|
|
18→/// Embedded configuration data (appended to executable)
|
|
19→#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
20→pub struct EmbeddedConfig {
|
|
21→ /// Server WebSocket URL
|
|
22→ pub server_url: String,
|
|
23→ /// API key for authentication
|
|
24→ pub api_key: String,
|
|
25→ /// Company/organization name
|
|
26→ #[serde(default)]
|
|
27→ pub company: Option<String>,
|
|
28→ /// Site/location name
|
|
29→ #[serde(default)]
|
|
30→ pub site: Option<String>,
|
|
31→ /// Tags for categorization
|
|
32→ #[serde(default)]
|
|
33→ pub tags: Vec<String>,
|
|
34→}
|
|
35→
|
|
36→/// Detected run mode based on filename
|
|
37→#[derive(Debug, Clone, PartialEq)]
|
|
38→pub enum RunMode {
|
|
39→ /// Viewer-only installation (filename contains "Viewer")
|
|
40→ Viewer,
|
|
41→ /// Temporary support session (filename contains 6-digit code)
|
|
42→ TempSupport(String),
|
|
43→ /// Permanent agent with embedded config
|
|
44→ PermanentAgent,
|
|
45→ /// Unknown/default mode
|
|
46→ Default,
|
|
47→}
|
|
48→
|
|
49→/// Agent configuration
|
|
50→#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
51→pub struct Config {
|
|
52→ /// Server WebSocket URL (e.g., wss://connect.example.com/ws)
|
|
53→ pub server_url: String,
|
|
54→
|
|
55→ /// Agent API key for authentication
|
|
56→ pub api_key: String,
|
|
57→
|
|
58→ /// Unique agent identifier (generated on first run)
|
|
59→ #[serde(default = "generate_agent_id")]
|
|
60→ pub agent_id: String,
|
|
61→
|
|
62→ /// Optional hostname override
|
|
63→ pub hostname_override: Option<String>,
|
|
64→
|
|
65→ /// Company/organization name (from embedded config)
|
|
66→ #[serde(default)]
|
|
67→ pub company: Option<String>,
|
|
68→
|
|
69→ /// Site/location name (from embedded config)
|
|
70→ #[serde(default)]
|
|
71→ pub site: Option<String>,
|
|
72→
|
|
73→ /// Tags for categorization (from embedded config)
|
|
74→ #[serde(default)]
|
|
75→ pub tags: Vec<String>,
|
|
76→
|
|
77→ /// Support code for one-time support sessions (set via command line or filename)
|
|
78→ #[serde(skip)]
|
|
79→ pub support_code: Option<String>,
|
|
80→
|
|
81→ /// Capture settings
|
|
82→ #[serde(default)]
|
|
83→ pub capture: CaptureConfig,
|
|
84→
|
|
85→ /// Encoding settings
|
|
86→ #[serde(default)]
|
|
87→ pub encoding: EncodingConfig,
|
|
88→}
|
|
89→
|
|
90→fn generate_agent_id() -> String {
|
|
91→ Uuid::new_v4().to_string()
|
|
92→}
|
|
93→
|
|
94→#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
95→pub struct CaptureConfig {
|
|
96→ /// Target frames per second (1-60)
|
|
97→ #[serde(default = "default_fps")]
|
|
98→ pub fps: u32,
|
|
99→
|
|
100→ /// Use DXGI Desktop Duplication (recommended)
|
|
101→ #[serde(default = "default_true")]
|
|
102→ pub use_dxgi: bool,
|
|
103→
|
|
104→ /// Fall back to GDI if DXGI fails
|
|
105→ #[serde(default = "default_true")]
|
|
106→ pub gdi_fallback: bool,
|
|
107→}
|
|
108→
|
|
109→#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
110→pub struct EncodingConfig {
|
|
111→ /// Preferred codec (auto, raw, vp9, h264)
|
|
112→ #[serde(default = "default_codec")]
|
|
113→ pub codec: String,
|
|
114→
|
|
115→ /// Quality (1-100, higher = better quality, more bandwidth)
|
|
116→ #[serde(default = "default_quality")]
|
|
117→ pub quality: u32,
|
|
118→
|
|
119→ /// Use hardware encoding if available
|
|
120→ #[serde(default = "default_true")]
|
|
121→ pub hardware_encoding: bool,
|
|
122→}
|
|
123→
|
|
124→fn default_fps() -> u32 {
|
|
125→ 30
|
|
126→}
|
|
127→
|
|
128→fn default_true() -> bool {
|
|
129→ true
|
|
130→}
|
|
131→
|
|
132→fn default_codec() -> String {
|
|
133→ "auto".to_string()
|
|
134→}
|
|
135→
|
|
136→fn default_quality() -> u32 {
|
|
137→ 75
|
|
138→}
|
|
139→
|
|
140→impl Default for CaptureConfig {
|
|
141→ fn default() -> Self {
|
|
142→ Self {
|
|
143→ fps: default_fps(),
|
|
144→ use_dxgi: true,
|
|
145→ gdi_fallback: true,
|
|
146→ }
|
|
147→ }
|
|
148→}
|
|
149→
|
|
150→impl Default for EncodingConfig {
|
|
151→ fn default() -> Self {
|
|
152→ Self {
|
|
153→ codec: default_codec(),
|
|
154→ quality: default_quality(),
|
|
155→ hardware_encoding: true,
|
|
156→ }
|
|
157→ }
|
|
158→}
|
|
159→
|
|
160→impl Config {
|
|
161→ /// Detect run mode from executable filename
|
|
162→ pub fn detect_run_mode() -> RunMode {
|
|
163→ let exe_path = match std::env::current_exe() {
|
|
164→ Ok(p) => p,
|
|
165→ Err(_) => return RunMode::Default,
|
|
166→ };
|
|
167→
|
|
168→ let filename = match exe_path.file_stem() {
|
|
169→ Some(s) => s.to_string_lossy().to_string(),
|
|
170→ None => return RunMode::Default,
|
|
171→ };
|
|
172→
|
|
173→ let filename_lower = filename.to_lowercase();
|
|
174→
|
|
175→ // Check for viewer mode
|
|
176→ if filename_lower.contains("viewer") {
|
|
177→ info!("Detected viewer mode from filename: {}", filename);
|
|
178→ return RunMode::Viewer;
|
|
179→ }
|
|
180→
|
|
181→ // Check for support code in filename (6-digit number)
|
|
182→ if let Some(code) = Self::extract_support_code(&filename) {
|
|
183→ info!("Detected support code from filename: {}", code);
|
|
184→ return RunMode::TempSupport(code);
|
|
185→ }
|
|
186→
|
|
187→ // Check for embedded config
|
|
188→ if Self::has_embedded_config() {
|
|
189→ info!("Detected embedded config in executable");
|
|
190→ return RunMode::PermanentAgent;
|
|
191→ }
|
|
192→
|
|
193→ RunMode::Default
|
|
194→ }
|
|
195→
|
|
196→ /// Extract 6-digit support code from filename
|
|
197→ fn extract_support_code(filename: &str) -> Option<String> {
|
|
198→ // Look for patterns like "GuruConnect-123456" or "GuruConnect_123456"
|
|
199→ for part in filename.split(|c| c == '-' || c == '_' || c == '.') {
|
|
200→ let trimmed = part.trim();
|
|
201→ if trimmed.len() == 6 && trimmed.chars().all(|c| c.is_ascii_digit()) {
|
|
202→ return Some(trimmed.to_string());
|
|
203→ }
|
|
204→ }
|
|
205→
|
|
206→ // Check if last 6 characters are all digits
|
|
207→ if filename.len() >= 6 {
|
|
208→ let last_six = &filename[filename.len() - 6..];
|
|
209→ if last_six.chars().all(|c| c.is_ascii_digit()) {
|
|
210→ return Some(last_six.to_string());
|
|
211→ }
|
|
212→ }
|
|
213→
|
|
214→ None
|
|
215→ }
|
|
216→
|
|
217→ /// Check if embedded configuration exists in the executable
|
|
218→ pub fn has_embedded_config() -> bool {
|
|
219→ Self::read_embedded_config().is_ok()
|
|
220→ }
|
|
221→
|
|
222→ /// Read embedded configuration from the executable
|
|
223→ pub fn read_embedded_config() -> Result<EmbeddedConfig> {
|
|
224→ let exe_path = std::env::current_exe()
|
|
225→ .context("Failed to get current executable path")?;
|
|
226→
|
|
227→ let mut file = std::fs::File::open(&exe_path)
|
|
228→ .context("Failed to open executable for reading")?;
|
|
229→
|
|
230→ let file_size = file.metadata()?.len();
|
|
231→ if file_size < (MAGIC_MARKER.len() + 4) as u64 {
|
|
232→ return Err(anyhow!("File too small to contain embedded config"));
|
|
233→ }
|
|
234→
|
|
235→ // Read the last part of the file to find magic marker
|
|
236→ // Structure: [PE binary][GURUCONFIG][length:u32][json config]
|
|
237→ // We need to search backwards from the end
|
|
238→
|
|
239→ // Read last 64KB (should be more than enough for config)
|
|
240→ let search_size = std::cmp::min(65536, file_size as usize);
|
|
241→ let search_start = file_size - search_size as u64;
|
|
242→
|
|
243→ file.seek(SeekFrom::Start(search_start))?;
|
|
244→ let mut buffer = vec![0u8; search_size];
|
|
245→ file.read_exact(&mut buffer)?;
|
|
246→
|
|
247→ // Find magic marker
|
|
248→ let marker_pos = buffer.windows(MAGIC_MARKER.len())
|
|
249→ .rposition(|window| window == MAGIC_MARKER)
|
|
250→ .ok_or_else(|| anyhow!("Magic marker not found"))?;
|
|
251→
|
|
252→ // Read config length (4 bytes after marker)
|
|
253→ let length_start = marker_pos + MAGIC_MARKER.len();
|
|
254→ if length_start + 4 > buffer.len() {
|
|
255→ return Err(anyhow!("Invalid embedded config: length field truncated"));
|
|
256→ }
|
|
257→
|
|
258→ let config_length = u32::from_le_bytes([
|
|
259→ buffer[length_start],
|
|
260→ buffer[length_start + 1],
|
|
261→ buffer[length_start + 2],
|
|
262→ buffer[length_start + 3],
|
|
263→ ]) as usize;
|
|
264→
|
|
265→ // Read config data
|
|
266→ let config_start = length_start + 4;
|
|
267→ if config_start + config_length > buffer.len() {
|
|
268→ return Err(anyhow!("Invalid embedded config: data truncated"));
|
|
269→ }
|
|
270→
|
|
271→ let config_bytes = &buffer[config_start..config_start + config_length];
|
|
272→ let config: EmbeddedConfig = serde_json::from_slice(config_bytes)
|
|
273→ .context("Failed to parse embedded config JSON")?;
|
|
274→
|
|
275→ info!("Loaded embedded config: server={}, company={:?}",
|
|
276→ config.server_url, config.company);
|
|
277→
|
|
278→ Ok(config)
|
|
279→ }
|
|
280→
|
|
281→ /// Check if an explicit agent configuration file exists
|
|
282→ /// This returns true only if there's a real config file, not generated defaults
|
|
283→ pub fn has_agent_config() -> bool {
|
|
284→ // Check for embedded config first
|
|
285→ if Self::has_embedded_config() {
|
|
286→ return true;
|
|
287→ }
|
|
288→
|
|
289→ // Check for config in current directory
|
|
290→ let local_config = PathBuf::from("guruconnect.toml");
|
|
291→ if local_config.exists() {
|
|
292→ return true;
|
|
293→ }
|
|
294→
|
|
295→ // Check in program data directory (Windows)
|
|
296→ #[cfg(windows)]
|
|
297→ {
|
|
298→ if let Ok(program_data) = std::env::var("ProgramData") {
|
|
299→ let path = PathBuf::from(program_data)
|
|
300→ .join("GuruConnect")
|
|
301→ .join("agent.toml");
|
|
302→ if path.exists() {
|
|
303→ return true;
|
|
304→ }
|
|
305→ }
|
|
306→ }
|
|
307→
|
|
308→ false
|
|
309→ }
|
|
310→
|
|
311→ /// Load configuration from embedded config, file, or environment
|
|
312→ pub fn load() -> Result<Self> {
|
|
313→ // Priority 1: Try loading from embedded config
|
|
314→ if let Ok(embedded) = Self::read_embedded_config() {
|
|
315→ info!("Using embedded configuration");
|
|
316→ let config = Config {
|
|
317→ server_url: embedded.server_url,
|
|
318→ api_key: embedded.api_key,
|
|
319→ agent_id: generate_agent_id(),
|
|
320→ hostname_override: None,
|
|
321→ company: embedded.company,
|
|
322→ site: embedded.site,
|
|
323→ tags: embedded.tags,
|
|
324→ support_code: None,
|
|
325→ capture: CaptureConfig::default(),
|
|
326→ encoding: EncodingConfig::default(),
|
|
327→ };
|
|
328→
|
|
329→ // Save to file for persistence (so agent_id is preserved)
|
|
330→ let _ = config.save();
|
|
331→ return Ok(config);
|
|
332→ }
|
|
333→
|
|
334→ // Priority 2: Try loading from config file
|
|
335→ let config_path = Self::config_path();
|
|
336→
|
|
337→ if config_path.exists() {
|
|
338→ let contents = std::fs::read_to_string(&config_path)
|
|
339→ .with_context(|| format!("Failed to read config from {:?}", config_path))?;
|
|
340→
|
|
341→ let mut config: Config = toml::from_str(&contents)
|
|
342→ .with_context(|| "Failed to parse config file")?;
|
|
343→
|
|
344→ // Ensure agent_id is set and saved
|
|
345→ if config.agent_id.is_empty() {
|
|
346→ config.agent_id = generate_agent_id();
|
|
347→ let _ = config.save();
|
|
348→ }
|
|
349→
|
|
350→ // support_code is always None when loading from file (set via CLI)
|
|
351→ config.support_code = None;
|
|
352→
|
|
353→ return Ok(config);
|
|
354→ }
|
|
355→
|
|
356→ // Priority 3: Fall back to environment variables
|
|
357→ let server_url = std::env::var("GURUCONNECT_SERVER_URL")
|
|
358→ .unwrap_or_else(|_| "wss://connect.azcomputerguru.com/ws/agent".to_string());
|
|
359→
|
|
360→ let api_key = std::env::var("GURUCONNECT_API_KEY")
|
|
361→ .unwrap_or_else(|_| "dev-key".to_string());
|
|
362→
|
|
363→ let agent_id = std::env::var("GURUCONNECT_AGENT_ID")
|
|
364→ .unwrap_or_else(|_| generate_agent_id());
|
|
365→
|
|
366→ let config = Config {
|
|
367→ server_url,
|
|
368→ api_key,
|
|
369→ agent_id,
|
|
370→ hostname_override: std::env::var("GURUCONNECT_HOSTNAME").ok(),
|
|
371→ company: None,
|
|
372→ site: None,
|
|
373→ tags: Vec::new(),
|
|
374→ support_code: None,
|
|
375→ capture: CaptureConfig::default(),
|
|
376→ encoding: EncodingConfig::default(),
|
|
377→ };
|
|
378→
|
|
379→ // Save config with generated agent_id for persistence
|
|
380→ let _ = config.save();
|
|
381→
|
|
382→ Ok(config)
|
|
383→ }
|
|
384→
|
|
385→ /// Get the configuration file path
|
|
386→ fn config_path() -> PathBuf {
|
|
387→ // Check for config in current directory first
|
|
388→ let local_config = PathBuf::from("guruconnect.toml");
|
|
389→ if local_config.exists() {
|
|
390→ return local_config;
|
|
391→ }
|
|
392→
|
|
393→ // Check in program data directory (Windows)
|
|
394→ #[cfg(windows)]
|
|
395→ {
|
|
396→ if let Ok(program_data) = std::env::var("ProgramData") {
|
|
397→ let path = PathBuf::from(program_data)
|
|
398→ .join("GuruConnect")
|
|
399→ .join("agent.toml");
|
|
400→ if path.exists() {
|
|
401→ return path;
|
|
402→ }
|
|
403→ }
|
|
404→ }
|
|
405→
|
|
406→ // Default to local config
|
|
407→ local_config
|
|
408→ }
|
|
409→
|
|
410→ /// Get the hostname to use
|
|
411→ pub fn hostname(&self) -> String {
|
|
412→ self.hostname_override
|
|
413→ .clone()
|
|
414→ .unwrap_or_else(|| {
|
|
415→ hostname::get()
|
|
416→ .map(|h| h.to_string_lossy().to_string())
|
|
417→ .unwrap_or_else(|_| "unknown".to_string())
|
|
418→ })
|
|
419→ }
|
|
420→
|
|
421→ /// Save current configuration to file
|
|
422→ pub fn save(&self) -> Result<()> {
|
|
423→ let config_path = Self::config_path();
|
|
424→
|
|
425→ // Ensure parent directory exists
|
|
426→ if let Some(parent) = config_path.parent() {
|
|
427→ std::fs::create_dir_all(parent)?;
|
|
428→ }
|
|
429→
|
|
430→ let contents = toml::to_string_pretty(self)?;
|
|
431→ std::fs::write(&config_path, contents)?;
|
|
432→
|
|
433→ Ok(())
|
|
434→ }
|
|
435→}
|
|
436→
|
|
437→/// Example configuration file content
|
|
438→pub fn example_config() -> &'static str {
|
|
439→ r#"# GuruConnect Agent Configuration
|
|
440→
|
|
441→# Server connection
|
|
442→server_url = "wss://connect.example.com/ws"
|
|
443→api_key = "your-agent-api-key"
|
|
444→agent_id = "auto-generated-uuid"
|
|
445→
|
|
446→# Optional: override hostname
|
|
447→# hostname_override = "custom-hostname"
|
|
448→
|
|
449→[capture]
|
|
450→fps = 30
|
|
451→use_dxgi = true
|
|
452→gdi_fallback = true
|
|
453→
|
|
454→[encoding]
|
|
455→codec = "auto" # auto, raw, vp9, h264
|
|
456→quality = 75 # 1-100
|
|
457→hardware_encoding = true
|
|
458→"#
|
|
459→}
|
|
460→
|
|
|
|
<system-reminder>
|
|
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
|
|
</system-reminder>
|