   155→        Ok(config)
   156→    }
   157→
   158→    /// Get the configuration file path
   159→    fn config_path() -> PathBuf {
   160→        // Check for config in current directory first
   161→        let local_config = PathBuf::from("guruconnect.toml");
   162→        if local_config.exists() {
   163→            return local_config;
   164→        }
   165→
   166→        // Check in program data directory (Windows)
   167→        #[cfg(windows)]
   168→        {
   169→            if let Ok(program_data) = std::env::var("ProgramData") {
   170→                let path = PathBuf::from(program_data)
   171→                    .join("GuruConnect")
   172→                    .join("agent.toml");
   173→                if path.exists() {
   174→                    return path;
   175→                }
   176→            }
   177→        }
   178→
   179→        // Default to local config
   180→        local_config
   181→    }
   182→
   183→    /// Get the hostname to use
   184→    pub fn hostname(&self) -> String {
   185→        self.hostname_override
   186→            .clone()
   187→            .unwrap_or_else(|| {
   188→                hostname::get()
   189→                    .map(|h| h.to_string_lossy().to_string())
   190→                    .unwrap_or_else(|_| "unknown".to_string())
   191→            })
   192→    }
   193→
   194→    /// Save current configuration to file
   195→    pub fn save(&self) -> Result<()> {
   196→        let config_path = Self::config_path();
   197→
   198→        // Ensure parent directory exists
   199→        if let Some(parent) = config_path.parent() {
   200→            std::fs::create_dir_all(parent)?;
   201→        }
   202→
   203→        let contents = toml::to_string_pretty(self)?;
   204→        std::fs::write(&config_path, contents)?;
   205→
   206→        Ok(())
   207→    }
   208→}
   209→
   210→/// Example configuration file content
   211→pub fn example_config() -> &'static str {
   212→    r#"# GuruConnect Agent Configuration
   213→
   214→# Server connection

<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>

        Ok(config)
    }

    /// Get the configuration file path
    fn config_path() -> PathBuf {
        // Check for config in current directory first
        let local_config = PathBuf::from("guruconnect.toml");
        if local_config.exists() {
            return local_config;
        }

        // Check in program data directory (Windows)
        #[cfg(windows)]
        {
            if let Ok(program_data) = std::env::var("ProgramData") {
                let path = PathBuf::from(program_data)
                    .join("GuruConnect")
                    .join("agent.toml");
                if path.exists() {
                    return path;
                }
            }
        }

        // Default to local config
        local_config
    }

    /// Get the hostname to use
    pub fn hostname(&self) -> String {
        self.hostname_override
            .clone()
            .unwrap_or_else(|| {
                hostname::get()
                    .map(|h| h.to_string_lossy().to_string())
                    .unwrap_or_else(|_| "unknown".to_string())
            })
    }

    /// Save current configuration to file
    pub fn save(&self) -> Result<()> {
        let config_path = Self::config_path();

        // Ensure parent directory exists
        if let Some(parent) = config_path.parent() {
            std::fs::create_dir_all(parent)?;
        }

        let contents = toml::to_string_pretty(self)?;
        std::fs::write(&config_path, contents)?;

        Ok(())
    }
}

/// Example configuration file content
pub fn example_config() -> &'static str {
    r#"# GuruConnect Agent Configuration

# Server connection


Now I understand the config structure. Let me add a function to check if an explicit agent config exists (not a generated one). I'll add:1. `config::has_agent_config()` - Returns true only if an agent config file exists
2. Modify main.rs to only run as agent if:   - `guruconnect agent` command is used, OR   - A support code is provided, OR   - An explicit agent config file existsThis way:- Viewer-only installs: No agent.toml → doesn't run as agent
- Agent installs: Has agent.toml → runs as agentLet me add the function to config.rs:


Let me add a function to check if an agent config explicitly exists, then update main.rs:


Edit

impl Config {
    /// Load configuration from file or environment
    pub fn load() -> Result<Self> {
        // Try loading from config file
        let config_path = Self::config_path();

impl Config {
    /// Check if an explicit agent configuration file exists
    /// This returns true only if there's a real config file, not generated defaults
    pub fn has_agent_config() -> bool {
        // Check for config in current directory
        let local_config = PathBuf::from("guruconnect.toml");
        if local_config.exists() {
            return true;
        }

        // Check in program data directory (Windows)
        #[cfg(windows)]
        {
            if let Ok(program_data) = std::env::var("ProgramData") {
                let path = PathBuf::from(program_data)
                    .join("GuruConnect")
                    .join("agent.toml");
                if path.exists() {
                    return true;
                }
            }
        }

        false
    }

    /// Load configuration from file or environment
    pub fn load() -> Result<Self> {
        // Try loading from config file
        let config_path = Self::config_path();


The file C:\Users\MikeSwanson\Claude\guru-connect\agent\src\config.rs has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
   103→        }
   104→    }
   105→}
   106→
   107→impl Config {
   108→    /// Check if an explicit agent configuration file exists
   109→    /// This returns true only if there's a real config file, not generated defaults
   110→    pub fn has_agent_config() -> bool {
   111→        // Check for config in current directory
   112→        let local_config = PathBuf::from("guruconnect.toml");
   113→        if local_config.exists() {
   114→            return true;
   115→        }
   116→
   117→        // Check in program data directory (Windows)
   118→        #[cfg(windows)]
   119→        {
   120→            if let Ok(program_data) = std::env::var("ProgramData") {
   121→                let path = PathBuf::from(program_data)
   122→                    .join("GuruConnect")
   123→                    .join("agent.toml");
   124→                if path.exists() {
   125→                    return true;
   126→                }
   127→            }
   128→        }
   129→
   130→        false
   131→    }
   132→
   133→    /// Load configuration from file or environment
   134→    pub fn load() -> Result<Self> {
   135→        // Try loading from config file
   136→        let config_path = Self::config_path();
   137→
   138→        if config_path.exists() {
   139→            let contents = std::fs::read_to_string(&config_path)
   140→                .with_context(|| format!("Failed to read config from {:?}", config_path))?;

impl Config {
    /// Check if an explicit agent configuration file exists
    /// This returns true only if there's a real config file, not generated defaults
    pub fn has_agent_config() -> bool {
        // Check for config in current directory
        let local_config = PathBuf::from("guruconnect.toml");
        if local_config.exists() {
            return true;
        }

        // Check in program data directory (Windows)
        #[cfg(windows)]
        {
            if let Ok(program_data) = std::env::var("ProgramData") {
                let path = PathBuf::from(program_data)
                    .join("GuruConnect")
                    .join("agent.toml");
                if path.exists() {
                    return true;
                }
            }
        }

        false
    }

    /// Load configuration from file or environment
    pub fn load() -> Result<Self> {
        // Try loading from config file
        let config_path = Self::config_path();

//! Agent configuration management

use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use uuid::Uuid;

/// Agent configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
    /// Server WebSocket URL (e.g., wss://connect.example.com/ws)
    pub server_url: String,

    /// Agent API key for authentication
    pub api_key: String,

    /// Unique agent identifier (generated on first run)
    #[serde(default = "generate_agent_id")]
    pub agent_id: String,

    /// Optional hostname override
    pub hostname_override: Option<String>,

    /// Support code for one-time support sessions (set via command line)
    #[serde(skip)]
    pub support_code: Option<String>,

    /// Capture settings
    #[serde(default)]
    pub capture: CaptureConfig,

    /// Encoding settings
    #[serde(default)]
    pub encoding: EncodingConfig,
}

fn generate_agent_id() -> String {
    Uuid::new_v4().to_string()
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CaptureConfig {
    /// Target frames per second (1-60)
    #[serde(default = "default_fps")]
    pub fps: u32,

    /// Use DXGI Desktop Duplication (recommended)
    #[serde(default = "default_true")]
    pub use_dxgi: bool,

    /// Fall back to GDI if DXGI fails
    #[serde(default = "default_true")]
    pub gdi_fallback: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncodingConfig {
    /// Preferred codec (auto, raw, vp9, h264)
    #[serde(default = "default_codec")]
    pub codec: String,

    /// Quality (1-100, higher = better quality, more bandwidth)
    #[serde(default = "default_quality")]
    pub quality: u32,

    /// Use hardware encoding if available
    #[serde(default = "default_true")]
    pub hardware_encoding: bool,
}

fn default_fps() -> u32 {
    30
}

fn default_true() -> bool {
    true
}

fn default_codec() -> String {
    "auto".to_string()
}

fn default_quality() -> u32 {
    75
}

impl Default for CaptureConfig {
    fn default() -> Self {
        Self {
            fps: default_fps(),
            use_dxgi: true,
            gdi_fallback: true,
        }
    }
}

impl Default for EncodingConfig {
    fn default() -> Self {
        Self {
            codec: default_codec(),
            quality: default_quality(),
            hardware_encoding: true,
        }
    }
}

impl Config {
    /// Load configuration from file or environment
    pub fn load() -> Result<Self> {
        // Try loading from config file
        let config_path = Self::config_path();

        if config_path.exists() {
            let contents = std::fs::read_to_string(&config_path)
                .with_context(|| format!("Failed to read config from {:?}", config_path))?;

            let mut config: Config = toml::from_str(&contents)
                .with_context(|| "Failed to parse config file")?;

            // Ensure agent_id is set and saved
            if config.agent_id.is_empty() {
                config.agent_id = generate_agent_id();
                let _ = config.save();
            }

            // support_code is always None when loading from file (set via CLI)
            config.support_code = None;

            return Ok(config);
        }

        // Fall back to environment variables
        let server_url = std::env::var("GURUCONNECT_SERVER_URL")
            .unwrap_or_else(|_| "wss://connect.azcomputerguru.com/ws/agent".to_string());

        let api_key = std::env::var("GURUCONNECT_API_KEY")
            .unwrap_or_else(|_| "dev-key".to_string());

        let agent_id = std::env::var("GURUCONNECT_AGENT_ID")
            .unwrap_or_else(|_| generate_agent_id());

        let config = Config {
            server_url,
            api_key,
            agent_id,
            hostname_override: std::env::var("GURUCONNECT_HOSTNAME").ok(),
            support_code: None, // Set via CLI
            capture: CaptureConfig::default(),
            encoding: EncodingConfig::default(),
        };

        // Save config with generated agent_id for persistence
        let _ = config.save();

        Ok(config)
    }

    /// Get the configuration file path
    fn config_path() -> PathBuf {
        // Check for config in current directory first
        let local_config = PathBuf::from("guruconnect.toml");
        if local_config.exists() {
            return local_config;
        }

        // Check in program data directory (Windows)
        #[cfg(windows)]
        {
            if let Ok(program_data) = std::env::var("ProgramData") {
                let path = PathBuf::from(program_data)
                    .join("GuruConnect")
                    .join("agent.toml");
                if path.exists() {
                    return path;
                }
            }
        }

        // Default to local config
        local_config
    }

    /// Get the hostname to use
    pub fn hostname(&self) -> String {
        self.hostname_override
            .clone()
            .unwrap_or_else(|| {
                hostname::get()
                    .map(|h| h.to_string_lossy().to_string())
                    .unwrap_or_else(|_| "unknown".to_string())
            })
    }

    /// Save current configuration to file
    pub fn save(&self) -> Result<()> {
        let config_path = Self::config_path();

        // Ensure parent directory exists
        if let Some(parent) = config_path.parent() {
            std::fs::create_dir_all(parent)?;
        }

        let contents = toml::to_string_pretty(self)?;
        std::fs::write(&config_path, contents)?;

        Ok(())
    }
}

/// Example configuration file content
pub fn example_config() -> &'static str {
    r#"# GuruConnect Agent Configuration

# Server connection
server_url = "wss://connect.example.com/ws"
api_key = "your-agent-api-key"
agent_id = "auto-generated-uuid"

# Optional: override hostname
# hostname_override = "custom-hostname"

[capture]
fps = 30
use_dxgi = true
gdi_fallback = true

[encoding]
codec = "auto"      # auto, raw, vp9, h264
quality = 75        # 1-100
hardware_encoding = true
"#
}

+    /// Check if an explicit agent configuration file exists

+    /// This returns true only if there's a real config file, not generated defaults

+    pub fn has_agent_config() -> bool {

+        // Check for config in current directory

+        let local_config = PathBuf::from("guruconnect.toml");

+        // Check in program data directory (Windows)

+            if let Ok(program_data) = std::env::var("ProgramData") {

+                let path = PathBuf::from(program_data)

+                    .join("GuruConnect")

+                    .join("agent.toml");


Now let me update main.rs to use this new function and change the default behavior:


Now let me update main.rs to only run as agent when there's an explicit config:


        None => {
            // Legacy mode: if a support code was provided, run as agent
            if let Some(code) = cli.support_code {
                run_agent_mode(Some(code))
            } else {
                // No args: check if protocol handler is installed
                // If not, run install mode (user likely downloaded from web)
                if !install::is_protocol_handler_registered() {
                    info!("Protocol handler not registered, running installer");
                    run_install(false)
                } else {
                    // Protocol handler exists, run as agent
                    run_agent_mode(None)
                }
            }
        }

        None => {
            // Legacy mode: if a support code was provided, run as agent
            if let Some(code) = cli.support_code {
                run_agent_mode(Some(code))
            } else {
                // No args: check what mode to run
                if !install::is_protocol_handler_registered() {
                    // Protocol handler not registered - user likely downloaded from web
                    // Run installer to set up protocol handler
                    info!("Protocol handler not registered, running installer");
                    run_install(false)
                } else if config::Config::has_agent_config() {
                    // Protocol handler exists AND agent config exists
                    // This is an agent installation - run as agent
                    info!("Agent config found, running as agent");
                    run_agent_mode(None)
                } else {
                    // Protocol handler exists but NO agent config
                    // This is a viewer-only installation - just exit silently
                    // The protocol handler will launch the viewer when needed
                    info!("Viewer-only installation, exiting (use 'guruconnect agent' to run as agent)");
                    Ok(())
                }
            }
        }


The file C:\Users\MikeSwanson\Claude\guru-connect\agent\src\main.rs has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
   215→            show_debug_console();
   216→            println!("{}", build_info::full_version());
   217→            Ok(())
   218→        }
   219→        None => {
   220→            // Legacy mode: if a support code was provided, run as agent
   221→            if let Some(code) = cli.support_code {
   222→                run_agent_mode(Some(code))
   223→            } else {
   224→                // No args: check what mode to run
   225→                if !install::is_protocol_handler_registered() {
   226→                    // Protocol handler not registered - user likely downloaded from web
   227→                    // Run installer to set up protocol handler
   228→                    info!("Protocol handler not registered, running installer");
   229→                    run_install(false)
   230→                } else if config::Config::has_agent_config() {
   231→                    // Protocol handler exists AND agent config exists
   232→                    // This is an agent installation - run as agent
   233→                    info!("Agent config found, running as agent");
   234→                    run_agent_mode(None)
   235→                } else {
   236→                    // Protocol handler exists but NO agent config
   237→                    // This is a viewer-only installation - just exit silently
   238→                    // The protocol handler will launch the viewer when needed
   239→                    info!("Viewer-only installation, exiting (use 'guruconnect agent' to run as agent)");
   240→                    Ok(())
   241→                }
   242→            }
   243→        }
   244→    }
   245→}
   246→
   247→/// Run in agent mode (receive remote connections)

        None => {
            // Legacy mode: if a support code was provided, run as agent
            if let Some(code) = cli.support_code {
                run_agent_mode(Some(code))
            } else {
                // No args: check what mode to run
                if !install::is_protocol_handler_registered() {
                    // Protocol handler not registered - user likely downloaded from web
                    // Run installer to set up protocol handler
                    info!("Protocol handler not registered, running installer");
                    run_install(false)
                } else if config::Config::has_agent_config() {
                    // Protocol handler exists AND agent config exists
                    // This is an agent installation - run as agent
                    info!("Agent config found, running as agent");
                    run_agent_mode(None)
                } else {
                    // Protocol handler exists but NO agent config
                    // This is a viewer-only installation - just exit silently
                    // The protocol handler will launch the viewer when needed
                    info!("Viewer-only installation, exiting (use 'guruconnect agent' to run as agent)");
                    Ok(())
                }
            }
        }

//! GuruConnect - Remote Desktop Agent and Viewer
//!
//! Single binary for both agent (receiving connections) and viewer (initiating connections).
//!
//! Usage:
//!   guruconnect agent              - Run as background agent
//!   guruconnect view <session_id>  - View a remote session
//!   guruconnect install            - Install and register protocol handler
//!   guruconnect launch <url>       - Handle guruconnect:// URL
//!   guruconnect [support_code]     - Legacy: run agent with support code

// Hide console window by default on Windows (release builds)
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

mod capture;
mod chat;
mod config;
mod encoder;
mod input;
mod install;
mod sas_client;
mod session;
mod startup;
mod transport;
mod tray;
mod update;
mod viewer;

pub mod proto {
    include!(concat!(env!("OUT_DIR"), "/guruconnect.rs"));
}

/// Build information embedded at compile time
pub mod build_info {
    /// Cargo package version (from Cargo.toml)
    pub const VERSION: &str = env!("CARGO_PKG_VERSION");

    /// Git commit hash (short, 8 chars)
    pub const GIT_HASH: &str = env!("GIT_HASH");

    /// Git commit hash (full)
    pub const GIT_HASH_FULL: &str = env!("GIT_HASH_FULL");

    /// Git branch name
    pub const GIT_BRANCH: &str = env!("GIT_BRANCH");

    /// Git dirty state ("clean" or "dirty")
    pub const GIT_DIRTY: &str = env!("GIT_DIRTY");

    /// Git commit date
    pub const GIT_COMMIT_DATE: &str = env!("GIT_COMMIT_DATE");

    /// Build timestamp (UTC)
    pub const BUILD_TIMESTAMP: &str = env!("BUILD_TIMESTAMP");

    /// Build profile (debug/release)
    pub const BUILD_PROFILE: &str = env!("BUILD_PROFILE");

    /// Target triple (e.g., x86_64-pc-windows-msvc)
    pub const BUILD_TARGET: &str = env!("BUILD_TARGET");

    /// Short version string for display (version + git hash)
    pub fn short_version() -> String {
        if GIT_DIRTY == "dirty" {
            format!("{}-{}-dirty", VERSION, GIT_HASH)
        } else {
            format!("{}-{}", VERSION, GIT_HASH)
        }
    }

    /// Full version string with all details
    pub fn full_version() -> String {
        format!(
            "GuruConnect v{}\n\
             Git:     {} ({})\n\
             Branch:  {}\n\
             Commit:  {}\n\
             Built:   {}\n\
             Profile: {}\n\
             Target:  {}",
            VERSION,
            GIT_HASH,
            GIT_DIRTY,
            GIT_BRANCH,
            GIT_COMMIT_DATE,
            BUILD_TIMESTAMP,
            BUILD_PROFILE,
            BUILD_TARGET
        )
    }
}

use anyhow::Result;
use clap::{Parser, Subcommand};
use tracing::{info, error, warn, Level};
use tracing_subscriber::FmtSubscriber;

#[cfg(windows)]
use windows::Win32::UI::WindowsAndMessaging::{MessageBoxW, MB_OK, MB_ICONINFORMATION, MB_ICONERROR};
#[cfg(windows)]
use windows::core::PCWSTR;
#[cfg(windows)]
use windows::Win32::System::Console::{AllocConsole, GetConsoleWindow};
#[cfg(windows)]
use windows::Win32::UI::WindowsAndMessaging::{ShowWindow, SW_SHOW};

/// GuruConnect Remote Desktop
#[derive(Parser)]
#[command(name = "guruconnect")]
#[command(version = concat!(env!("CARGO_PKG_VERSION"), "-", env!("GIT_HASH")), about = "Remote desktop agent and viewer")]
struct Cli {
    #[command(subcommand)]
    command: Option<Commands>,

    /// Support code for legacy mode (runs agent with code)
    #[arg(value_name = "SUPPORT_CODE")]
    support_code: Option<String>,

    /// Enable verbose logging
    #[arg(short, long, global = true)]
    verbose: bool,

    /// Internal flag: set after auto-update to trigger cleanup
    #[arg(long, hide = true)]
    post_update: bool,
}

#[derive(Subcommand)]
enum Commands {
    /// Run as background agent (receive remote connections)
    Agent {
        /// Support code for one-time session
        #[arg(short, long)]
        code: Option<String>,
    },

    /// View a remote session (connect to an agent)
    View {
        /// Session ID to connect to
        session_id: String,

        /// Server URL
        #[arg(short, long, default_value = "wss://connect.azcomputerguru.com/ws/viewer")]
        server: String,

        /// API key for authentication
        #[arg(short, long, default_value = "")]
        api_key: String,
    },

    /// Install GuruConnect and register protocol handler
    Install {
        /// Skip UAC elevation, install for current user only
        #[arg(long)]
        user_only: bool,

        /// Called internally when running elevated
        #[arg(long, hide = true)]
        elevated: bool,
    },

    /// Uninstall GuruConnect
    Uninstall,

    /// Handle a guruconnect:// protocol URL
    Launch {
        /// The guruconnect:// URL to handle
        url: String,
    },

    /// Show detailed version and build information
    #[command(name = "version-info")]
    VersionInfo,
}

fn main() -> Result<()> {
    let cli = Cli::parse();

    // Initialize logging
    let level = if cli.verbose { Level::DEBUG } else { Level::INFO };
    FmtSubscriber::builder()
        .with_max_level(level)
        .with_target(true)
        .with_thread_ids(true)
        .init();

    info!("GuruConnect {} ({})", build_info::short_version(), build_info::BUILD_TARGET);
    info!("Built: {} | Commit: {}", build_info::BUILD_TIMESTAMP, build_info::GIT_COMMIT_DATE);

    // Handle post-update cleanup
    if cli.post_update {
        info!("Post-update mode: cleaning up old executable");
        update::cleanup_post_update();
    }

    match cli.command {
        Some(Commands::Agent { code }) => {
            run_agent_mode(code)
        }
        Some(Commands::View { session_id, server, api_key }) => {
            run_viewer_mode(&server, &session_id, &api_key)
        }
        Some(Commands::Install { user_only, elevated }) => {
            run_install(user_only || elevated)
        }
        Some(Commands::Uninstall) => {
            run_uninstall()
        }
        Some(Commands::Launch { url }) => {
            run_launch(&url)
        }
        Some(Commands::VersionInfo) => {
            // Show detailed version info (allocate console on Windows for visibility)
            #[cfg(windows)]
            show_debug_console();
            println!("{}", build_info::full_version());
            Ok(())
        }
        None => {
            // Legacy mode: if a support code was provided, run as agent
            if let Some(code) = cli.support_code {
                run_agent_mode(Some(code))
            } else {
                // No args: check if protocol handler is installed
                // If not, run install mode (user likely downloaded from web)
                if !install::is_protocol_handler_registered() {
                    info!("Protocol handler not registered, running installer");
                    run_install(false)
                } else {
                    // Protocol handler exists, run as agent
                    run_agent_mode(None)
                }
            }
        }
    }
}

/// Run in agent mode (receive remote connections)
fn run_agent_mode(support_code: Option<String>) -> Result<()> {
    info!("Running in agent mode");

    // Check elevation status
    if install::is_elevated() {
        info!("Running with elevated (administrator) privileges");
    } else {
        info!("Running with standard user privileges");
    }

    // Also check for support code in filename (legacy compatibility)
    let code = support_code.or_else(extract_code_from_filename);
    if let Some(ref c) = code {
        info!("Support code: {}", c);
    }

    // Load configuration
    let mut config = config::Config::load()?;
    config.support_code = code;
    info!("Server: {}", config.server_url);

    // Run the agent
    let rt = tokio::runtime::Runtime::new()?;
    rt.block_on(run_agent(config))
}

/// Run in viewer mode (connect to remote session)
fn run_viewer_mode(server: &str, session_id: &str, api_key: &str) -> Result<()> {
    info!("Running in viewer mode");
    info!("Connecting to session: {}", session_id);

    let rt = tokio::runtime::Runtime::new()?;
    rt.block_on(viewer::run(server, session_id, api_key))
}

/// Handle guruconnect:// URL launch
fn run_launch(url: &str) -> Result<()> {
    info!("Handling protocol URL: {}", url);

    match install::parse_protocol_url(url) {
        Ok((server, session_id, token)) => {
            let api_key = token.unwrap_or_default();
            run_viewer_mode(&server, &session_id, &api_key)
        }
        Err(e) => {
            error!("Failed to parse URL: {}", e);
            show_error_box("GuruConnect", &format!("Invalid URL: {}", e));
            Err(e)
        }
    }
}

/// Install GuruConnect
fn run_install(force_user_install: bool) -> Result<()> {
    info!("Installing GuruConnect...");

    match install::install(force_user_install) {
        Ok(()) => {
            show_message_box("GuruConnect", "Installation complete!\n\nYou can now use guruconnect:// links.");
            Ok(())
        }
        Err(e) => {
            error!("Installation failed: {}", e);
            show_error_box("GuruConnect", &format!("Installation failed: {}", e));
            Err(e)
        }
    }
}

/// Uninstall GuruConnect
fn run_uninstall() -> Result<()> {
    info!("Uninstalling GuruConnect...");

    // Remove from startup
    if let Err(e) = startup::remove_from_startup() {
        warn!("Failed to remove from startup: {}", e);
    }

    // TODO: Remove registry keys for protocol handler
    // TODO: Remove install directory

    show_message_box("GuruConnect", "Uninstall complete.");
    Ok(())
}

/// Extract a 6-digit support code from the executable's filename
fn extract_code_from_filename() -> Option<String> {
    let exe_path = std::env::current_exe().ok()?;
    let filename = exe_path.file_stem()?.to_str()?;

    // Look for a 6-digit number in the filename
    for part in filename.split(|c| c == '-' || c == '_' || c == '.') {
        let trimmed = part.trim();
        if trimmed.len() == 6 && trimmed.chars().all(|c| c.is_ascii_digit()) {
            return Some(trimmed.to_string());
        }
    }

    // Check if the last 6 characters are digits
    if filename.len() >= 6 {
        let last_six = &filename[filename.len() - 6..];
        if last_six.chars().all(|c| c.is_ascii_digit()) {
            return Some(last_six.to_string());
        }
    }

    None
}

/// Show a message box (Windows only)
#[cfg(windows)]
fn show_message_box(title: &str, message: &str) {
    use std::ffi::OsStr;
    use std::os::windows::ffi::OsStrExt;

    let title_wide: Vec<u16> = OsStr::new(title)
        .encode_wide()
        .chain(std::iter::once(0))
        .collect();
    let message_wide: Vec<u16> = OsStr::new(message)
        .encode_wide()
        .chain(std::iter::once(0))
        .collect();

    unsafe {
        MessageBoxW(
            None,
            PCWSTR(message_wide.as_ptr()),
            PCWSTR(title_wide.as_ptr()),
            MB_OK | MB_ICONINFORMATION,
        );
    }
}

#[cfg(not(windows))]
fn show_message_box(_title: &str, message: &str) {
    println!("{}", message);
}

/// Show an error message box (Windows only)
#[cfg(windows)]
fn show_error_box(title: &str, message: &str) {
    use std::ffi::OsStr;
    use std::os::windows::ffi::OsStrExt;

    let title_wide: Vec<u16> = OsStr::new(title)
        .encode_wide()
        .chain(std::iter::once(0))
        .collect();
    let message_wide: Vec<u16> = OsStr::new(message)
        .encode_wide()
        .chain(std::iter::once(0))
        .collect();

    unsafe {
        MessageBoxW(
            None,
            PCWSTR(message_wide.as_ptr()),
            PCWSTR(title_wide.as_ptr()),
            MB_OK | MB_ICONERROR,
        );
    }
}

#[cfg(not(windows))]
fn show_error_box(_title: &str, message: &str) {
    eprintln!("ERROR: {}", message);
}

/// Show debug console window (Windows only)
#[cfg(windows)]
#[allow(dead_code)]
fn show_debug_console() {
    unsafe {
        let hwnd = GetConsoleWindow();
        if hwnd.0 == std::ptr::null_mut() {
            let _ = AllocConsole();
        } else {
            let _ = ShowWindow(hwnd, SW_SHOW);
        }
    }
}

#[cfg(not(windows))]
#[allow(dead_code)]
fn show_debug_console() {}

/// Clean up before exiting
fn cleanup_on_exit() {
    info!("Cleaning up before exit");
    if let Err(e) = startup::remove_from_startup() {
        warn!("Failed to remove from startup: {}", e);
    }
}

/// Run the agent main loop
async fn run_agent(config: config::Config) -> Result<()> {
    let elevated = install::is_elevated();
    let mut session = session::SessionManager::new(config.clone(), elevated);
    let is_support_session = config.support_code.is_some();
    let hostname = config.hostname();

    // Add to startup
    if let Err(e) = startup::add_to_startup() {
        warn!("Failed to add to startup: {}", e);
    }

    // Create tray icon
    let tray = match tray::TrayController::new(&hostname, config.support_code.as_deref(), is_support_session) {
        Ok(t) => {
            info!("Tray icon created");
            Some(t)
        }
        Err(e) => {
            warn!("Failed to create tray icon: {}", e);
            None
        }
    };

    // Create chat controller
    let chat_ctrl = chat::ChatController::new();

    // Connect to server and run main loop
    loop {
        info!("Connecting to server...");

        if is_support_session {
            if let Some(ref t) = tray {
                if t.exit_requested() {
                    info!("Exit requested by user");
                    cleanup_on_exit();
                    return Ok(());
                }
            }
        }

        match session.connect().await {
            Ok(_) => {
                info!("Connected to server");

                if let Some(ref t) = tray {
                    t.update_status("Status: Connected");
                }

                if let Err(e) = session.run_with_tray(tray.as_ref(), chat_ctrl.as_ref()).await {
                    let error_msg = e.to_string();

                    if error_msg.contains("USER_EXIT") {
                        info!("Session ended by user");
                        cleanup_on_exit();
                        return Ok(());
                    }

                    if error_msg.contains("SESSION_CANCELLED") {
                        info!("Session was cancelled by technician");
                        cleanup_on_exit();
                        show_message_box("Support Session Ended", "The support session was cancelled.");
                        return Ok(());
                    }

                    if error_msg.contains("ADMIN_DISCONNECT") {
                        info!("Session disconnected by administrator - uninstalling");
                        if let Err(e) = startup::uninstall() {
                            warn!("Uninstall failed: {}", e);
                        }
                        show_message_box("Remote Session Ended", "The session was ended by the administrator.");
                        return Ok(());
                    }

                    if error_msg.contains("ADMIN_UNINSTALL") {
                        info!("Uninstall command received from server - uninstalling");
                        if let Err(e) = startup::uninstall() {
                            warn!("Uninstall failed: {}", e);
                        }
                        show_message_box("GuruConnect Removed", "This computer has been removed from remote management.");
                        return Ok(());
                    }

                    if error_msg.contains("ADMIN_RESTART") {
                        info!("Restart command received - will reconnect");
                        // Don't exit, just let the loop continue to reconnect
                    } else {
                        error!("Session error: {}", e);
                    }
                }
            }
            Err(e) => {
                let error_msg = e.to_string();

                if error_msg.contains("cancelled") {
                    info!("Support code was cancelled");
                    cleanup_on_exit();
                    show_message_box("Support Session Cancelled", "This support session has been cancelled.");
                    return Ok(());
                }

                error!("Connection failed: {}", e);
            }
        }

        if is_support_session {
            info!("Support session ended, not reconnecting");
            cleanup_on_exit();
            return Ok(());
        }

        info!("Reconnecting in 5 seconds...");
        tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
    }
}

-                // No args: check if protocol handler is installed

-                // If not, run install mode (user likely downloaded from web)

+                // No args: check what mode to run

+                    // Protocol handler not registered - user likely downloaded from web

+                    // Run installer to set up protocol handler

-                    // Protocol handler exists, run as agent

+                } else if config::Config::has_agent_config() {

+                    // Protocol handler exists AND agent config exists

+                    // This is an agent installation - run as agent

+                    info!("Agent config found, running as agent");

+                    // Protocol handler exists but NO agent config

+                    // This is a viewer-only installation - just exit silently

+                    // The protocol handler will launch the viewer when needed

+                    info!("Viewer-only installation, exiting (use 'guruconnect agent' to run as agent)");


Now I need to update the installer to not create an agent config by default. Let me check the install.rs to see what it does currently.


Now I need to check the installer to make sure it doesn't create an agent config for viewer-only installs:


     1→//! Installation and protocol handler registration
     2→//!
     3→//! Handles:
     4→//! - Self-installation to Program Files (with UAC) or LocalAppData (fallback)
     5→//! - Protocol handler registration (guruconnect://)
     6→//! - UAC elevation with graceful fallback
     7→
     8→use anyhow::{anyhow, Result};
     9→use tracing::{info, warn, error};
    10→
    11→#[cfg(windows)]
    12→use windows::{
    13→    core::PCWSTR,
    14→    Win32::Foundation::HANDLE,
    15→    Win32::Security::{GetTokenInformation, TokenElevation, TOKEN_ELEVATION, TOKEN_QUERY},
    16→    Win32::System::Threading::{GetCurrentProcess, OpenProcessToken},
    17→    Win32::System::Registry::{
    18→        RegCreateKeyExW, RegSetValueExW, RegCloseKey, HKEY, HKEY_CLASSES_ROOT,
    19→        HKEY_CURRENT_USER, KEY_WRITE, REG_SZ, REG_OPTION_NON_VOLATILE,
    20→    },
    21→    Win32::UI::Shell::ShellExecuteW,
    22→    Win32::UI::WindowsAndMessaging::SW_SHOWNORMAL,
    23→};
    24→
    25→#[cfg(windows)]
    26→use std::ffi::OsStr;
    27→#[cfg(windows)]
    28→use std::os::windows::ffi::OsStrExt;
    29→
    30→/// Install locations
    31→pub const SYSTEM_INSTALL_PATH: &str = r"C:\Program Files\GuruConnect";
    32→pub const USER_INSTALL_PATH: &str = r"GuruConnect"; // Relative to %LOCALAPPDATA%
    33→
    34→/// Check if running with elevated privileges
    35→#[cfg(windows)]
    36→pub fn is_elevated() -> bool {
    37→    unsafe {
    38→        let mut token_handle = HANDLE::default();
    39→        if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token_handle).is_err() {
    40→            return false;
    41→        }
    42→
    43→        let mut elevation = TOKEN_ELEVATION::default();
    44→        let mut size = std::mem::size_of::<TOKEN_ELEVATION>() as u32;
    45→
    46→        let result = GetTokenInformation(
    47→            token_handle,
    48→            TokenElevation,
    49→            Some(&mut elevation as *mut _ as *mut _),
    50→            size,
    51→            &mut size,
    52→        );
    53→
    54→        let _ = windows::Win32::Foundation::CloseHandle(token_handle);
    55→
    56→        result.is_ok() && elevation.TokenIsElevated != 0
    57→    }
    58→}
    59→
    60→#[cfg(not(windows))]
    61→pub fn is_elevated() -> bool {
    62→    unsafe { libc::geteuid() == 0 }
    63→}
    64→
    65→/// Get the install path based on elevation status
    66→pub fn get_install_path(elevated: bool) -> std::path::PathBuf {
    67→    if elevated {
    68→        std::path::PathBuf::from(SYSTEM_INSTALL_PATH)
    69→    } else {
    70→        let local_app_data = std::env::var("LOCALAPPDATA")
    71→            .unwrap_or_else(|_| {
    72→                let home = std::env::var("USERPROFILE").unwrap_or_else(|_| ".".to_string());
    73→                format!(r"{}\AppData\Local", home)
    74→            });
    75→        std::path::PathBuf::from(local_app_data).join(USER_INSTALL_PATH)
    76→    }
    77→}
    78→
    79→/// Get the executable path
    80→pub fn get_exe_path(install_path: &std::path::Path) -> std::path::PathBuf {
    81→    install_path.join("guruconnect.exe")
    82→}
    83→
    84→/// Attempt to elevate and re-run with install command
    85→#[cfg(windows)]
    86→pub fn try_elevate_and_install() -> Result<bool> {
    87→    let exe_path = std::env::current_exe()?;
    88→    let exe_path_wide: Vec<u16> = OsStr::new(exe_path.as_os_str())
    89→        .encode_wide()
    90→        .chain(std::iter::once(0))
    91→        .collect();
    92→
    93→    let verb: Vec<u16> = OsStr::new("runas")
    94→        .encode_wide()
    95→        .chain(std::iter::once(0))
    96→        .collect();
    97→
    98→    let params: Vec<u16> = OsStr::new("install --elevated")
    99→        .encode_wide()
   100→        .chain(std::iter::once(0))

<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>

//! Installation and protocol handler registration
//!
//! Handles:
//! - Self-installation to Program Files (with UAC) or LocalAppData (fallback)
//! - Protocol handler registration (guruconnect://)
//! - UAC elevation with graceful fallback

use anyhow::{anyhow, Result};
use tracing::{info, warn, error};

#[cfg(windows)]
use windows::{
    core::PCWSTR,
    Win32::Foundation::HANDLE,
    Win32::Security::{GetTokenInformation, TokenElevation, TOKEN_ELEVATION, TOKEN_QUERY},
    Win32::System::Threading::{GetCurrentProcess, OpenProcessToken},
    Win32::System::Registry::{
        RegCreateKeyExW, RegSetValueExW, RegCloseKey, HKEY, HKEY_CLASSES_ROOT,
        HKEY_CURRENT_USER, KEY_WRITE, REG_SZ, REG_OPTION_NON_VOLATILE,
    },
    Win32::UI::Shell::ShellExecuteW,
    Win32::UI::WindowsAndMessaging::SW_SHOWNORMAL,
};

#[cfg(windows)]
use std::ffi::OsStr;
#[cfg(windows)]
use std::os::windows::ffi::OsStrExt;

/// Install locations
pub const SYSTEM_INSTALL_PATH: &str = r"C:\Program Files\GuruConnect";
pub const USER_INSTALL_PATH: &str = r"GuruConnect"; // Relative to %LOCALAPPDATA%

/// Check if running with elevated privileges
#[cfg(windows)]
pub fn is_elevated() -> bool {
    unsafe {
        let mut token_handle = HANDLE::default();
        if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token_handle).is_err() {
            return false;
        }

        let mut elevation = TOKEN_ELEVATION::default();
        let mut size = std::mem::size_of::<TOKEN_ELEVATION>() as u32;

        let result = GetTokenInformation(
            token_handle,
            TokenElevation,
            Some(&mut elevation as *mut _ as *mut _),
            size,
            &mut size,
        );

        let _ = windows::Win32::Foundation::CloseHandle(token_handle);

        result.is_ok() && elevation.TokenIsElevated != 0
    }
}

#[cfg(not(windows))]
pub fn is_elevated() -> bool {
    unsafe { libc::geteuid() == 0 }
}

/// Get the install path based on elevation status
pub fn get_install_path(elevated: bool) -> std::path::PathBuf {
    if elevated {
        std::path::PathBuf::from(SYSTEM_INSTALL_PATH)
    } else {
        let local_app_data = std::env::var("LOCALAPPDATA")
            .unwrap_or_else(|_| {
                let home = std::env::var("USERPROFILE").unwrap_or_else(|_| ".".to_string());
                format!(r"{}\AppData\Local", home)
            });
        std::path::PathBuf::from(local_app_data).join(USER_INSTALL_PATH)
    }
}

/// Get the executable path
pub fn get_exe_path(install_path: &std::path::Path) -> std::path::PathBuf {
    install_path.join("guruconnect.exe")
}

/// Attempt to elevate and re-run with install command
#[cfg(windows)]
pub fn try_elevate_and_install() -> Result<bool> {
    let exe_path = std::env::current_exe()?;
    let exe_path_wide: Vec<u16> = OsStr::new(exe_path.as_os_str())
        .encode_wide()
        .chain(std::iter::once(0))
        .collect();

    let verb: Vec<u16> = OsStr::new("runas")
        .encode_wide()
        .chain(std::iter::once(0))
        .collect();

    let params: Vec<u16> = OsStr::new("install --elevated")
        .encode_wide()
        .chain(std::iter::once(0))


Let me continue reading the install.rs to see the main install() function:


254:pub fn install(force_user_install: bool) -> Result<()> {


Let me read the install() function to see what it does:


   254→pub fn install(force_user_install: bool) -> Result<()> {
   255→    let elevated = is_elevated();
   256→
   257→    // If not elevated and not forcing user install, try to elevate
   258→    if !elevated && !force_user_install {
   259→        info!("Attempting UAC elevation for system-wide install...");
   260→        match try_elevate_and_install() {
   261→            Ok(true) => {
   262→                // Elevation was requested, exit this instance
   263→                // The elevated instance will continue the install
   264→                info!("Elevated process started, exiting current instance");
   265→                std::process::exit(0);
   266→            }
   267→            Ok(false) => {
   268→                info!("UAC denied, falling back to user install");
   269→            }
   270→            Err(e) => {
   271→                warn!("Elevation failed: {}, falling back to user install", e);
   272→            }
   273→        }
   274→    }
   275→
   276→    let install_path = get_install_path(elevated);
   277→    let exe_path = get_exe_path(&install_path);
   278→
   279→    info!("Installing to: {}", install_path.display());
   280→
   281→    // Create install directory
   282→    std::fs::create_dir_all(&install_path)?;
   283→
   284→    // Copy ourselves to install location
   285→    let current_exe = std::env::current_exe()?;
   286→    if current_exe != exe_path {
   287→        std::fs::copy(&current_exe, &exe_path)?;
   288→        info!("Copied executable to: {}", exe_path.display());
   289→    }
   290→
   291→    // Register protocol handler
   292→    register_protocol_handler(elevated)?;
   293→
   294→    info!("Installation complete!");
   295→    if elevated {
   296→        info!("Installed system-wide to: {}", install_path.display());
   297→    } else {
   298→        info!("Installed for current user to: {}", install_path.display());
   299→    }
   300→
   301→    Ok(())
   302→}
   303→
   304→/// Check if the guruconnect:// protocol handler is registered
   305→#[cfg(windows)]
   306→pub fn is_protocol_handler_registered() -> bool {
   307→    use windows::Win32::System::Registry::{
   308→        RegOpenKeyExW, RegCloseKey, HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, KEY_READ,
   309→    };
   310→
   311→    unsafe {
   312→        // Check system-wide registration (HKCR\guruconnect)
   313→        let mut key = HKEY::default();
   314→        let key_path = to_wide("guruconnect");
   315→        if RegOpenKeyExW(
   316→            HKEY_CLASSES_ROOT,
   317→            PCWSTR(key_path.as_ptr()),
   318→            0,
   319→            KEY_READ,
   320→            &mut key,
   321→        ).is_ok() {
   322→            let _ = RegCloseKey(key);
   323→            return true;
   324→        }
   325→
   326→        // Check user-level registration (HKCU\Software\Classes\guruconnect)
   327→        let key_path = to_wide(r"Software\Classes\guruconnect");
   328→        if RegOpenKeyExW(
   329→            HKEY_CURRENT_USER,
   330→            PCWSTR(key_path.as_ptr()),
   331→            0,
   332→            KEY_READ,
   333→            &mut key,

<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>

pub fn install(force_user_install: bool) -> Result<()> {
    let elevated = is_elevated();

    // If not elevated and not forcing user install, try to elevate
    if !elevated && !force_user_install {
        info!("Attempting UAC elevation for system-wide install...");
        match try_elevate_and_install() {
            Ok(true) => {
                // Elevation was requested, exit this instance
                // The elevated instance will continue the install
                info!("Elevated process started, exiting current instance");
                std::process::exit(0);
            }
            Ok(false) => {
                info!("UAC denied, falling back to user install");
            }
            Err(e) => {
                warn!("Elevation failed: {}, falling back to user install", e);
            }
        }
    }

    let install_path = get_install_path(elevated);
    let exe_path = get_exe_path(&install_path);

    info!("Installing to: {}", install_path.display());

    // Create install directory
    std::fs::create_dir_all(&install_path)?;

    // Copy ourselves to install location
    let current_exe = std::env::current_exe()?;
    if current_exe != exe_path {
        std::fs::copy(&current_exe, &exe_path)?;
        info!("Copied executable to: {}", exe_path.display());
    }

    // Register protocol handler
    register_protocol_handler(elevated)?;

    info!("Installation complete!");
    if elevated {
        info!("Installed system-wide to: {}", install_path.display());
    } else {
        info!("Installed for current user to: {}", install_path.display());
    }

    Ok(())
}

/// Check if the guruconnect:// protocol handler is registered
#[cfg(windows)]
pub fn is_protocol_handler_registered() -> bool {
    use windows::Win32::System::Registry::{
        RegOpenKeyExW, RegCloseKey, HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, KEY_READ,
    };

    unsafe {
        // Check system-wide registration (HKCR\guruconnect)
        let mut key = HKEY::default();
        let key_path = to_wide("guruconnect");
        if RegOpenKeyExW(
            HKEY_CLASSES_ROOT,
            PCWSTR(key_path.as_ptr()),
            0,
            KEY_READ,
            &mut key,
        ).is_ok() {
            let _ = RegCloseKey(key);
            return true;
        }

        // Check user-level registration (HKCU\Software\Classes\guruconnect)
        let key_path = to_wide(r"Software\Classes\guruconnect");
        if RegOpenKeyExW(
            HKEY_CURRENT_USER,
            PCWSTR(key_path.as_ptr()),
            0,
            KEY_READ,
            &mut key,


Good - the install() function doesn't create an agent config file. It only:
1. Copies the executable to the install path
2. Registers the protocol handler

So the logic change in main.rs should be enough - if no agent config file exists, it won't run as agent.

Now I need to make sure the agent mode creates a config file when it runs. Let me check the run_agent_mode function:


Good - the installer doesn't create an agent config, it only registers the protocol handler. Now let me verify that running as agent creates the config properly (which it should via `Config::load()` which calls `save()`).

Let me also update the todo list and build the agent:


TodoWrite

Investigate console window appearing (may be old binary or debug build)

Fix default behavior: viewer-only install shouldn't run as agent

Rebuild and deploy fixed agent


Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable

Investigate console window appearing (may be old binary or debug build)

Fix default behavior: viewer-only install shouldn't run as agent

Add install mode selection (Agent vs Viewer-only)

Rebuild and deploy fixed agent

Rebuild and deploy fixed agent


Now let me build the agent to make sure everything compiles:


Bash

cd /c/Users/MikeSwanson/Claude/guru-connect/agent && cargo build --release 2>&1 | tail -30

Build agent with viewer-only fix


   |
help: use `let _ = ...` to ignore the resulting value
   |
74 |                 let _ = DeleteDC(mem_dc);
   |                 +++++++

warning: unused `BOOL` that must be used
   --> agent\src\capture\gdi.rs:114:13
    |
114 |             DeleteObject(bitmap);
    |             ^^^^^^^^^^^^^^^^^^^^
    |
help: use `let _ = ...` to ignore the resulting value
    |
114 |             let _ = DeleteObject(bitmap);
    |             +++++++

warning: unused `BOOL` that must be used
   --> agent\src\capture\gdi.rs:115:13
    |
115 |             DeleteDC(mem_dc);
    |             ^^^^^^^^^^^^^^^^
    |
help: use `let _ = ...` to ignore the resulting value
    |
115 |             let _ = DeleteDC(mem_dc);
    |             +++++++

warning: `guruconnect` (bin "guruconnect") generated 61 warnings (run `cargo fix --bin "guruconnect" -p guruconnect` to apply 20 suggestions)
    Finished `release` profile [optimized] target(s) in 14.18s


The build succeeded with just warnings. Now let me commit these changes and then deploy the new agent.


Build succeeded. Let me commit and deploy:


cd /c/Users/MikeSwanson/Claude/guru-connect && git add -A && git diff --cached --stat

Stage changes


 agent/src/config.rs | 25 +++++++++++++++++++++++++
 agent/src/main.rs   | 17 +++++++++++++----
 2 files changed, 38 insertions(+), 4 deletions(-)


Good, the changes look reasonable. Let me commit them.


cd /c/Users/MikeSwanson/Claude/guru-connect && git commit -m "$(cat <<'EOF'
Fix viewer-only installs registering as agents

Previously, any installation with the protocol handler registered
would default to running as an agent. This caused admin/technician
machines (viewer-only) to appear in the sessions list.

Changes:
- Add Config::has_agent_config() to check for explicit agent config
- Only run as agent when: explicit 'agent' command, support code
  provided, OR agent config file exists
- Viewer-only installs (protocol handler but no config) now exit
  silently when launched without arguments

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
EOF
)"

Commit viewer-only fix


[main 0387295] Fix viewer-only installs registering as agents
 2 files changed, 38 insertions(+), 4 deletions(-)


Good, committed. Now let me push and deploy to the server.


cd /c/Users/MikeSwanson/Claude/guru-connect && git push origin main 2>&1

Push changes


remote: . Processing 1 references
remote: Processed 1 references in total
To https://git.azcomputerguru.com/azcomputerguru/guru-connect.git
   4e5328f..0387295  main -> main


Good, pushed. Now let me copy the new agent binary to the server:


scp -o ConnectTimeout=30 /c/Users/MikeSwanson/Claude/guru-connect/target/x86_64-pc-windows-msvc/release/guruconnect.exe guru@172.16.3.30:~/guru-connect/server/static/downloads/guruconnect.exe 2>&1 && echo "Upload complete"

Upload new agent to server


Upload complete


Let me verify the upload and get the checksum:


ssh -o ConnectTimeout=30 guru@172.16.3.30 "ls -la ~/guru-connect/server/static/downloads/guruconnect.exe && sha256sum ~/guru-connect/server/static/downloads/guruconnect.exe"

Verify new agent binary


-rwxr-xr-x 1 guru guru 7361536 Dec 30 17:10 /home/guru/guru-connect/server/static/downloads/guruconnect.exe
cac957d6d3e682cb51b2bfd67ffdaf69704fa3be40990009fd35e380986d76ae  /home/guru/guru-connect/server/static/downloads/guruconnect.exe


Good, the new binary is deployed. Let me update the todo list.


TodoWrite

Investigate console window appearing (may be old binary or debug build)

Fix default behavior: viewer-only install shouldn't run as agent

Rebuild and deploy fixed agent


Rebuild and deploy fixed agent

Rebuild and deploy fixed agent


Both issues have been addressed:1. **Console window appearing**: This was likely due to either a debug build or an old binary. The current release build has `windows_subsystem = "windows"` which hides the console. The new binary should fix this.2. **Viewer machine appearing in sessions**: Fixed by checking for an explicit agent config file. Viewer-only installs won't have a config file and will exit silently.Let me summarize what was done for the user.


Done! Here's the summary:

## Changes Made

### Issue: Viewer machine appearing in sessions list
**Fixed** by changing default behavior in `main.rs`:
- Now checks for explicit agent config file (`guruconnect.toml` or `%ProgramData%\GuruConnect\agent.toml`)
- **Viewer-only install** (protocol handler only): exits silently when run without args
- **Agent install** (has config file): runs as agent
- **With support code**: always runs as agent

### Issue: Console window appearing
This should be fixed by the new binary. The `#![windows_subsystem = "windows"]` attribute hides the console in release builds. If the old laptop had a debug build or old version, downloading the new binary will fix it.

## New Binary Deployed
- **Path**: `https://connect.azcomputerguru.com/downloads/guruconnect.exe`
- **SHA256**: `cac957d6d3e682cb51b2bfd67ffdaf69704fa3be40990009fd35e380986d76ae`

## Behavior Summary

| Scenario | Result |
|----------|--------|
| Download and run | Installs protocol handler, exits |
| Run after install (viewer-only) | Exits silently |
| `guruconnect agent` | Runs as agent, creates config |
| `guruconnect 123456` | Runs as support session agent |
| Click `guruconnect://` link | Opens viewer |
| Run after agent config created | Runs as agent |

To convert a viewer-only install to an agent install, run: `guruconnect agent`


Let's change things up a bit.  One rule for this build is end users never need to use flags/switches/arguments.  So what if we have the server provide different filesnames for different functins. At least for viewer or temp agents.  We still ahve to consider permanent agents which will have a lot more data associated with them.  How about a magic-bye for them to encode all the required data (company/site/tag/etc)

Rebuild and deploy fixed agent


The user wants to simplify the user experience so end users never need to use command-line flags. They're proposing:

1. **Different filenames for different functions** - like ScreenConnect does with dynamically named executables
2. **Magic bytes for permanent agents** - embed configuration data directly in the binary

This is a great UX improvement. Let me think about how this would work:

## Current Modes:
1. **Viewer** - Admin/tech connects to view remote sessions (launched via `guruconnect://` protocol)
2. **Temp Agent** - One-time support session with a 6-digit code (e.g., `GuruConnect-123456.exe`)
3. **Permanent Agent** - Managed endpoint that persists (needs company/site/tags embedded)

## Filename-based Configuration:

For **Temp Agents** (support sessions):
- Filename: `GuruConnect-123456.exe` where 123456 is the support code
- Already partially supported (see `extract_code_from_filename()` in main.rs)

For **Viewers**:
- Filename: `GuruConnect-Viewer.exe` or just download and run with no code
- Could be: `GuruConnectViewer.exe` - detects "Viewer" in name

For **Permanent Agents**:
- Need to embed: server URL, API key, company name, site name, tags, etc.
- Options:
  1. **Magic bytes at end of PE** - Append JSON/binary config after PE ends
  2. **PE resource section** - Embed as Windows resource
  3. **Overlay data** - Common technique, appended after PE signature
  4. **Filename with encoded data** - Limited length, but could work for basic info

## Magic Bytes Approach for Permanent Agents:

The cleanest approach is to append configuration data to the end of the executable:

```
[Original EXE bytes]

[Magic marker: "GCFG"]
[Config length: 4 bytes]
[Config data: JSON or TOML]
```

I'll explore a streamlined configuration embedding method that minimizes user friction. The approach involves creating a flexible mechanism for injecting configuration directly into the executable, eliminating external configuration files. By using a magic marker and embedded config, we can dynamically configure agents without complex setup processes.

The server-side generation process becomes straightforward: start with a base binary, append configuration data, and generate a uniquely named executable. This method provides a clean, self-contained distribution mechanism that simplifies deployment and reduces potential configuration errors.

Key implementation steps include validating magic marker detection, creating a flexible server endpoint for generating configured binaries, and ensuring seamless filename and configuration detection. The goal is to remove all manual configuration steps for end users, creating a frictionless onboarding experience.


Great idea! This is exactly how ScreenConnect works. Let me propose a design:

## Filename-Based Detection

| Filename Pattern | Behavior |
|-----------------|----------|
| `GuruConnect-123456.exe` | Temp support agent with code 123456 |
| `GuruConnect-Viewer.exe` | Viewer-only (protocol handler install) |
| `GuruConnect-Setup.exe` or just `GuruConnect.exe` | Permanent agent (reads embedded config) |

## Magic Bytes for Permanent Agents

Append configuration to the end of the executable:

```
┌─────────────────────────────────┐
│     Original PE Binary          │
│     (guruconnect.exe)           │
├─────────────────────────────────┤
│  Magic Marker: "GURUCONFIG"     │  (10 bytes)
│  Config Length: uint32          │  (4 bytes)
│  Config Data (JSON):            │
│  {                              │
│    "server": "wss://...",       │
│    "api_key": "...",            │
│    "company": "Acme Corp",      │
│    "site": "Main Office",       │
│    "tags": ["windows", "pc"]    │
│  }                              │
└─────────────────────────────────┘
```

## Server-Side Generation

```
GET /api/download/agent?company=AcmeCorp&site=MainOffice&tags=windows,workstation
```

Server would:
1. Read base `guruconnect.exe`
2. Append magic marker + config JSON
3. Return with filename: `GuruConnect-AcmeCorp-Setup.exe`

## User Experience

| User Action | What Happens |
|-------------|--------------|
| Tech sends support link | User downloads `GuruConnect-847291.exe`, runs it, session starts |
| Tech sends permanent agent link | User downloads `GuruConnect-ClientName-Setup.exe`, runs it, installs permanently |
| Tech installs viewer | Downloads `GuruConnect-Viewer.exe`, runs it, protocol handler registered |

No flags, no arguments, just double-click.

Want me to implement this? I'd start with:
1. Magic bytes reader in the agent
2. Filename detection logic
3. Server endpoint for generating configured binaries


yes, implement it

Rebuild and deploy fixed agent


The user wants me to implement the magic bytes system for permanent agents and filename-based detection. Let me plan this out:

## Implementation Plan

### Phase 1: Agent-side - Filename Detection
1. Update `main.rs` to detect mode from filename:
   - `*-Viewer*` → Viewer-only install
   - `*-XXXXXX*` (6 digits) → Support session
   - Otherwise → Check for embedded config or run as permanent agent

### Phase 2: Agent-side - Magic Bytes Reader
1. Create function to read embedded config from self-executable
2. Look for magic marker "GURUCONFIG" at end of file
3. Parse config JSON if found
4. Use embedded config instead of external file

### Phase 3: Server-side - Binary Generator
1. Create endpoint `/api/download/agent` that accepts parameters
2. Read base binary
3. Append magic marker + config
4. Return with appropriate filename

Let me start implementing.


Add magic bytes reader to agent config module

Update main.rs with filename-based mode detection

Create server endpoint for generating configured binaries

Update dashboard with agent download/generation UI

Test end-to-end: viewer, temp agent, permanent agent


Rebuild and deploy fixed agent

Add magic bytes reader to agent config module

Update main.rs with filename-based mode detection

Create server endpoint for generating configured binaries

Update dashboard with agent download/generation UI

Test end-to-end: viewer, temp agent, permanent agent


Let me start with the agent-side changes. First, I'll add the magic bytes reader to the config module.


Let me start with the agent-side implementation. First, I'll add the magic bytes reader to config.rs:


     1→//! Agent configuration management
     2→
     3→use anyhow::{Context, Result};
     4→use serde::{Deserialize, Serialize};
     5→use std::path::PathBuf;
     6→use uuid::Uuid;
     7→
     8→/// Agent configuration
     9→#[derive(Debug, Clone, Serialize, Deserialize)]
    10→pub struct Config {
    11→    /// Server WebSocket URL (e.g., wss://connect.example.com/ws)
    12→    pub server_url: String,
    13→
    14→    /// Agent API key for authentication
    15→    pub api_key: String,
    16→
    17→    /// Unique agent identifier (generated on first run)
    18→    #[serde(default = "generate_agent_id")]
    19→    pub agent_id: String,
    20→
    21→    /// Optional hostname override
    22→    pub hostname_override: Option<String>,
    23→
    24→    /// Support code for one-time support sessions (set via command line)
    25→    #[serde(skip)]
    26→    pub support_code: Option<String>,
    27→
    28→    /// Capture settings
    29→    #[serde(default)]
    30→    pub capture: CaptureConfig,
    31→
    32→    /// Encoding settings
    33→    #[serde(default)]
    34→    pub encoding: EncodingConfig,
    35→}
    36→
    37→fn generate_agent_id() -> String {
    38→    Uuid::new_v4().to_string()
    39→}
    40→
    41→#[derive(Debug, Clone, Serialize, Deserialize)]
    42→pub struct CaptureConfig {
    43→    /// Target frames per second (1-60)
    44→    #[serde(default = "default_fps")]
    45→    pub fps: u32,
    46→
    47→    /// Use DXGI Desktop Duplication (recommended)
    48→    #[serde(default = "default_true")]
    49→    pub use_dxgi: bool,
    50→
    51→    /// Fall back to GDI if DXGI fails
    52→    #[serde(default = "default_true")]
    53→    pub gdi_fallback: bool,
    54→}
    55→
    56→#[derive(Debug, Clone, Serialize, Deserialize)]
    57→pub struct EncodingConfig {
    58→    /// Preferred codec (auto, raw, vp9, h264)
    59→    #[serde(default = "default_codec")]
    60→    pub codec: String,
    61→
    62→    /// Quality (1-100, higher = better quality, more bandwidth)
    63→    #[serde(default = "default_quality")]
    64→    pub quality: u32,
    65→
    66→    /// Use hardware encoding if available
    67→    #[serde(default = "default_true")]
    68→    pub hardware_encoding: bool,
    69→}
    70→
    71→fn default_fps() -> u32 {
    72→    30
    73→}
    74→
    75→fn default_true() -> bool {
    76→    true
    77→}
    78→
    79→fn default_codec() -> String {
    80→    "auto".to_string()
    81→}
    82→
    83→fn default_quality() -> u32 {
    84→    75
    85→}
    86→
    87→impl Default for CaptureConfig {
    88→    fn default() -> Self {
    89→        Self {
    90→            fps: default_fps(),
    91→            use_dxgi: true,
    92→            gdi_fallback: true,
    93→        }
    94→    }
    95→}
    96→
    97→impl Default for EncodingConfig {
    98→    fn default() -> Self {
    99→        Self {
   100→            codec: default_codec(),
   101→            quality: default_quality(),
   102→            hardware_encoding: true,
   103→        }
   104→    }
   105→}
   106→
   107→impl Config {
   108→    /// Check if an explicit agent configuration file exists
   109→    /// This returns true only if there's a real config file, not generated defaults
   110→    pub fn has_agent_config() -> bool {
   111→        // Check for config in current directory
   112→        let local_config = PathBuf::from("guruconnect.toml");
   113→        if local_config.exists() {
   114→            return true;
   115→        }
   116→
   117→        // Check in program data directory (Windows)
   118→        #[cfg(windows)]
   119→        {
   120→            if let Ok(program_data) = std::env::var("ProgramData") {
   121→                let path = PathBuf::from(program_data)
   122→                    .join("GuruConnect")
   123→                    .join("agent.toml");
   124→                if path.exists() {
   125→                    return true;
   126→                }
   127→            }
   128→        }
   129→
   130→        false
   131→    }
   132→
   133→    /// Load configuration from file or environment
   134→    pub fn load() -> Result<Self> {
   135→        // Try loading from config file
   136→        let config_path = Self::config_path();
   137→
   138→        if config_path.exists() {
   139→            let contents = std::fs::read_to_string(&config_path)
   140→                .with_context(|| format!("Failed to read config from {:?}", config_path))?;
   141→
   142→            let mut config: Config = toml::from_str(&contents)
   143→                .with_context(|| "Failed to parse config file")?;
   144→
   145→            // Ensure agent_id is set and saved
   146→            if config.agent_id.is_empty() {
   147→                config.agent_id = generate_agent_id();
   148→                let _ = config.save();
   149→            }
   150→
   151→            // support_code is always None when loading from file (set via CLI)
   152→            config.support_code = None;
   153→
   154→            return Ok(config);
   155→        }
   156→
   157→        // Fall back to environment variables
   158→        let server_url = std::env::var("GURUCONNECT_SERVER_URL")
   159→            .unwrap_or_else(|_| "wss://connect.azcomputerguru.com/ws/agent".to_string());
   160→
   161→        let api_key = std::env::var("GURUCONNECT_API_KEY")
   162→            .unwrap_or_else(|_| "dev-key".to_string());
   163→
   164→        let agent_id = std::env::var("GURUCONNECT_AGENT_ID")
   165→            .unwrap_or_else(|_| generate_agent_id());
   166→
   167→        let config = Config {
   168→            server_url,
   169→            api_key,
   170→            agent_id,
   171→            hostname_override: std::env::var("GURUCONNECT_HOSTNAME").ok(),
   172→            support_code: None, // Set via CLI
   173→            capture: CaptureConfig::default(),
   174→            encoding: EncodingConfig::default(),
   175→        };
   176→
   177→        // Save config with generated agent_id for persistence
   178→        let _ = config.save();
   179→
   180→        Ok(config)
   181→    }
   182→
   183→    /// Get the configuration file path
   184→    fn config_path() -> PathBuf {
   185→        // Check for config in current directory first
   186→        let local_config = PathBuf::from("guruconnect.toml");
   187→        if local_config.exists() {
   188→            return local_config;
   189→        }
   190→
   191→        // Check in program data directory (Windows)
   192→        #[cfg(windows)]
   193→        {
   194→            if let Ok(program_data) = std::env::var("ProgramData") {
   195→                let path = PathBuf::from(program_data)
   196→                    .join("GuruConnect")
   197→                    .join("agent.toml");
   198→                if path.exists() {
   199→                    return path;
   200→                }
   201→            }
   202→        }
   203→
   204→        // Default to local config
   205→        local_config
   206→    }
   207→
   208→    /// Get the hostname to use
   209→    pub fn hostname(&self) -> String {
   210→        self.hostname_override
   211→            .clone()
   212→            .unwrap_or_else(|| {
   213→                hostname::get()
   214→                    .map(|h| h.to_string_lossy().to_string())
   215→                    .unwrap_or_else(|_| "unknown".to_string())
   216→            })
   217→    }
   218→
   219→    /// Save current configuration to file
   220→    pub fn save(&self) -> Result<()> {
   221→        let config_path = Self::config_path();
   222→
   223→        // Ensure parent directory exists
   224→        if let Some(parent) = config_path.parent() {
   225→            std::fs::create_dir_all(parent)?;
   226→        }
   227→
   228→        let contents = toml::to_string_pretty(self)?;
   229→        std::fs::write(&config_path, contents)?;
   230→
   231→        Ok(())
   232→    }
   233→}
   234→
   235→/// Example configuration file content
   236→pub fn example_config() -> &'static str {
   237→    r#"# GuruConnect Agent Configuration
   238→
   239→# Server connection
   240→server_url = "wss://connect.example.com/ws"
   241→api_key = "your-agent-api-key"
   242→agent_id = "auto-generated-uuid"
   243→
   244→# Optional: override hostname
   245→# hostname_override = "custom-hostname"
   246→
   247→[capture]
   248→fps = 30
   249→use_dxgi = true
   250→gdi_fallback = true
   251→
   252→[encoding]
   253→codec = "auto"      # auto, raw, vp9, h264
   254→quality = 75        # 1-100
   255→hardware_encoding = true
   256→"#
   257→}
   258→

<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>

//! Agent configuration management

use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use uuid::Uuid;

/// Agent configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
    /// Server WebSocket URL (e.g., wss://connect.example.com/ws)
    pub server_url: String,

    /// Agent API key for authentication
    pub api_key: String,

    /// Unique agent identifier (generated on first run)
    #[serde(default = "generate_agent_id")]
    pub agent_id: String,

    /// Optional hostname override
    pub hostname_override: Option<String>,

    /// Support code for one-time support sessions (set via command line)
    #[serde(skip)]
    pub support_code: Option<String>,

    /// Capture settings
    #[serde(default)]
    pub capture: CaptureConfig,

    /// Encoding settings
    #[serde(default)]
    pub encoding: EncodingConfig,
}

fn generate_agent_id() -> String {
    Uuid::new_v4().to_string()
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CaptureConfig {
    /// Target frames per second (1-60)
    #[serde(default = "default_fps")]
    pub fps: u32,

    /// Use DXGI Desktop Duplication (recommended)
    #[serde(default = "default_true")]
    pub use_dxgi: bool,

    /// Fall back to GDI if DXGI fails
    #[serde(default = "default_true")]
    pub gdi_fallback: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncodingConfig {
    /// Preferred codec (auto, raw, vp9, h264)
    #[serde(default = "default_codec")]
    pub codec: String,

    /// Quality (1-100, higher = better quality, more bandwidth)
    #[serde(default = "default_quality")]
    pub quality: u32,

    /// Use hardware encoding if available
    #[serde(default = "default_true")]
    pub hardware_encoding: bool,
}

fn default_fps() -> u32 {
    30
}

fn default_true() -> bool {
    true
}

fn default_codec() -> String {
    "auto".to_string()
}

fn default_quality() -> u32 {
    75
}

impl Default for CaptureConfig {
    fn default() -> Self {
        Self {
            fps: default_fps(),
            use_dxgi: true,
            gdi_fallback: true,
        }
    }
}

impl Default for EncodingConfig {
    fn default() -> Self {
        Self {
            codec: default_codec(),
            quality: default_quality(),
            hardware_encoding: true,
        }
    }
}

impl Config {
    /// Check if an explicit agent configuration file exists
    /// This returns true only if there's a real config file, not generated defaults
    pub fn has_agent_config() -> bool {
        // Check for config in current directory
        let local_config = PathBuf::from("guruconnect.toml");
        if local_config.exists() {
            return true;
        }

        // Check in program data directory (Windows)
        #[cfg(windows)]
        {
            if let Ok(program_data) = std::env::var("ProgramData") {
                let path = PathBuf::from(program_data)
                    .join("GuruConnect")
                    .join("agent.toml");
                if path.exists() {
                    return true;
                }
            }
        }

        false
    }

    /// Load configuration from file or environment
    pub fn load() -> Result<Self> {
        // Try loading from config file
        let config_path = Self::config_path();

        if config_path.exists() {
            let contents = std::fs::read_to_string(&config_path)
                .with_context(|| format!("Failed to read config from {:?}", config_path))?;

            let mut config: Config = toml::from_str(&contents)
                .with_context(|| "Failed to parse config file")?;

            // Ensure agent_id is set and saved
            if config.agent_id.is_empty() {
                config.agent_id = generate_agent_id();
                let _ = config.save();
            }

            // support_code is always None when loading from file (set via CLI)
            config.support_code = None;

            return Ok(config);
        }

        // Fall back to environment variables
        let server_url = std::env::var("GURUCONNECT_SERVER_URL")
            .unwrap_or_else(|_| "wss://connect.azcomputerguru.com/ws/agent".to_string());

        let api_key = std::env::var("GURUCONNECT_API_KEY")
            .unwrap_or_else(|_| "dev-key".to_string());

        let agent_id = std::env::var("GURUCONNECT_AGENT_ID")
            .unwrap_or_else(|_| generate_agent_id());

        let config = Config {
            server_url,
            api_key,
            agent_id,
            hostname_override: std::env::var("GURUCONNECT_HOSTNAME").ok(),
            support_code: None, // Set via CLI
            capture: CaptureConfig::default(),
            encoding: EncodingConfig::default(),
        };

        // Save config with generated agent_id for persistence
        let _ = config.save();

        Ok(config)
    }

    /// Get the configuration file path
    fn config_path() -> PathBuf {
        // Check for config in current directory first
        let local_config = PathBuf::from("guruconnect.toml");
        if local_config.exists() {
            return local_config;
        }

        // Check in program data directory (Windows)
        #[cfg(windows)]
        {
            if let Ok(program_data) = std::env::var("ProgramData") {
                let path = PathBuf::from(program_data)
                    .join("GuruConnect")
                    .join("agent.toml");
                if path.exists() {
                    return path;
                }
            }
        }

        // Default to local config
        local_config
    }

    /// Get the hostname to use
    pub fn hostname(&self) -> String {
        self.hostname_override
            .clone()
            .unwrap_or_else(|| {
                hostname::get()
                    .map(|h| h.to_string_lossy().to_string())
                    .unwrap_or_else(|_| "unknown".to_string())
            })
    }

    /// Save current configuration to file
    pub fn save(&self) -> Result<()> {
        let config_path = Self::config_path();

        // Ensure parent directory exists
        if let Some(parent) = config_path.parent() {
            std::fs::create_dir_all(parent)?;
        }

        let contents = toml::to_string_pretty(self)?;
        std::fs::write(&config_path, contents)?;

        Ok(())
    }
}

/// Example configuration file content
pub fn example_config() -> &'static str {
    r#"# GuruConnect Agent Configuration

# Server connection
server_url = "wss://connect.example.com/ws"
api_key = "your-agent-api-key"
agent_id = "auto-generated-uuid"

# Optional: override hostname
# hostname_override = "custom-hostname"

[capture]
fps = 30
use_dxgi = true
gdi_fallback = true

[encoding]
codec = "auto"      # auto, raw, vp9, h264
quality = 75        # 1-100
hardware_encoding = true
"#
}


Now let me add the embedded config structures and the magic bytes reader. I'll add:

1. `EmbeddedConfig` struct for the embedded JSON data
2. `MAGIC_MARKER` constant
3. `read_embedded_config()` function
4. `has_embedded_config()` function
5. Update `load()` to check embedded config first

I'll also add filename detection utilities.


Now let me add the embedded config support and filename detection:


Edit

//! Agent configuration management

use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use uuid::Uuid;

//! Agent configuration management
//!
//! Supports three configuration sources (in priority order):
//! 1. Embedded config (magic bytes appended to executable)
//! 2. Config file (guruconnect.toml or %ProgramData%\GuruConnect\agent.toml)
//! 3. Environment variables (fallback)

use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};
use std::io::{Read, Seek, SeekFrom};
use std::path::PathBuf;
use tracing::{info, warn};
use uuid::Uuid;

/// Magic marker for embedded configuration (10 bytes)
const MAGIC_MARKER: &[u8] = b"GURUCONFIG";

/// Embedded configuration data (appended to executable)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmbeddedConfig {
    /// Server WebSocket URL
    pub server_url: String,
    /// API key for authentication
    pub api_key: String,
    /// Company/organization name
    #[serde(default)]
    pub company: Option<String>,
    /// Site/location name
    #[serde(default)]
    pub site: Option<String>,
    /// Tags for categorization
    #[serde(default)]
    pub tags: Vec<String>,
}

/// Detected run mode based on filename
#[derive(Debug, Clone, PartialEq)]
pub enum RunMode {
    /// Viewer-only installation (filename contains "Viewer")
    Viewer,
    /// Temporary support session (filename contains 6-digit code)
    TempSupport(String),
    /// Permanent agent with embedded config
    PermanentAgent,
    /// Unknown/default mode
    Default,
}


The file C:\Users\MikeSwanson\Claude\guru-connect\agent\src\config.rs has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
     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 {

//! Agent configuration management
//!
//! Supports three configuration sources (in priority order):
//! 1. Embedded config (magic bytes appended to executable)
//! 2. Config file (guruconnect.toml or %ProgramData%\GuruConnect\agent.toml)
//! 3. Environment variables (fallback)

use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};
use std::io::{Read, Seek, SeekFrom};
use std::path::PathBuf;
use tracing::{info, warn};
use uuid::Uuid;

/// Magic marker for embedded configuration (10 bytes)
const MAGIC_MARKER: &[u8] = b"GURUCONFIG";

/// Embedded configuration data (appended to executable)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmbeddedConfig {
    /// Server WebSocket URL
    pub server_url: String,
    /// API key for authentication
    pub api_key: String,
    /// Company/organization name
    #[serde(default)]
    pub company: Option<String>,
    /// Site/location name
    #[serde(default)]
    pub site: Option<String>,
    /// Tags for categorization
    #[serde(default)]
    pub tags: Vec<String>,
}

/// Detected run mode based on filename
#[derive(Debug, Clone, PartialEq)]
pub enum RunMode {
    /// Viewer-only installation (filename contains "Viewer")
    Viewer,
    /// Temporary support session (filename contains 6-digit code)
    TempSupport(String),
    /// Permanent agent with embedded config
    PermanentAgent,
    /// Unknown/default mode
    Default,
}

//! Agent configuration management

use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use uuid::Uuid;

/// Agent configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
    /// Server WebSocket URL (e.g., wss://connect.example.com/ws)
    pub server_url: String,

    /// Agent API key for authentication
    pub api_key: String,

    /// Unique agent identifier (generated on first run)
    #[serde(default = "generate_agent_id")]
    pub agent_id: String,

    /// Optional hostname override
    pub hostname_override: Option<String>,

    /// Support code for one-time support sessions (set via command line)
    #[serde(skip)]
    pub support_code: Option<String>,

    /// Capture settings
    #[serde(default)]
    pub capture: CaptureConfig,

    /// Encoding settings
    #[serde(default)]
    pub encoding: EncodingConfig,
}

fn generate_agent_id() -> String {
    Uuid::new_v4().to_string()
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CaptureConfig {
    /// Target frames per second (1-60)
    #[serde(default = "default_fps")]
    pub fps: u32,

    /// Use DXGI Desktop Duplication (recommended)
    #[serde(default = "default_true")]
    pub use_dxgi: bool,

    /// Fall back to GDI if DXGI fails
    #[serde(default = "default_true")]
    pub gdi_fallback: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncodingConfig {
    /// Preferred codec (auto, raw, vp9, h264)
    #[serde(default = "default_codec")]
    pub codec: String,

    /// Quality (1-100, higher = better quality, more bandwidth)
    #[serde(default = "default_quality")]
    pub quality: u32,

    /// Use hardware encoding if available
    #[serde(default = "default_true")]
    pub hardware_encoding: bool,
}

fn default_fps() -> u32 {
    30
}

fn default_true() -> bool {
    true
}

fn default_codec() -> String {
    "auto".to_string()
}

fn default_quality() -> u32 {
    75
}

impl Default for CaptureConfig {
    fn default() -> Self {
        Self {
            fps: default_fps(),
            use_dxgi: true,
            gdi_fallback: true,
        }
    }
}

impl Default for EncodingConfig {
    fn default() -> Self {
        Self {
            codec: default_codec(),
            quality: default_quality(),
            hardware_encoding: true,
        }
    }
}

impl Config {
    /// Check if an explicit agent configuration file exists
    /// This returns true only if there's a real config file, not generated defaults
    pub fn has_agent_config() -> bool {
        // Check for config in current directory
        let local_config = PathBuf::from("guruconnect.toml");
        if local_config.exists() {
            return true;
        }

        // Check in program data directory (Windows)
        #[cfg(windows)]
        {
            if let Ok(program_data) = std::env::var("ProgramData") {
                let path = PathBuf::from(program_data)
                    .join("GuruConnect")
                    .join("agent.toml");
                if path.exists() {
                    return true;
                }
            }
        }

        false
    }

    /// Load configuration from file or environment
    pub fn load() -> Result<Self> {
        // Try loading from config file
        let config_path = Self::config_path();

        if config_path.exists() {
            let contents = std::fs::read_to_string(&config_path)
                .with_context(|| format!("Failed to read config from {:?}", config_path))?;

            let mut config: Config = toml::from_str(&contents)
                .with_context(|| "Failed to parse config file")?;

            // Ensure agent_id is set and saved
            if config.agent_id.is_empty() {
                config.agent_id = generate_agent_id();
                let _ = config.save();
            }

            // support_code is always None when loading from file (set via CLI)
            config.support_code = None;

            return Ok(config);
        }

        // Fall back to environment variables
        let server_url = std::env::var("GURUCONNECT_SERVER_URL")
            .unwrap_or_else(|_| "wss://connect.azcomputerguru.com/ws/agent".to_string());

        let api_key = std::env::var("GURUCONNECT_API_KEY")
            .unwrap_or_else(|_| "dev-key".to_string());

        let agent_id = std::env::var("GURUCONNECT_AGENT_ID")
            .unwrap_or_else(|_| generate_agent_id());

        let config = Config {
            server_url,
            api_key,
            agent_id,
            hostname_override: std::env::var("GURUCONNECT_HOSTNAME").ok(),
            support_code: None, // Set via CLI
            capture: CaptureConfig::default(),
            encoding: EncodingConfig::default(),
        };

        // Save config with generated agent_id for persistence
        let _ = config.save();

        Ok(config)
    }

    /// Get the configuration file path
    fn config_path() -> PathBuf {
        // Check for config in current directory first
        let local_config = PathBuf::from("guruconnect.toml");
        if local_config.exists() {
            return local_config;
        }

        // Check in program data directory (Windows)
        #[cfg(windows)]
        {
            if let Ok(program_data) = std::env::var("ProgramData") {
                let path = PathBuf::from(program_data)
                    .join("GuruConnect")
                    .join("agent.toml");
                if path.exists() {
                    return path;
                }
            }
        }

        // Default to local config
        local_config
    }

    /// Get the hostname to use
    pub fn hostname(&self) -> String {
        self.hostname_override
            .clone()
            .unwrap_or_else(|| {
                hostname::get()
                    .map(|h| h.to_string_lossy().to_string())
                    .unwrap_or_else(|_| "unknown".to_string())
            })
    }

    /// Save current configuration to file
    pub fn save(&self) -> Result<()> {
        let config_path = Self::config_path();

        // Ensure parent directory exists
        if let Some(parent) = config_path.parent() {
            std::fs::create_dir_all(parent)?;
        }

        let contents = toml::to_string_pretty(self)?;
        std::fs::write(&config_path, contents)?;

        Ok(())
    }
}

/// Example configuration file content
pub fn example_config() -> &'static str {
    r#"# GuruConnect Agent Configuration

# Server connection
server_url = "wss://connect.example.com/ws"
api_key = "your-agent-api-key"
agent_id = "auto-generated-uuid"

# Optional: override hostname
# hostname_override = "custom-hostname"

[capture]
fps = 30
use_dxgi = true
gdi_fallback = true

[encoding]
codec = "auto"      # auto, raw, vp9, h264
quality = 75        # 1-100
hardware_encoding = true
"#
}

+//! Supports three configuration sources (in priority order):

+//! 1. Embedded config (magic bytes appended to executable)

+//! 2. Config file (guruconnect.toml or %ProgramData%\GuruConnect\agent.toml)

+//! 3. Environment variables (fallback)

+/// Magic marker for embedded configuration (10 bytes)

+const MAGIC_MARKER: &[u8] = b"GURUCONFIG";

+/// Embedded configuration data (appended to executable)

+#[derive(Debug, Clone, Serialize, Deserialize)]

+/// Detected run mode based on filename

+    /// Viewer-only installation (filename contains "Viewer")

+    /// Temporary support session (filename contains 6-digit code)

+    /// Permanent agent with embedded config


Good, now I need to add the company and site fields to the Config struct to store them when loaded from embedded config.


Now let me add the company/site fields to the Config struct:


/// Agent configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
    /// Server WebSocket URL (e.g., wss://connect.example.com/ws)
    pub server_url: String,

    /// Agent API key for authentication
    pub api_key: String,

    /// Unique agent identifier (generated on first run)
    #[serde(default = "generate_agent_id")]
    pub agent_id: String,

    /// Optional hostname override
    pub hostname_override: Option<String>,

    /// Support code for one-time support sessions (set via command line)
    #[serde(skip)]
    pub support_code: Option<String>,

    /// Capture settings
    #[serde(default)]
    pub capture: CaptureConfig,

    /// Encoding settings
    #[serde(default)]
    pub encoding: EncodingConfig,
}

/// Agent configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
    /// Server WebSocket URL (e.g., wss://connect.example.com/ws)
    pub server_url: String,

    /// Agent API key for authentication
    pub api_key: String,

    /// Unique agent identifier (generated on first run)
    #[serde(default = "generate_agent_id")]
    pub agent_id: String,

    /// Optional hostname override
    pub hostname_override: Option<String>,

    /// Company/organization name (from embedded config)
    #[serde(default)]
    pub company: Option<String>,

    /// Site/location name (from embedded config)
    #[serde(default)]
    pub site: Option<String>,

    /// Tags for categorization (from embedded config)
    #[serde(default)]
    pub tags: Vec<String>,

    /// Support code for one-time support sessions (set via command line or filename)
    #[serde(skip)]
    pub support_code: Option<String>,

    /// Capture settings
    #[serde(default)]
    pub capture: CaptureConfig,

    /// Encoding settings
    #[serde(default)]
    pub encoding: EncodingConfig,
}