mirror of
https://github.com/block/goose.git
synced 2026-04-28 03:29:36 +00:00
fix: extension command with quotes in cli (#8150)
This commit is contained in:
parent
88c06ab5dc
commit
98bab02aa5
3 changed files with 72 additions and 41 deletions
|
|
@ -1106,9 +1106,13 @@ fn configure_stdio_extension() -> anyhow::Result<()> {
|
|||
|
||||
let timeout = prompt_extension_timeout()?;
|
||||
|
||||
let mut parts = command_str.split_whitespace();
|
||||
let cmd = parts.next().unwrap_or("").to_string();
|
||||
let args: Vec<String> = parts.map(String::from).collect();
|
||||
let mut parts = crate::session::split_quoted(&command_str)?;
|
||||
let cmd = if parts.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
parts.remove(0)
|
||||
};
|
||||
let args = parts;
|
||||
|
||||
let description = prompt_extension_description()?;
|
||||
let (envs, env_keys) = collect_env_vars()?;
|
||||
|
|
|
|||
|
|
@ -231,6 +231,36 @@ pub async fn classify_planner_response(
|
|||
}
|
||||
}
|
||||
|
||||
pub fn split_quoted(input: &str) -> Result<Vec<String>> {
|
||||
let mut parts = Vec::new();
|
||||
let mut current = String::new();
|
||||
let mut in_double_quote = false;
|
||||
let mut in_single_quote = false;
|
||||
|
||||
for c in input.chars() {
|
||||
match c {
|
||||
'"' if !in_single_quote => in_double_quote = !in_double_quote,
|
||||
'\'' if !in_double_quote => in_single_quote = !in_single_quote,
|
||||
c if c.is_whitespace() && !in_double_quote && !in_single_quote => {
|
||||
if !current.is_empty() {
|
||||
parts.push(std::mem::take(&mut current));
|
||||
}
|
||||
}
|
||||
_ => current.push(c),
|
||||
}
|
||||
}
|
||||
|
||||
if in_double_quote || in_single_quote {
|
||||
return Err(anyhow::anyhow!("Unmatched quote in command"));
|
||||
}
|
||||
|
||||
if !current.is_empty() {
|
||||
parts.push(current);
|
||||
}
|
||||
|
||||
Ok(parts)
|
||||
}
|
||||
|
||||
impl CliSession {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn new(
|
||||
|
|
@ -273,7 +303,7 @@ impl CliSession {
|
|||
/// Parse a stdio extension command string into an ExtensionConfig
|
||||
/// Format: "ENV1=val1 ENV2=val2 command args..."
|
||||
pub fn parse_stdio_extension(extension_command: &str) -> Result<ExtensionConfig> {
|
||||
let mut parts: Vec<&str> = extension_command.split_whitespace().collect();
|
||||
let mut parts = split_quoted(extension_command)?;
|
||||
let mut envs = HashMap::new();
|
||||
|
||||
while let Some(part) = parts.first() {
|
||||
|
|
@ -289,7 +319,7 @@ impl CliSession {
|
|||
return Err(anyhow::anyhow!("No command provided in extension string"));
|
||||
}
|
||||
|
||||
let cmd = parts.remove(0).to_string();
|
||||
let cmd = parts.remove(0);
|
||||
let name = std::path::Path::new(&cmd)
|
||||
.file_name()
|
||||
.and_then(|f| f.to_str())
|
||||
|
|
@ -299,7 +329,7 @@ impl CliSession {
|
|||
Ok(ExtensionConfig::Stdio {
|
||||
name,
|
||||
cmd,
|
||||
args: parts.iter().map(|s| s.to_string()).collect(),
|
||||
args: parts,
|
||||
envs: Envs::new(envs),
|
||||
env_keys: Vec::new(),
|
||||
description: goose::config::DEFAULT_EXTENSION_DESCRIPTION.to_string(),
|
||||
|
|
@ -2014,6 +2044,21 @@ mod tests {
|
|||
}
|
||||
; "env_prefix_name_from_cmd"
|
||||
)]
|
||||
#[test_case(
|
||||
r#""/Applications/IntelliJ IDEA.app/Contents/jbr/Contents/Home/bin/java" -classpath "/path/with spaces/lib.jar" Main"#,
|
||||
ExtensionConfig::Stdio {
|
||||
name: "java".into(),
|
||||
cmd: "/Applications/IntelliJ IDEA.app/Contents/jbr/Contents/Home/bin/java".into(),
|
||||
args: vec!["-classpath".into(), "/path/with spaces/lib.jar".into(), "Main".into()],
|
||||
envs: Envs::default(),
|
||||
env_keys: vec![],
|
||||
description: goose::config::DEFAULT_EXTENSION_DESCRIPTION.to_string(),
|
||||
timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT),
|
||||
bundled: None,
|
||||
available_tools: vec![],
|
||||
}
|
||||
; "quoted_path_with_spaces"
|
||||
)]
|
||||
fn test_parse_stdio_extension(input: &str, expected: ExtensionConfig) {
|
||||
assert_eq!(CliSession::parse_stdio_extension(input).unwrap(), expected);
|
||||
}
|
||||
|
|
@ -2023,6 +2068,23 @@ mod tests {
|
|||
assert!(CliSession::parse_stdio_extension("").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_quoted_windows_paths() {
|
||||
assert_eq!(
|
||||
split_quoted(r"C:\tools\mcp.exe --arg value").unwrap(),
|
||||
vec![r"C:\tools\mcp.exe", "--arg", "value"]
|
||||
);
|
||||
assert_eq!(
|
||||
split_quoted(r#""C:\Program Files\server\mcp.exe" --arg"#).unwrap(),
|
||||
vec![r"C:\Program Files\server\mcp.exe", "--arg"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_quoted_unmatched_quote() {
|
||||
assert!(split_quoted(r#""unmatched"#).is_err());
|
||||
}
|
||||
|
||||
#[test_case(
|
||||
"https://mcp.kiwi.com", 300,
|
||||
ExtensionConfig::StreamableHttp {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ This tutorial covers how to add the JetBrains extension to integrate with any Je
|
|||
|
||||
Versions 2025.2 and later have built-in MCP server support and generate a dynamic configuration specific to your IDE instance. See your IDE's documentation for more details (e.g. [MCP Server](https://www.jetbrains.com/help/idea/mcp-server.html) for IntelliJ IDEA).
|
||||
|
||||
<!-- hide until parsing bugs for paths with spaces are fixed in Desktop and CLI
|
||||
:::tip Quick Install
|
||||
<Tabs groupId="interface">
|
||||
<TabItem value="ui" label="goose Desktop" default>
|
||||
|
|
@ -34,7 +33,6 @@ This tutorial covers how to add the JetBrains extension to integrate with any Je
|
|||
</TabItem>
|
||||
</Tabs>
|
||||
:::
|
||||
-->
|
||||
|
||||
<br/>
|
||||
Configure the extension using your IDE's built-in MCP server support:
|
||||
|
|
@ -49,10 +47,6 @@ This tutorial covers how to add the JetBrains extension to integrate with any Je
|
|||
|
||||
2. Add the JetBrains extension to goose using the command from the config:
|
||||
|
||||
:::info
|
||||
If the goose Desktop or goose CLI configuration steps aren't successful, follow the `Config File` steps.
|
||||
:::
|
||||
|
||||
<Tabs groupId="interface">
|
||||
<TabItem value="ui" label="goose Desktop" default>
|
||||
1. Click the <PanelLeft className="inline" size={16} /> button in the top-left to open the sidebar
|
||||
|
|
@ -82,35 +76,6 @@ This tutorial covers how to add the JetBrains extension to integrate with any Je
|
|||
}
|
||||
/>
|
||||
</TabItem>
|
||||
<TabItem value="config" label="Config File">
|
||||
1. Open your goose [`config.yaml`](/docs/guides/config-files) file
|
||||
2. In the `extensions` section, add an entry that uses your IDE's Stdio config, for example:
|
||||
|
||||
```yaml
|
||||
extensions:
|
||||
jetbrains:
|
||||
enabled: true
|
||||
type: stdio
|
||||
name: JetBrains
|
||||
description: Integrate goose with any JetBrains IDE
|
||||
cmd: /Applications/IntelliJ IDEA.app/Contents/jbr/Contents/Home/bin/java
|
||||
args:
|
||||
- -classpath
|
||||
- /Applications/IntelliJ IDEA.app/Contents/plugins/mcpserver/lib/mcpserver-frontend.jar:/Applications/IntelliJ IDEA.app/Contents/lib/util-8.jar
|
||||
- com.intellij.mcpserver.stdio.McpStdioRunnerKt
|
||||
envs:
|
||||
IJ_MCP_SERVER_PORT: "63342"
|
||||
env_keys:
|
||||
- IJ_MCP_SERVER_PORT
|
||||
timeout: 300
|
||||
bundled: null
|
||||
available_tools: []
|
||||
```
|
||||
|
||||
Make sure to:
|
||||
- Replace the `cmd` and `args` values in the example to match your JetBrains IDE installation
|
||||
- Update `IJ_MCP_SERVER_PORT` to match your IDE's MCP server port
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
</TabItem>
|
||||
<TabItem value="earlier" label="2025.1 and earlier">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue