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
2 changes: 1 addition & 1 deletion crates/goose-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1006,7 +1006,7 @@ pub async fn cli() -> Result<()> {
.unwrap_or(&recipe_name);

let recipe_version =
crate::recipes::search_recipe::retrieve_recipe_file(&recipe_name)
crate::recipes::search_recipe::load_recipe_file(&recipe_name)
.ok()
.and_then(|rf| {
goose::recipe::template_recipe::parse_recipe_content(
Expand Down
4 changes: 2 additions & 2 deletions crates/goose-cli/src/recipes/extract_from_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use goose::recipe::SubRecipe;

use crate::recipes::print_recipe::print_recipe_info;
use crate::recipes::recipe::load_recipe;
use crate::recipes::search_recipe::retrieve_recipe_file;
use crate::recipes::search_recipe::load_recipe_file;
use crate::{
cli::{InputConfig, RecipeInfo},
session::SessionSettings,
Expand All @@ -24,7 +24,7 @@ pub fn extract_recipe_info_from_cli(
let mut all_sub_recipes = recipe.sub_recipes.clone().unwrap_or_default();
if !additional_sub_recipes.is_empty() {
for sub_recipe_name in additional_sub_recipes {
match retrieve_recipe_file(&sub_recipe_name) {
match load_recipe_file(&sub_recipe_name) {
Ok(recipe_file) => {
let name = extract_recipe_name(&sub_recipe_name);
let recipe_file_path = recipe_file.file_path;
Expand Down
2 changes: 1 addition & 1 deletion crates/goose-cli/src/recipes/github_recipe.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use anyhow::{anyhow, Result};
use console::style;
use goose::recipe::template_recipe::parse_recipe_content;
use goose::recipe::RECIPE_FILE_EXTENSIONS;
use serde::{Deserialize, Serialize};

use crate::recipes::recipe::RECIPE_FILE_EXTENSIONS;
use goose::recipe::read_recipe_file_content::RecipeFile;
use std::env;
use std::fs;
Expand Down
8 changes: 3 additions & 5 deletions crates/goose-cli/src/recipes/recipe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::recipes::print_recipe::{
missing_parameters_command_line, print_recipe_explanation,
print_required_parameters_for_template,
};
use crate::recipes::search_recipe::retrieve_recipe_file;
use crate::recipes::search_recipe::load_recipe_file;
use crate::recipes::secret_discovery::{discover_recipe_secrets, SecretRequirement};
use anyhow::Result;
use goose::config::Config;
Expand All @@ -15,8 +15,6 @@ use goose::recipe::Recipe;
use serde_json::Value;
use std::collections::HashMap;

pub const RECIPE_FILE_EXTENSIONS: &[&str] = &["yaml", "json"];

fn create_user_prompt_callback() -> impl Fn(&str, &str) -> Result<String> {
|key: &str, description: &str| -> Result<String> {
let input_value =
Expand All @@ -26,7 +24,7 @@ fn create_user_prompt_callback() -> impl Fn(&str, &str) -> Result<String> {
}

fn load_recipe_file_with_dir(recipe_name: &str) -> Result<(RecipeFile, String)> {
let recipe_file = retrieve_recipe_file(recipe_name)?;
let recipe_file = load_recipe_file(recipe_name)?;
let recipe_dir_str = recipe_file
.parent_dir
.to_str()
Expand All @@ -36,7 +34,7 @@ fn load_recipe_file_with_dir(recipe_name: &str) -> Result<(RecipeFile, String)>
}

pub fn load_recipe(recipe_name: &str, params: Vec<(String, String)>) -> Result<Recipe> {
let recipe_file = retrieve_recipe_file(recipe_name)?;
let recipe_file = load_recipe_file(recipe_name)?;
match build_recipe_from_template(recipe_file, params, Some(create_user_prompt_callback())) {
Ok(recipe) => {
let secret_requirements = discover_recipe_secrets(&recipe);
Expand Down
176 changes: 21 additions & 155 deletions crates/goose-cli/src/recipes/search_recipe.rs
Original file line number Diff line number Diff line change
@@ -1,35 +1,15 @@
use anyhow::{anyhow, Result};
use anyhow::Result;
use goose::config::Config;
use goose::recipe::read_recipe_file_content::{read_recipe_file, RecipeFile};
use goose::recipe::template_recipe::parse_recipe_content;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};

use crate::recipes::recipe::RECIPE_FILE_EXTENSIONS;
use goose::recipe::read_recipe_file_content::RecipeFile;

use super::github_recipe::{
list_github_recipes, retrieve_recipe_from_github, RecipeInfo, RecipeSource,
GOOSE_RECIPE_GITHUB_REPO_CONFIG_KEY,
};
use goose::recipe::local_recipes::{list_local_recipes, load_local_recipe_file};

const GOOSE_RECIPE_PATH_ENV_VAR: &str = "GOOSE_RECIPE_PATH";

pub fn retrieve_recipe_file(recipe_name: &str) -> Result<RecipeFile> {
if RECIPE_FILE_EXTENSIONS
.iter()
.any(|ext| recipe_name.ends_with(&format!(".{}", ext)))
{
let path = PathBuf::from(recipe_name);
return read_recipe_file(path);
}
if is_file_path(recipe_name) || is_file_name(recipe_name) {
return Err(anyhow!(
"Recipe file {} is not a json or yaml file",
recipe_name
));
}
retrieve_recipe_from_local_path(recipe_name).or_else(|e| {
pub fn load_recipe_file(recipe_name: &str) -> Result<RecipeFile> {
load_local_recipe_file(recipe_name).or_else(|e| {
if let Some(recipe_repo_full_name) = configured_github_recipe_repo() {
retrieve_recipe_from_github(recipe_name, &recipe_repo_full_name)
} else {
Expand All @@ -38,60 +18,6 @@ pub fn retrieve_recipe_file(recipe_name: &str) -> Result<RecipeFile> {
})
}

fn is_file_path(recipe_name: &str) -> bool {
recipe_name.contains('/')
|| recipe_name.contains('\\')
|| recipe_name.starts_with('~')
|| recipe_name.starts_with('.')
}

fn is_file_name(recipe_name: &str) -> bool {
Path::new(recipe_name).extension().is_some()
}

fn read_recipe_in_dir(dir: &Path, recipe_name: &str) -> Result<RecipeFile> {
for ext in RECIPE_FILE_EXTENSIONS {
let recipe_path = dir.join(format!("{}.{}", recipe_name, ext));
if let Ok(result) = read_recipe_file(recipe_path) {
return Ok(result);
}
}
Err(anyhow!(format!(
"No {}.yaml or {}.json recipe file found in directory: {}",
recipe_name,
recipe_name,
dir.display()
)))
}

fn retrieve_recipe_from_local_path(recipe_name: &str) -> Result<RecipeFile> {
let mut search_dirs = vec![PathBuf::from(".")];
if let Ok(recipe_path_env) = env::var(GOOSE_RECIPE_PATH_ENV_VAR) {
let path_separator = if cfg!(windows) { ';' } else { ':' };
let recipe_path_env_dirs: Vec<PathBuf> = recipe_path_env
.split(path_separator)
.map(PathBuf::from)
.collect();
search_dirs.extend(recipe_path_env_dirs);
}
for dir in &search_dirs {
if let Ok(result) = read_recipe_in_dir(dir, recipe_name) {
return Ok(result);
}
}
let search_dirs_str = search_dirs
.iter()
.map(|p| p.to_string_lossy())
.collect::<Vec<_>>()
.join(":");
Err(anyhow!(
"ℹ️ Failed to retrieve {}.yaml or {}.json in {}",
recipe_name,
recipe_name,
search_dirs_str
))
}

fn configured_github_recipe_repo() -> Option<String> {
let config = Config::global();
match config.get_param(GOOSE_RECIPE_GITHUB_REPO_CONFIG_KEY) {
Expand All @@ -105,8 +31,22 @@ pub fn list_available_recipes() -> Result<Vec<RecipeInfo>> {
let mut recipes = Vec::new();

// Search local recipes
if let Ok(local_recipes) = discover_local_recipes() {
recipes.extend(local_recipes);
if let Ok(local_recipes) = list_local_recipes() {
recipes.extend(local_recipes.into_iter().map(|(path, recipe)| {
let name = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown")
.to_string();

RecipeInfo {
name,
source: RecipeSource::Local,
path: path.to_string_lossy().to_string(),
title: Some(recipe.title),
description: Some(recipe.description),
}
}));
}

// Search GitHub recipes if configured
Expand All @@ -118,77 +58,3 @@ pub fn list_available_recipes() -> Result<Vec<RecipeInfo>> {

Ok(recipes)
}

fn discover_local_recipes() -> Result<Vec<RecipeInfo>> {
let mut recipes = Vec::new();
let mut search_dirs = vec![PathBuf::from(".")];

// Add GOOSE_RECIPE_PATH directories
if let Ok(recipe_path_env) = env::var(GOOSE_RECIPE_PATH_ENV_VAR) {
let path_separator = if cfg!(windows) { ';' } else { ':' };
let recipe_path_env_dirs: Vec<PathBuf> = recipe_path_env
.split(path_separator)
.map(PathBuf::from)
.collect();
search_dirs.extend(recipe_path_env_dirs);
}

for dir in search_dirs {
if let Ok(dir_recipes) = scan_directory_for_recipes(&dir) {
recipes.extend(dir_recipes);
}
}

Ok(recipes)
}

fn scan_directory_for_recipes(dir: &Path) -> Result<Vec<RecipeInfo>> {
let mut recipes = Vec::new();

if !dir.exists() || !dir.is_dir() {
return Ok(recipes);
}

for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();

if path.is_file() {
if let Some(extension) = path.extension() {
if RECIPE_FILE_EXTENSIONS.contains(&extension.to_string_lossy().as_ref()) {
if let Ok(recipe_info) = create_local_recipe_info(&path) {
recipes.push(recipe_info);
}
}
}
}
}

Ok(recipes)
}

fn create_local_recipe_info(path: &Path) -> Result<RecipeInfo> {
let content = fs::read_to_string(path)?;
let recipe_dir = path
.parent()
.unwrap_or_else(|| Path::new("."))
.to_string_lossy()
.to_string();
let (recipe, _) = parse_recipe_content(&content, recipe_dir)?;

let name = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown")
.to_string();

let path_str = path.to_string_lossy().to_string();

Ok(RecipeInfo {
name,
source: RecipeSource::Local,
path: path_str,
title: Some(recipe.title),
description: Some(recipe.description),
})
}
4 changes: 2 additions & 2 deletions crates/goose-cli/src/recipes/secret_discovery.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::recipes::search_recipe::retrieve_recipe_file;
use crate::recipes::search_recipe::load_recipe_file;
use goose::agents::extension::ExtensionConfig;
use goose::recipe::Recipe;
use std::collections::HashSet;
Expand Down Expand Up @@ -116,7 +116,7 @@ fn discover_recipe_secrets_recursive(
/// For secret discovery, we only need the recipe structure (extensions and env_keys),
/// not parameter-substituted content, so we parse the raw YAML directly for speed and robustness.
fn load_sub_recipe(recipe_path: &str) -> Result<Recipe, Box<dyn std::error::Error>> {
let recipe_file = retrieve_recipe_file(recipe_path)?;
let recipe_file = load_recipe_file(recipe_path)?;
let recipe: Recipe = serde_yaml::from_str(&recipe_file.content)?;
Ok(recipe)
}
Expand Down
4 changes: 2 additions & 2 deletions crates/goose-server/src/routes/recipe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::sync::Arc;

use axum::routing::get;
use axum::{extract::State, http::StatusCode, routing::post, Json, Router};
use goose::recipe::recipe_library;
use goose::recipe::local_recipes;
use goose::recipe::Recipe;
use goose::recipe_deeplink;
use goose::session::SessionManager;
Expand Down Expand Up @@ -324,7 +324,7 @@ async fn save_recipe(
None => None,
};

match recipe_library::save_recipe_to_file(request.recipe, request.is_global, file_path) {
match local_recipes::save_recipe_to_file(request.recipe, request.is_global, file_path) {
Ok(_) => Ok(StatusCode::NO_CONTENT),
Err(e) => Err(ErrorResponse {
message: e.to_string(),
Expand Down
4 changes: 2 additions & 2 deletions crates/goose-server/src/routes/recipe_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::path::PathBuf;

use anyhow::Result;

use goose::recipe::recipe_library::list_all_recipes_from_library;
use goose::recipe::local_recipes::list_local_recipes;
use goose::recipe::Recipe;

use std::path::Path;
Expand All @@ -29,7 +29,7 @@ fn short_id_from_path(path: &str) -> String {
}

pub fn get_all_recipes_manifests() -> Result<Vec<RecipeManifestWithPath>> {
let recipes_with_path = list_all_recipes_from_library()?;
let recipes_with_path = list_local_recipes()?;
let mut recipe_manifests_with_path = Vec::new();
for (file_path, recipe) in recipes_with_path {
let Ok(last_modified) = fs::metadata(file_path.clone())
Expand Down
Loading
Loading