mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
Read paths from an interactive & login shell (#5774)
This commit is contained in:
Generated
-1
@@ -2787,7 +2787,6 @@ dependencies = [
|
||||
"docx-rs",
|
||||
"etcetera",
|
||||
"glob",
|
||||
"goose",
|
||||
"http-body-util",
|
||||
"hyper 1.6.0",
|
||||
"ignore",
|
||||
|
||||
@@ -2,6 +2,10 @@ use anyhow::Result;
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
|
||||
use goose::config::{Config, ExtensionConfig};
|
||||
use goose_mcp::mcp_server_runner::{serve, McpCommand};
|
||||
use goose_mcp::{
|
||||
AutoVisualiserRouter, ComputerControllerServer, DeveloperServer, MemoryServer, TutorialServer,
|
||||
};
|
||||
|
||||
use crate::commands::acp::run_acp_agent;
|
||||
use crate::commands::bench::agent_generator;
|
||||
@@ -420,7 +424,10 @@ enum Command {
|
||||
|
||||
/// Manage system prompts and behaviors
|
||||
#[command(about = "Run one of the mcp servers bundled with goose")]
|
||||
Mcp { name: String },
|
||||
Mcp {
|
||||
#[arg(value_parser = clap::value_parser!(McpCommand))]
|
||||
server: McpCommand,
|
||||
},
|
||||
|
||||
/// Run goose as an ACP (Agent Client Protocol) agent
|
||||
#[command(about = "Run goose as an ACP agent server on stdio")]
|
||||
@@ -877,15 +884,18 @@ pub async fn cli() -> anyhow::Result<()> {
|
||||
);
|
||||
|
||||
match cli.command {
|
||||
Some(Command::Configure {}) => {
|
||||
handle_configure().await?;
|
||||
}
|
||||
Some(Command::Info { verbose }) => {
|
||||
handle_info(verbose)?;
|
||||
}
|
||||
Some(Command::Mcp { name }) => {
|
||||
Some(Command::Configure {}) => handle_configure().await?,
|
||||
Some(Command::Info { verbose }) => handle_info(verbose)?,
|
||||
Some(Command::Mcp { server }) => {
|
||||
let name = server.name();
|
||||
crate::logging::setup_logging(Some(&format!("mcp-{name}")), None)?;
|
||||
goose_mcp::mcp_server_runner::run_mcp_server(&name).await?;
|
||||
match server {
|
||||
McpCommand::AutoVisualiser => serve(AutoVisualiserRouter::new()).await?,
|
||||
McpCommand::ComputerController => serve(ComputerControllerServer::new()).await?,
|
||||
McpCommand::Memory => serve(MemoryServer::new()).await?,
|
||||
McpCommand::Tutorial => serve(TutorialServer::new()).await?,
|
||||
McpCommand::Developer => serve(DeveloperServer::new()).await?,
|
||||
}
|
||||
}
|
||||
Some(Command::Acp {}) => {
|
||||
run_acp_agent().await?;
|
||||
|
||||
@@ -11,7 +11,6 @@ description.workspace = true
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
goose = { path = "../goose" }
|
||||
rmcp = { version = "0.8.1", features = ["server", "client", "transport-io", "macros"] }
|
||||
anyhow = "1.0.94"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
@@ -77,13 +76,13 @@ libc = "0.2"
|
||||
# ~1000 downloads). Pinned to exact version to prevent supply chain attacks.
|
||||
mpatch = "=0.2.0"
|
||||
tokio-util = "0.7.16"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
serial_test = "3.0.0"
|
||||
sysinfo = "0.32.1"
|
||||
temp-env = "0.3.6"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
colored = "2"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -2,11 +2,19 @@ use crate::developer::analyze::types::{
|
||||
AnalysisMode, AnalysisResult, CallChain, EntryType, FocusedAnalysisData,
|
||||
};
|
||||
use crate::developer::lang;
|
||||
use goose::utils::safe_truncate;
|
||||
use rmcp::model::{Content, Role};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn safe_truncate(s: &str, max_chars: usize) -> String {
|
||||
if s.chars().count() <= max_chars {
|
||||
s.to_string()
|
||||
} else {
|
||||
let truncated: String = s.chars().take(max_chars.saturating_sub(3)).collect();
|
||||
format!("{}...", truncated)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Formatter;
|
||||
|
||||
impl Formatter {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod analyze;
|
||||
mod editor_models;
|
||||
mod lang;
|
||||
pub mod paths;
|
||||
mod shell;
|
||||
mod text_editor;
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
use anyhow::Result;
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use tokio::process::Command;
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
static SHELL_PATH_DIRS: OnceCell<Result<Vec<PathBuf>, anyhow::Error>> = OnceCell::const_new();
|
||||
|
||||
pub async fn get_shell_path_dirs() -> Result<&'static Vec<PathBuf>> {
|
||||
let result = SHELL_PATH_DIRS
|
||||
.get_or_init(|| async {
|
||||
get_shell_path_async()
|
||||
.await
|
||||
.map(|path| env::split_paths(&path).collect())
|
||||
})
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(dirs) => Ok(dirs),
|
||||
Err(e) => Err(anyhow::anyhow!(
|
||||
"Failed to get shell PATH directories: {}",
|
||||
e
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_shell_path_async() -> Result<String> {
|
||||
let shell = env::var("SHELL").unwrap_or_else(|_| {
|
||||
if cfg!(windows) {
|
||||
"cmd".to_string()
|
||||
} else {
|
||||
"/bin/bash".to_string()
|
||||
}
|
||||
});
|
||||
|
||||
if cfg!(windows) {
|
||||
get_windows_path_async(&shell).await
|
||||
} else {
|
||||
get_unix_path_async(&shell).await
|
||||
}
|
||||
.or_else(|e| {
|
||||
tracing::warn!(
|
||||
"Failed to get PATH from shell ({}), falling back to current PATH",
|
||||
e
|
||||
);
|
||||
env::var("PATH").map_err(|_| anyhow::anyhow!("No PATH variable available"))
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_unix_path_async(shell: &str) -> Result<String> {
|
||||
let output = Command::new(shell)
|
||||
.args(["-l", "-i", "-c", "echo $PATH"])
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to execute shell command: {}", e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(anyhow::anyhow!("Shell command failed: {}", stderr));
|
||||
}
|
||||
|
||||
let path = String::from_utf8(output.stdout)
|
||||
.map_err(|e| anyhow::anyhow!("Invalid UTF-8 in shell output: {}", e))?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
if path.is_empty() {
|
||||
return Err(anyhow::anyhow!("Shell returned empty PATH"));
|
||||
}
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
async fn get_windows_path_async(shell: &str) -> Result<String> {
|
||||
let shell_name = std::path::Path::new(shell)
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("cmd");
|
||||
|
||||
let output = match shell_name {
|
||||
"pwsh" | "powershell" => {
|
||||
Command::new(shell)
|
||||
.args(["-NoLogo", "-Command", "$env:PATH"])
|
||||
.output()
|
||||
.await
|
||||
}
|
||||
_ => {
|
||||
Command::new(shell)
|
||||
.args(["/c", "echo %PATH%"])
|
||||
.output()
|
||||
.await
|
||||
}
|
||||
};
|
||||
|
||||
let output = output.map_err(|e| anyhow::anyhow!("Failed to execute shell command: {}", e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(anyhow::anyhow!("Shell command failed: {}", stderr));
|
||||
}
|
||||
|
||||
let path = String::from_utf8(output.stdout)
|
||||
.map_err(|e| anyhow::anyhow!("Invalid UTF-8 in shell output: {}", e))?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
if path.is_empty() {
|
||||
return Err(anyhow::anyhow!("Shell returned empty PATH"));
|
||||
}
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use anyhow::anyhow;
|
||||
use base64::Engine;
|
||||
use ignore::gitignore::{Gitignore, GitignoreBuilder};
|
||||
use include_dir::{include_dir, Dir};
|
||||
@@ -17,6 +18,8 @@ use rmcp::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env::join_paths,
|
||||
ffi::OsString,
|
||||
future::Future,
|
||||
io::Cursor,
|
||||
path::{Path, PathBuf},
|
||||
@@ -31,11 +34,11 @@ use tokio::{
|
||||
use tokio_stream::{wrappers::SplitStream, StreamExt as _};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::developer::{paths::get_shell_path_dirs, shell::ShellConfig};
|
||||
|
||||
use super::analyze::{types::AnalyzeParams, CodeAnalyzer};
|
||||
use super::editor_models::{create_editor_model, EditorModel};
|
||||
use super::shell::{
|
||||
configure_shell_command, expand_path, get_shell_config, is_absolute_path, kill_process_group,
|
||||
};
|
||||
use super::shell::{configure_shell_command, expand_path, is_absolute_path, kill_process_group};
|
||||
use super::text_editor::{
|
||||
text_editor_insert, text_editor_replace, text_editor_undo, text_editor_view, text_editor_write,
|
||||
};
|
||||
@@ -179,6 +182,8 @@ pub struct DeveloperServer {
|
||||
pub running_processes: Arc<RwLock<HashMap<String, CancellationToken>>>,
|
||||
#[cfg(not(test))]
|
||||
running_processes: Arc<RwLock<HashMap<String, CancellationToken>>>,
|
||||
bash_env_file: Option<PathBuf>,
|
||||
extend_path_with_shell: bool,
|
||||
}
|
||||
|
||||
#[tool_handler(router = self.tool_router)]
|
||||
@@ -549,9 +554,21 @@ impl DeveloperServer {
|
||||
prompts: load_prompt_files(),
|
||||
code_analyzer: CodeAnalyzer::new(),
|
||||
running_processes: Arc::new(RwLock::new(HashMap::new())),
|
||||
extend_path_with_shell: false,
|
||||
bash_env_file: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extend_path_with_shell(mut self, value: bool) -> Self {
|
||||
self.extend_path_with_shell = value;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn bash_env_file(mut self, value: Option<PathBuf>) -> Self {
|
||||
self.bash_env_file = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// List all available windows that can be used with screen_capture.
|
||||
/// Returns a list of window titles that can be used with the window_title parameter
|
||||
/// of the screen_capture tool.
|
||||
@@ -942,10 +959,34 @@ impl DeveloperServer {
|
||||
peer: &rmcp::service::Peer<RoleServer>,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> Result<String, ErrorData> {
|
||||
// Get platform-specific shell configuration
|
||||
let shell_config = get_shell_config();
|
||||
let mut shell_config = ShellConfig::default();
|
||||
let shell_name = std::path::Path::new(&shell_config.executable)
|
||||
.file_name()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("bash");
|
||||
|
||||
let mut child = configure_shell_command(&shell_config, command)
|
||||
if let Some(ref env_file) = self.bash_env_file {
|
||||
if shell_name == "bash" {
|
||||
shell_config.envs.push((
|
||||
OsString::from("BASH_ENV"),
|
||||
env_file.clone().into_os_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
let mut command = configure_shell_command(&shell_config, command);
|
||||
|
||||
if self.extend_path_with_shell {
|
||||
if let Err(e) = get_shell_path_dirs()
|
||||
.await
|
||||
.and_then(|dirs| join_paths(dirs).map_err(|e| anyhow!(e)))
|
||||
.map(|path| command.env("PATH", path))
|
||||
{
|
||||
tracing::error!("Failed to extend PATH with shell directories: {}", e)
|
||||
}
|
||||
}
|
||||
|
||||
let mut child = command
|
||||
.spawn()
|
||||
.map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?;
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::{env, ffi::OsString, process::Stdio};
|
||||
|
||||
use goose::config::paths::Paths;
|
||||
#[cfg(unix)]
|
||||
#[allow(unused_imports)] // False positive: trait is used for process_group method
|
||||
use std::os::unix::process::CommandExt;
|
||||
@@ -22,24 +21,10 @@ impl Default for ShellConfig {
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let shell = env::var("SHELL").unwrap_or_else(|_| "bash".to_string());
|
||||
// Get just the shell name from the path (e.g., /bin/zsh -> zsh)
|
||||
let shell_name = std::path::Path::new(&shell)
|
||||
.file_name()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("bash");
|
||||
|
||||
// Configure environment based on shell type
|
||||
let envs = if shell_name == "bash" {
|
||||
let bash_env = Paths::config_dir().join(".bash_env").into_os_string();
|
||||
vec![(OsString::from("BASH_ENV"), bash_env)]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
Self {
|
||||
executable: shell,
|
||||
args: vec!["-c".to_string()], // -c is standard across bash/zsh/fish
|
||||
envs,
|
||||
envs: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,10 +67,6 @@ impl ShellConfig {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_shell_config() -> ShellConfig {
|
||||
ShellConfig::default()
|
||||
}
|
||||
|
||||
pub fn expand_path(path_str: &str) -> String {
|
||||
if cfg!(windows) {
|
||||
// Expand Windows environment variables (%VAR%)
|
||||
|
||||
@@ -1,37 +1,45 @@
|
||||
use crate::{
|
||||
AutoVisualiserRouter, ComputerControllerServer, DeveloperServer, MemoryServer, TutorialServer,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Result;
|
||||
use rmcp::{transport::stdio, ServiceExt};
|
||||
|
||||
/// Run an MCP server by name
|
||||
///
|
||||
/// This function handles the common logic for starting MCP servers.
|
||||
/// The caller is responsible for setting up logging before calling this function.
|
||||
pub async fn run_mcp_server(name: &str) -> Result<()> {
|
||||
if name == "googledrive" || name == "google_drive" {
|
||||
return Err(anyhow!(
|
||||
"the built-in Google Drive extension has been removed"
|
||||
));
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum McpCommand {
|
||||
AutoVisualiser,
|
||||
ComputerController,
|
||||
Developer,
|
||||
Memory,
|
||||
Tutorial,
|
||||
}
|
||||
|
||||
tracing::info!("Starting MCP server");
|
||||
impl FromStr for McpCommand {
|
||||
type Err = String;
|
||||
|
||||
match name.to_lowercase().replace(' ', "").as_str() {
|
||||
"autovisualiser" => serve_and_wait(AutoVisualiserRouter::new()).await,
|
||||
"computercontroller" => serve_and_wait(ComputerControllerServer::new()).await,
|
||||
"developer" => serve_and_wait(DeveloperServer::new()).await,
|
||||
"memory" => serve_and_wait(MemoryServer::new()).await,
|
||||
"tutorial" => serve_and_wait(TutorialServer::new()).await,
|
||||
_ => {
|
||||
tracing::warn!("Unknown MCP server name: {}", name);
|
||||
Err(anyhow!("Unknown MCP server name: {}", name))
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().replace(' ', "").as_str() {
|
||||
"autovisualiser" => Ok(McpCommand::AutoVisualiser),
|
||||
"computercontroller" => Ok(McpCommand::ComputerController),
|
||||
"developer" => Ok(McpCommand::Developer),
|
||||
"memory" => Ok(McpCommand::Memory),
|
||||
"tutorial" => Ok(McpCommand::Tutorial),
|
||||
_ => Err(format!("Invalid command: {}", s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to run any MCP server with common error handling
|
||||
async fn serve_and_wait<S>(server: S) -> Result<()>
|
||||
impl McpCommand {
|
||||
pub fn name(&self) -> &str {
|
||||
match self {
|
||||
McpCommand::AutoVisualiser => "autovisualiser",
|
||||
McpCommand::ComputerController => "computercontroller",
|
||||
McpCommand::Developer => "developer",
|
||||
McpCommand::Memory => "memory",
|
||||
McpCommand::Tutorial => "tutorial",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn serve<S>(server: S) -> Result<()>
|
||||
where
|
||||
S: rmcp::ServerHandler,
|
||||
{
|
||||
|
||||
@@ -7,6 +7,11 @@ mod routes;
|
||||
mod state;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use goose::config::paths::Paths;
|
||||
use goose_mcp::{
|
||||
mcp_server_runner::{serve, McpCommand},
|
||||
AutoVisualiserRouter, ComputerControllerServer, DeveloperServer, MemoryServer, TutorialServer,
|
||||
};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
@@ -22,8 +27,8 @@ enum Commands {
|
||||
Agent,
|
||||
/// Run the MCP server
|
||||
Mcp {
|
||||
/// Name of the MCP server type
|
||||
name: String,
|
||||
#[arg(value_parser = clap::value_parser!(McpCommand))]
|
||||
server: McpCommand,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -31,13 +36,27 @@ enum Commands {
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match &cli.command {
|
||||
match cli.command {
|
||||
Commands::Agent => {
|
||||
commands::agent::run().await?;
|
||||
}
|
||||
Commands::Mcp { name } => {
|
||||
logging::setup_logging(Some(&format!("mcp-{name}")))?;
|
||||
goose_mcp::mcp_server_runner::run_mcp_server(name).await?;
|
||||
Commands::Mcp { server } => {
|
||||
logging::setup_logging(Some(&format!("mcp-{}", server.name())))?;
|
||||
match server {
|
||||
McpCommand::AutoVisualiser => serve(AutoVisualiserRouter::new()).await?,
|
||||
McpCommand::ComputerController => serve(ComputerControllerServer::new()).await?,
|
||||
McpCommand::Memory => serve(MemoryServer::new()).await?,
|
||||
McpCommand::Tutorial => serve(TutorialServer::new()).await?,
|
||||
McpCommand::Developer => {
|
||||
let bash_env = Paths::config_dir().join(".bash_env");
|
||||
serve(
|
||||
DeveloperServer::new()
|
||||
.extend_path_with_shell(true)
|
||||
.bash_env_file(Some(bash_env)),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user