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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ description = "An AI agent"

[workspace.lints.clippy]
uninlined_format_args = "allow"
string_slice = "warn"

[workspace.dependencies]
rmcp = { version = "0.8.3", features = ["schemars", "auth"] }
Expand Down
4 changes: 2 additions & 2 deletions crates/goose-cli/src/session/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ impl GooseCompleter {
/// Complete prompt names for the /prompt command
fn complete_prompt_names(&self, line: &str) -> Result<(usize, Vec<Pair>)> {
// Get the prefix of the prompt name being typed
let prefix = if line.len() > 8 { &line[8..] } else { "" };
let prefix = line.get(8..).unwrap_or("");

// Get available prompts from cache
let cache = self.completion_cache.read().unwrap();
Expand Down Expand Up @@ -156,7 +156,7 @@ impl GooseCompleter {

/// Complete argument keys for a specific prompt
fn complete_argument_keys(&self, line: &str) -> Result<(usize, Vec<Pair>)> {
let parts: Vec<&str> = line[8..].split_whitespace().collect();
let parts: Vec<&str> = line.get(8..).unwrap_or("").split_whitespace().collect();

// We need at least the prompt name
if parts.is_empty() {
Expand Down
18 changes: 10 additions & 8 deletions crates/goose-cli/src/session/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,15 +169,17 @@ fn handle_slash_command(input: &str) -> Option<InputResult> {
}
}
s if s.starts_with(CMD_EXTENSION) => Some(InputResult::AddExtension(
s[CMD_EXTENSION.len()..].to_string(),
s.get(CMD_EXTENSION.len()..).unwrap_or("").to_string(),
)),
s if s.starts_with(CMD_BUILTIN) => {
Some(InputResult::AddBuiltin(s[CMD_BUILTIN.len()..].to_string()))
}
s if s.starts_with(CMD_MODE) => {
Some(InputResult::GooseMode(s[CMD_MODE.len()..].to_string()))
s if s.starts_with(CMD_BUILTIN) => Some(InputResult::AddBuiltin(
s.get(CMD_BUILTIN.len()..).unwrap_or("").to_string(),
)),
s if s.starts_with(CMD_MODE) => Some(InputResult::GooseMode(
s.get(CMD_MODE.len()..).unwrap_or("").to_string(),
)),
s if s.starts_with(CMD_PLAN) => {
parse_plan_command(s.get(CMD_PLAN.len()..).unwrap_or("").trim().to_string())
}
s if s.starts_with(CMD_PLAN) => parse_plan_command(s[CMD_PLAN.len()..].trim().to_string()),
s if s == CMD_ENDPLAN => Some(InputResult::EndPlan),
s if s == CMD_CLEAR => Some(InputResult::Clear),
s if s.starts_with(CMD_RECIPE) => parse_recipe_command(s),
Expand All @@ -199,7 +201,7 @@ fn parse_recipe_command(s: &str) -> Option<InputResult> {
}

// Extract the filepath from the command
let filepath = s[CMD_RECIPE.len()..].trim();
let filepath = s.get(CMD_RECIPE.len()..).unwrap_or("").trim();

if filepath.is_empty() {
return Some(InputResult::Recipe(None));
Expand Down
18 changes: 6 additions & 12 deletions crates/goose-mcp/src/developer/analyze/formatter.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use rmcp::model::{Content, Role};
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};

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

pub struct Formatter;

Expand Down Expand Up @@ -164,13 +164,7 @@ impl Formatter {
if imports.len() > 1 {
format!("{}({})", group, imports.len())
} else {
// For single imports, show more detail
let imp = &imports[0];
if imp.len() > 40 {
format!("{}...", &imp[..37])
} else {
imp.clone()
}
safe_truncate(&imports[0], 40)
}
})
.collect();
Expand Down Expand Up @@ -727,7 +721,7 @@ impl Formatter {
if let Some(header_line) = output
.lines()
.rev()
.find(|l| l.starts_with("##") && line.contains(&l[3..]))
.find(|l| l.starts_with("##") && l.get(3..).is_some_and(|s| line.contains(s)))
{
if !filtered.contains(header_line) {
filtered.push_str(header_line);
Expand Down
2 changes: 1 addition & 1 deletion crates/goose-mcp/src/developer/analyze/languages/go.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ pub fn find_method_for_receiver(
for i in 0..parent.child_count() {
if let Some(child) = parent.child(i) {
if child.kind() == "field_identifier" {
return Some(source[child.byte_range()].to_string());
return source.get(child.byte_range()).map(|s| s.to_string());
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/goose-mcp/src/developer/analyze/languages/ruby.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ fn find_method_in_body_with_depth(
for j in 0..child.child_count() {
if let Some(name_node) = child.child(j) {
if name_node.kind() == "identifier" {
return Some(source[name_node.byte_range()].to_string());
return source.get(name_node.byte_range()).map(|s| s.to_string());
}
}
}
Expand Down
8 changes: 5 additions & 3 deletions crates/goose-mcp/src/developer/analyze/languages/rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ pub fn extract_function_name_for_kind(
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if child.kind() == "type_identifier" {
return Some(format!("impl {}", &source[child.byte_range()]));
return source
.get(child.byte_range())
.map(|s| format!("impl {}", s));
}
}
}
Expand All @@ -109,7 +111,7 @@ pub fn find_method_for_receiver(
for i in 0..parent.child_count() {
if let Some(child) = parent.child(i) {
if child.kind() == "identifier" {
return Some(source[child.byte_range()].to_string());
return source.get(child.byte_range()).map(|s| s.to_string());
}
}
}
Expand All @@ -133,7 +135,7 @@ pub fn find_receiver_type(node: &tree_sitter::Node, source: &str) -> Option<Stri
for i in 0..parent.child_count() {
if let Some(child) = parent.child(i) {
if child.kind() == "type_identifier" {
return Some(source[child.byte_range()].to_string());
return source.get(child.byte_range()).map(|s| s.to_string());
}
}
}
Expand Down
50 changes: 35 additions & 15 deletions crates/goose-mcp/src/developer/analyze/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ impl ElementExtractor {
source: &str,
kinds: &[&str],
) -> Option<String> {
Self::find_child_by_kind(node, kinds).map(|child| source[child.byte_range()].to_string())
Self::find_child_by_kind(node, kinds)
.and_then(|child| source.get(child.byte_range()).map(|s| s.to_string()))
}

pub fn extract_with_depth(
Expand Down Expand Up @@ -216,8 +217,13 @@ impl ElementExtractor {
for match_ in matches.by_ref() {
for capture in match_.captures {
let node = capture.node;
let text = &source[node.byte_range()];
let line = source[..node.start_byte()].lines().count() + 1;
let Some(text) = source.get(node.byte_range()) else {
continue;
};
let line = source
.get(..node.start_byte())
.map(|s| s.lines().count() + 1)
.unwrap_or(1);

match query.capture_names()[capture.index as usize] {
"func" | "const" => {
Expand Down Expand Up @@ -284,18 +290,25 @@ impl ElementExtractor {
for match_ in matches.by_ref() {
for capture in match_.captures {
let node = capture.node;
let text = &source[node.byte_range()];
let Some(text) = source.get(node.byte_range()) else {
continue;
};
let start_pos = node.start_position();

let line_start = source[..node.start_byte()]
.rfind('\n')
let line_start = source
.get(..node.start_byte())
.and_then(|s| s.rfind('\n'))
.map(|i| i + 1)
.unwrap_or(0);
let line_end = source[node.end_byte()..]
.find('\n')
let line_end = source
.get(node.end_byte()..)
.and_then(|s| s.find('\n'))
.map(|i| node.end_byte() + i)
.unwrap_or(source.len());
let context = source[line_start..line_end].trim().to_string();
let context = source
.get(line_start..line_end)
.map(|s| s.trim().to_string())
.unwrap_or_default();

let caller_name = Self::find_containing_function(&node, source, language);

Expand Down Expand Up @@ -356,18 +369,25 @@ impl ElementExtractor {
for match_ in matches.by_ref() {
for capture in match_.captures {
let node = capture.node;
let text = &source[node.byte_range()];
let Some(text) = source.get(node.byte_range()) else {
continue;
};
let start_pos = node.start_position();

let line_start = source[..node.start_byte()]
.rfind('\n')
let line_start = source
.get(..node.start_byte())
.and_then(|s| s.rfind('\n'))
.map(|i| i + 1)
.unwrap_or(0);
let line_end = source[node.end_byte()..]
.find('\n')
let line_end = source
.get(node.end_byte()..)
.and_then(|s| s.find('\n'))
.map(|i| node.end_byte() + i)
.unwrap_or(source.len());
let context = source[line_start..line_end].trim().to_string();
let context = source
.get(line_start..line_end)
.map(|s| s.trim().to_string())
.unwrap_or_default();

let capture_name = query.capture_names()[capture.index as usize];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ impl MorphLLMEditor {
if let (Some(start_pos), Some(end_pos)) = (text.find(&start_tag), text.find(&end_tag)) {
if start_pos < end_pos {
let content_start = start_pos + start_tag.len();
let content = &text[content_start..end_pos];
return Some(content.trim().to_string());
if let Some(content) = text.get(content_start..end_pos) {
return Some(content.trim().to_string());
}
}
}
None
Expand Down
27 changes: 16 additions & 11 deletions crates/goose-mcp/src/developer/rmcp_developer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1335,20 +1335,22 @@ impl DeveloperServer {

// Find the last space before AM/PM and replace it with U+202F
let space_pos = filename.rfind(meridian)
.map(|pos| filename[..pos].trim_end().len())
.and_then(|pos| filename.get(..pos).map(|s| s.trim_end().len()))
.unwrap_or(0);

if space_pos > 0 {
let parent = path.parent().unwrap_or(Path::new(""));
let new_filename = format!(
"{}{}{}",
&filename[..space_pos],
'\u{202F}',
&filename[space_pos+1..]
);
let new_path = parent.join(new_filename);

return new_path;
if let (Some(before), Some(after)) = (filename.get(..space_pos), filename.get(space_pos+1..)) {
let new_filename = format!(
"{}{}{}",
before,
'\u{202F}',
after
);
let new_path = parent.join(new_filename);

return new_path;
}
}
}
}
Expand Down Expand Up @@ -3208,7 +3210,10 @@ mod tests {
) {
let start_idx = start + start_tag.len();
if start_idx < end {
let path = assistant_content.text[start_idx..end].trim();
let Some(path) = assistant_content.text.get(start_idx..end).map(|s| s.trim())
else {
panic!("Failed to extract path from assistant content");
};
println!("Extracted path: {}", path);

let file_contents =
Expand Down
2 changes: 1 addition & 1 deletion crates/goose-test/src/mcp/stdio/playback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ struct LogEntry {
fn parse_log_line(line: &str) -> Option<LogEntry> {
line.find(": ").and_then(|pos| {
let (prefix, content) = line.split_at(pos);
let content = &content[2..]; // Skip ": "
let content = content.get(2..)?; // Skip ": "

let stream_type = match prefix {
"STDIN" => StreamType::Stdin,
Expand Down
8 changes: 0 additions & 8 deletions crates/goose/src/agents/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1365,17 +1365,9 @@ impl Agent {
.unwrap_or(&content)
.trim()
.to_string();
tracing::debug!(
"Cleaned content for parsing: {}",
&clean_content[..std::cmp::min(200, clean_content.len())]
);

// try to parse json response from the LLM
tracing::debug!("Attempting to parse recipe content as JSON");
let (instructions, activities) =
if let Ok(json_content) = serde_json::from_str::<Value>(&clean_content) {
tracing::debug!("Successfully parsed JSON content");

let instructions = json_content
.get("instructions")
.ok_or_else(|| anyhow!("Missing 'instructions' in json response"))?
Expand Down
2 changes: 1 addition & 1 deletion crates/goose/src/agents/subagent_execution_tool/tasks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ fn extract_json_from_line(line: &str) -> Option<String> {
return None;
}

let potential_json = &line[start..=end];
let potential_json = line.get(start..=end)?;
if serde_json::from_str::<Value>(potential_json).is_ok() {
Some(potential_json.to_string())
} else {
Expand Down
27 changes: 2 additions & 25 deletions crates/goose/src/providers/claude_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use tokio::process::Command;

use super::base::{ConfigKey, Provider, ProviderMetadata, ProviderUsage, Usage};
use super::errors::ProviderError;
use super::utils::RequestLog;
use super::utils::{filter_extensions_from_system_prompt, RequestLog};
use crate::config::{Config, GooseMode};
use crate::conversation::message::{Message, MessageContent};
use crate::model::ModelConfig;
Expand Down Expand Up @@ -103,28 +103,6 @@ impl ClaudeCodeProvider {
None
}

/// Filter out the Extensions section from the system prompt
fn filter_extensions_from_system_prompt(&self, system: &str) -> String {
// Find the Extensions section and remove it
if let Some(extensions_start) = system.find("# Extensions") {
// Look for the next major section that starts with #
let after_extensions = &system[extensions_start..];
if let Some(next_section_pos) = after_extensions[1..].find("\n# ") {
// Found next section, keep everything before Extensions and after the next section
let before_extensions = &system[..extensions_start];
let next_section_start = extensions_start + next_section_pos + 1;
let after_next_section = &system[next_section_start..];
format!("{}{}", before_extensions.trim_end(), after_next_section)
} else {
// No next section found, just remove everything from Extensions onward
system[..extensions_start].trim_end().to_string()
}
} else {
// No Extensions section found, return original
system.to_string()
}
}

/// Convert goose messages to the format expected by claude CLI
fn messages_to_claude_format(&self, _system: &str, messages: &[Message]) -> Result<Value> {
let mut claude_messages = Vec::new();
Expand Down Expand Up @@ -303,8 +281,7 @@ impl ClaudeCodeProvider {
ProviderError::RequestFailed(format!("Failed to format messages: {}", e))
})?;

// Create a filtered system prompt without Extensions section
let filtered_system = self.filter_extensions_from_system_prompt(system);
let filtered_system = filter_extensions_from_system_prompt(system);

if std::env::var("GOOSE_CLAUDE_CODE_DEBUG").is_ok() {
println!("=== CLAUDE CODE PROVIDER DEBUG ===");
Expand Down
Loading