Files
Mike Swanson 75ce1c2fd5 feat: Add Sequential Thinking to Code Review + Frontend Validation
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>
2026-01-17 16:23:52 -07:00

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>