Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 43 additions & 14 deletions crates/reach-cli/src/commands/connect.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
use clap::Args;
use reach_cli::docker::DockerClient;
use reach_cli::mcp::{
JsonRpcRequest, JsonRpcResponse, McpInitializeResult, RequestId, ToolResponse,
tool_definitions,
JsonRpcRequest, JsonRpcResponse, McpInitializeResult, RequestId, ToolResponse, tool_definitions,
};
use clap::Args;
use std::io::{BufRead, Write};

#[derive(Args)]
Expand Down Expand Up @@ -63,7 +62,11 @@ async fn handle_request(
JsonRpcResponse::success(req.id.clone(), serde_json::json!({ "tools": tools }))
}
"tools/call" => {
let tool_name = req.params.get("name").and_then(|v| v.as_str()).unwrap_or("");
let tool_name = req
.params
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("");
let arguments = req.params.get("arguments").cloned().unwrap_or_default();
dispatch_tool(docker, target, req, tool_name, &arguments).await
}
Expand Down Expand Up @@ -102,7 +105,12 @@ async fn dispatch_tool(
Some("middle") => "2",
_ => "1",
};
exec_cmd(docker, target, &format!("xdotool mousemove {x} {y} click {btn}")).await
exec_cmd(
docker,
target,
&format!("xdotool mousemove {x} {y} click {btn}"),
)
.await
}
"type" => {
let text = args.get("text").and_then(|v| v.as_str()).unwrap_or("");
Expand All @@ -114,11 +122,17 @@ async fn dispatch_tool(
.await
}
"key" => {
let combo = args.get("combo").and_then(|v| v.as_str()).unwrap_or("Return");
let combo = args
.get("combo")
.and_then(|v| v.as_str())
.unwrap_or("Return");
exec_cmd(docker, target, &format!("xdotool key {combo}")).await
}
"browse" => {
let url = args.get("url").and_then(|v| v.as_str()).unwrap_or("about:blank");
let url = args
.get("url")
.and_then(|v| v.as_str())
.unwrap_or("about:blank");
exec_cmd(
docker,
target,
Expand All @@ -131,9 +145,19 @@ async fn dispatch_tool(
}
"scrape" => {
let url = args.get("url").and_then(|v| v.as_str()).unwrap_or("");
let selector = args.get("selector").and_then(|v| v.as_str()).unwrap_or("body");
let stealth = args.get("stealth").and_then(|v| v.as_bool()).unwrap_or(true);
let fetcher = if stealth { "StealthyFetcher" } else { "Fetcher" };
let selector = args
.get("selector")
.and_then(|v| v.as_str())
.unwrap_or("body");
let stealth = args
.get("stealth")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let fetcher = if stealth {
"StealthyFetcher"
} else {
"Fetcher"
};
let script = format!(
"from scrapling import {fetcher}; r = {fetcher}().get('{url}'); \
elems = r.css('{selector}'); \
Expand All @@ -146,7 +170,10 @@ async fn dispatch_tool(
exec_python(docker, target, script).await
}
"exec" => {
let cmd = args.get("command").and_then(|v| v.as_str()).unwrap_or("echo");
let cmd = args
.get("command")
.and_then(|v| v.as_str())
.unwrap_or("echo");
exec_cmd(docker, target, cmd).await
}
_ => ToolResponse::error(format!("unknown tool: {tool}")),
Expand All @@ -160,9 +187,11 @@ async fn exec_cmd(docker: &DockerClient, target: &str, cmd: &str) -> ToolRespons
.exec(target, &["bash".into(), "-c".into(), cmd.into()])
.await
{
Ok(out) if out.exit_code == 0 => {
ToolResponse::text(if out.stdout.is_empty() { "ok".into() } else { out.stdout })
}
Ok(out) if out.exit_code == 0 => ToolResponse::text(if out.stdout.is_empty() {
"ok".into()
} else {
out.stdout
}),
Ok(out) => ToolResponse::error(format!("exit {}: {}", out.exit_code, out.stderr)),
Err(e) => ToolResponse::error(e.to_string()),
}
Expand Down
4 changes: 2 additions & 2 deletions crates/reach-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use reach_cli::config::ReachConfig;
use reach_cli::docker::{DockerClient, Resolution, SandboxConfig, SandboxPorts};
use clap::Args;
use colored::Colorize;
use reach_cli::config::ReachConfig;
use reach_cli::docker::{DockerClient, Resolution, SandboxConfig, SandboxPorts};
use std::time::Duration;

#[derive(Args)]
Expand Down
2 changes: 1 addition & 1 deletion crates/reach-cli/src/commands/destroy.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use reach_cli::docker::DockerClient;
use clap::Args;
use colored::Colorize;
use reach_cli::docker::DockerClient;

#[derive(Args)]
pub struct DestroyArgs {
Expand Down
2 changes: 1 addition & 1 deletion crates/reach-cli/src/commands/exec.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use reach_cli::docker::DockerClient;
use clap::Args;
use reach_cli::docker::DockerClient;

#[derive(Args)]
pub struct ExecArgs {
Expand Down
2 changes: 1 addition & 1 deletion crates/reach-cli/src/commands/list.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use reach_cli::docker::{DockerClient, SandboxStatus};
use colored::Colorize;
use reach_cli::docker::{DockerClient, SandboxStatus};

pub async fn run() -> anyhow::Result<()> {
let docker = DockerClient::new()?;
Expand Down
2 changes: 1 addition & 1 deletion crates/reach-cli/src/commands/screenshot.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use reach_cli::docker::DockerClient;
use clap::Args;
use colored::Colorize;
use reach_cli::docker::DockerClient;
use std::io::Write;

#[derive(Args)]
Expand Down
72 changes: 52 additions & 20 deletions crates/reach-cli/src/commands/serve.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use reach_cli::docker::DockerClient;
use reach_cli::mcp::{
JsonRpcRequest, JsonRpcResponse, McpInitializeResult, ToolResponse, tool_definitions,
};
use axum::extract::State;
use axum::response::sse::{Event, Sse};
use axum::routing::{get, post};
use axum::{Json, Router};
use clap::Args;
use reach_cli::docker::DockerClient;
use reach_cli::mcp::{
JsonRpcRequest, JsonRpcResponse, McpInitializeResult, ToolResponse, tool_definitions,
};
use std::convert::Infallible;
use std::sync::Arc;

Expand Down Expand Up @@ -74,9 +74,7 @@ async fn mcp_handler(
}

async fn sse_handler() -> Sse<impl tokio_stream::Stream<Item = Result<Event, Infallible>>> {
let stream = tokio_stream::once(Ok(
Event::default().event("endpoint").data("/mcp"),
));
let stream = tokio_stream::once(Ok(Event::default().event("endpoint").data("/mcp")));
Sse::new(stream)
}

Expand All @@ -91,7 +89,11 @@ async fn handle_mcp(state: &AppState, req: &JsonRpcRequest) -> JsonRpcResponse {
JsonRpcResponse::success(req.id.clone(), serde_json::json!({ "tools": tools }))
}
"tools/call" => {
let tool = req.params.get("name").and_then(|v| v.as_str()).unwrap_or("");
let tool = req
.params
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("");
let args = req.params.get("arguments").cloned().unwrap_or_default();
let sandbox_arg = args.get("sandbox").and_then(|v| v.as_str());

Expand Down Expand Up @@ -144,19 +146,34 @@ async fn dispatch(
Some("middle") => "2",
_ => "1",
};
sh(state, target, &format!("xdotool mousemove {x} {y} click {btn}")).await
sh(
state,
target,
&format!("xdotool mousemove {x} {y} click {btn}"),
)
.await
}
"type" => {
let text = args.get("text").and_then(|v| v.as_str()).unwrap_or("");
sh(state, target, &format!("xdotool type -- '{}'", text.replace('\'', "'\\''")))
.await
sh(
state,
target,
&format!("xdotool type -- '{}'", text.replace('\'', "'\\''")),
)
.await
}
"key" => {
let combo = args.get("combo").and_then(|v| v.as_str()).unwrap_or("Return");
let combo = args
.get("combo")
.and_then(|v| v.as_str())
.unwrap_or("Return");
sh(state, target, &format!("xdotool key {combo}")).await
}
"browse" => {
let url = args.get("url").and_then(|v| v.as_str()).unwrap_or("about:blank");
let url = args
.get("url")
.and_then(|v| v.as_str())
.unwrap_or("about:blank");
sh(
state,
target,
Expand All @@ -169,9 +186,19 @@ async fn dispatch(
}
"scrape" => {
let url = args.get("url").and_then(|v| v.as_str()).unwrap_or("");
let sel = args.get("selector").and_then(|v| v.as_str()).unwrap_or("body");
let stealth = args.get("stealth").and_then(|v| v.as_bool()).unwrap_or(true);
let f = if stealth { "StealthyFetcher" } else { "Fetcher" };
let sel = args
.get("selector")
.and_then(|v| v.as_str())
.unwrap_or("body");
let stealth = args
.get("stealth")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let f = if stealth {
"StealthyFetcher"
} else {
"Fetcher"
};
py(
state,
target,
Expand All @@ -188,7 +215,10 @@ async fn dispatch(
py(state, target, script).await
}
"exec" => {
let cmd = args.get("command").and_then(|v| v.as_str()).unwrap_or("echo");
let cmd = args
.get("command")
.and_then(|v| v.as_str())
.unwrap_or("echo");
sh(state, target, cmd).await
}
_ => ToolResponse::error(format!("unknown tool: {tool}")),
Expand All @@ -201,9 +231,11 @@ async fn sh(state: &AppState, target: &str, cmd: &str) -> ToolResponse {
.exec(target, &["bash".into(), "-c".into(), cmd.into()])
.await
{
Ok(out) if out.exit_code == 0 => {
ToolResponse::text(if out.stdout.is_empty() { "ok".into() } else { out.stdout })
}
Ok(out) if out.exit_code == 0 => ToolResponse::text(if out.stdout.is_empty() {
"ok".into()
} else {
out.stdout
}),
Ok(out) => ToolResponse::error(format!("exit {}: {}", out.exit_code, out.stderr)),
Err(e) => ToolResponse::error(e.to_string()),
}
Expand Down
2 changes: 1 addition & 1 deletion crates/reach-cli/src/commands/vnc.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use reach_cli::docker::DockerClient;
use clap::Args;
use colored::Colorize;
use reach_cli::docker::DockerClient;

#[derive(Args)]
pub struct VncArgs {
Expand Down
2 changes: 0 additions & 2 deletions crates/reach-cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ pub struct DockerConfig {
// Defaults
// ═══════════════════════════════════════════════════════════


impl Default for SandboxDefaults {
fn default() -> Self {
Self {
Expand All @@ -75,7 +74,6 @@ impl Default for ServerConfig {
}
}


// ═══════════════════════════════════════════════════════════
// Loading
// ═══════════════════════════════════════════════════════════
Expand Down
21 changes: 8 additions & 13 deletions crates/reach-cli/src/docker.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use anyhow::{Context, Result, bail};
use bollard::container::{
Config, CreateContainerOptions, ListContainersOptions,
RemoveContainerOptions, StopContainerOptions,
Config, CreateContainerOptions, ListContainersOptions, RemoveContainerOptions,
StopContainerOptions,
};
use bollard::exec::{CreateExecOptions, StartExecResults};
use bollard::models::{HostConfig, PortBinding};
Expand Down Expand Up @@ -261,10 +261,7 @@ impl DockerClient {
let sandbox = self.find(target).await?;

self.client
.stop_container(
&sandbox.container_id,
Some(StopContainerOptions { t: 10 }),
)
.stop_container(&sandbox.container_id, Some(StopContainerOptions { t: 10 }))
.await
.context("failed to stop container")?;

Expand Down Expand Up @@ -314,10 +311,7 @@ impl DockerClient {
status,
image: c.image.unwrap_or_default(),
ports,
created_at: labels
.get(Labels::CREATED)
.cloned()
.unwrap_or_default(),
created_at: labels.get(Labels::CREATED).cloned().unwrap_or_default(),
}
})
.collect();
Expand Down Expand Up @@ -423,9 +417,10 @@ impl DockerClient {
.await;

if let Ok(result) = out
&& result.exit_code == 0 {
return Ok(());
}
&& result.exit_code == 0
{
return Ok(());
}

tokio::time::sleep(Duration::from_millis(500)).await;
}
Expand Down
4 changes: 3 additions & 1 deletion crates/reach-cli/src/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,9 @@ pub enum ExtractMode {
Html,
/// Extract a specific attribute value
#[serde(rename = "attr")]
Attribute { name: String },
Attribute {
name: String,
},
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down
6 changes: 5 additions & 1 deletion crates/reach-cli/tests/docker_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,11 @@ fn labels_for_sandbox_includes_all_required_keys() {
fn labels_filter_targets_managed_containers() {
let filter = Labels::filter();
let label_filters = filter.get("label").unwrap();
assert!(label_filters.iter().any(|f| f.contains("reach.sandbox=true")));
assert!(
label_filters
.iter()
.any(|f| f.contains("reach.sandbox=true"))
);
}

// ═══════════════════════════════════════════════════════════
Expand Down
Loading
Loading