feat: find-window, run, clipboard, label-routing, persistent session-id, exe icon

This commit is contained in:
Helios Agent 2026-03-03 15:19:54 +01:00
parent ef4ca0ccbb
commit 672676d3d7
No known key found for this signature in database
GPG key ID: C8259547CD8309B5
9 changed files with 214 additions and 15 deletions

View file

@ -11,6 +11,7 @@ use tokio_tungstenite::{connect_async_tls_with_config, tungstenite::Message, Con
use base64::Engine;
use helios_common::{ClientMessage, ServerMessage};
use uuid::Uuid;
mod shell;
mod screenshot;
@ -49,22 +50,14 @@ macro_rules! log_cmd {
};
}
fn session_id() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let t = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.subsec_nanos();
format!("{:06x}", t & 0xFFFFFF)
}
// ────────────────────────────────────────────────────────────────────────────
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Config {
relay_url: String,
api_key: String,
label: Option<String>,
session_id: Option<String>, // persistent UUID
}
impl Config {
@ -129,7 +122,7 @@ fn prompt_config() -> Config {
}
};
Config { relay_url, api_key, label }
Config { relay_url, api_key, label, session_id: None }
}
#[tokio::main]
@ -158,6 +151,20 @@ async fn main() {
}
};
// Resolve or generate persistent session UUID
let sid: Uuid = match &config.session_id {
Some(id) => Uuid::parse_str(id).unwrap_or_else(|_| Uuid::new_v4()),
None => {
let id = Uuid::new_v4();
let mut cfg = config.clone();
cfg.session_id = Some(id.to_string());
if let Err(e) = cfg.save() {
log_err!("Failed to save session_id: {e}");
}
id
}
};
let config = Arc::new(config);
let shell = Arc::new(Mutex::new(shell::PersistentShell::new()));
@ -183,14 +190,13 @@ async fn main() {
match connect_async_tls_with_config(&config.relay_url, None, false, Some(connector)).await {
Ok((ws_stream, _)) => {
let sid = session_id();
let label = config.label.clone().unwrap_or_else(|| hostname());
log_ok!(
"Connected {} {} {} Session {}",
"·".dimmed(),
label.bold(),
"·".dimmed(),
sid.dimmed()
sid.to_string().dimmed()
);
println!();
backoff = Duration::from_secs(1);
@ -499,6 +505,51 @@ async fn handle_message(
}
}
ServerMessage::RunRequest { request_id, program, args } => {
log_cmd!("run {}", program);
use std::process::Command as StdCommand;
match StdCommand::new(&program).args(&args).spawn() {
Ok(_) => {
log_ok!("Started {}", program);
ClientMessage::Ack { request_id }
}
Err(e) => {
log_err!("run failed: {e}");
ClientMessage::Error { request_id, message: format!("Failed to start '{}': {e}", program) }
}
}
}
ServerMessage::ClipboardGetRequest { request_id } => {
log_cmd!("clipboard-get");
let out = tokio::process::Command::new("powershell.exe")
.args(["-NoProfile", "-NonInteractive", "-Command", "Get-Clipboard"])
.output().await;
match out {
Ok(o) => {
let text = String::from_utf8_lossy(&o.stdout).trim().to_string();
log_ok!("Got {} chars", text.len());
ClientMessage::ClipboardGetResponse { request_id, text }
}
Err(e) => ClientMessage::Error { request_id, message: format!("Clipboard get failed: {e}") }
}
}
ServerMessage::ClipboardSetRequest { request_id, text } => {
log_cmd!("clipboard-set {} chars", text.len());
let cmd = format!("Set-Clipboard -Value '{}'", text.replace('\'', "''"));
let out = tokio::process::Command::new("powershell.exe")
.args(["-NoProfile", "-NonInteractive", "-Command", &cmd])
.output().await;
match out {
Ok(_) => {
log_ok!("Set clipboard");
ClientMessage::Ack { request_id }
}
Err(e) => ClientMessage::Error { request_id, message: format!("Clipboard set failed: {e}") }
}
}
ServerMessage::Ack { request_id } => {
ClientMessage::Ack { request_id }
}