Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7c0ebf9
applied server side call save recipe
lifeizhou-ap Oct 6, 2025
2388fa6
applied server side call parse recipe for importing recipe
lifeizhou-ap Oct 6, 2025
eb594ab
handle concurrency write file
lifeizhou-ap Oct 6, 2025
c14f079
reverted the change to persist mapping in file system
lifeizhou-ap Oct 6, 2025
ab63000
fixed ui tests
lifeizhou-ap Oct 6, 2025
0fb7312
Merge branch 'main' into lifei/use-server-side-save-import-recipe
lifeizhou-ap Oct 6, 2025
281e327
added save recipe payload validate and generate non duplication file
lifeizhou-ap Oct 7, 2025
3e0133b
removed recipe name, title and global input when saving recipe
lifeizhou-ap Oct 7, 2025
53396ef
removed unused functions
lifeizhou-ap Oct 7, 2025
ab07266
removed recipe name and is global on the server side when listing and…
lifeizhou-ap Oct 7, 2025
5ab4c55
update ui matching server side change
lifeizhou-ap Oct 7, 2025
780b84d
refactored recipe validation
lifeizhou-ap Oct 7, 2025
2f904bf
fixed error messages
lifeizhou-ap Oct 7, 2025
f9f8e97
Merge branch 'main' into lifei/use-server-side-save-import-recipe
lifeizhou-ap Oct 7, 2025
4ebddb7
fixed merge issue
lifeizhou-ap Oct 7, 2025
e09192c
added default value (empty string) for deserialization on extension …
lifeizhou-ap Oct 7, 2025
d68ab75
applied more validation on parse and save recipe
lifeizhou-ap Oct 7, 2025
5a46123
removed recipeStorage
lifeizhou-ap Oct 7, 2025
6a2cfb2
deleted tests
lifeizhou-ap Oct 7, 2025
91510ee
used recipeExtensionConfigInternal deserializer
lifeizhou-ap Oct 8, 2025
726f675
keep the recipe extensions even they are not defined in the config
lifeizhou-ap Oct 8, 2025
5b0399f
clean up error message and make the adapter concise
lifeizhou-ap Oct 8, 2025
2ab1a90
added validation and show error message
lifeizhou-ap Oct 9, 2025
f42e8e2
fixed test
lifeizhou-ap Oct 9, 2025
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
24 changes: 18 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/goose-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1019,7 +1019,7 @@ pub async fn cli() -> Result<()> {
.and_then(|rf| {
goose::recipe::template_recipe::parse_recipe_content(
&rf.content,
rf.parent_dir.to_string_lossy().to_string(),
Some(rf.parent_dir.to_string_lossy().to_string()),
)
.ok()
.map(|(r, _)| r.version)
Expand Down
104 changes: 14 additions & 90 deletions crates/goose-cli/src/commands/recipe.rs
Original file line number Diff line number Diff line change
@@ -1,43 +1,25 @@
use anyhow::Result;
use console::style;
use goose::recipe::validate_recipe::validate_recipe_template_from_file;

use crate::recipes::github_recipe::RecipeSource;
use crate::recipes::recipe::load_recipe_for_validation;
use crate::recipes::search_recipe::list_available_recipes;
use crate::recipes::search_recipe::{list_available_recipes, load_recipe_file};
use goose::recipe_deeplink;

/// Validates a recipe file
///
/// # Arguments
///
/// * `file_path` - Path to the recipe file to validate
///
/// # Returns
///
/// Result indicating success or failure
pub fn handle_validate(recipe_name: &str) -> Result<()> {
// Load and validate the recipe file
match load_recipe_for_validation(recipe_name) {
Ok(_) => {
println!("{} recipe file is valid", style("✓").green().bold());
Ok(())
}
Err(err) => {
println!("{} {}", style("✗").red().bold(), err);
Err(err)
}
}
let recipe_file = load_recipe_file(recipe_name)?;
validate_recipe_template_from_file(&recipe_file).map_err(|err| {
anyhow::anyhow!(
"{} recipe file is invalid: {}",
style("✗").red().bold(),
err
)
})?;
println!("{} recipe file is valid", style("✓").green().bold());
Ok(())
}

/// Generates a deeplink for a recipe file
///
/// # Arguments
///
/// * `recipe_name` - Path to the recipe file
///
/// # Returns
///
/// Result indicating success or failure
pub fn handle_deeplink(recipe_name: &str) -> Result<String> {
match generate_deeplink(recipe_name) {
Ok((deeplink_url, recipe)) => {
Expand All @@ -60,15 +42,6 @@ pub fn handle_deeplink(recipe_name: &str) -> Result<String> {
}
}

/// Opens a recipe in Goose Desktop
///
/// # Arguments
///
/// * `recipe_name` - Path to the recipe file
///
/// # Returns
///
/// Result indicating success or failure
pub fn handle_open(recipe_name: &str) -> Result<()> {
// Generate the deeplink using the helper function (no printing)
// This reuses all the validation and encoding logic
Expand Down Expand Up @@ -107,16 +80,6 @@ pub fn handle_open(recipe_name: &str) -> Result<()> {
}
}

/// Lists all available recipes from local paths and GitHub repositories
///
/// # Arguments
///
/// * `format` - Output format ("text" or "json")
/// * `verbose` - Whether to show detailed information
///
/// # Returns
///
/// Result indicating success or failure
pub fn handle_list(format: &str, verbose: bool) -> Result<()> {
let recipes = match list_available_recipes() {
Ok(recipes) => recipes,
Expand Down Expand Up @@ -168,18 +131,10 @@ pub fn handle_list(format: &str, verbose: bool) -> Result<()> {
Ok(())
}

/// Helper function to generate a deeplink
///
/// # Arguments
///
/// * `recipe_name` - Path to the recipe file
///
/// # Returns
///
/// Result containing the deeplink URL and recipe
fn generate_deeplink(recipe_name: &str) -> Result<(String, goose::recipe::Recipe)> {
let recipe_file = load_recipe_file(recipe_name)?;
// Load the recipe file first to validate it
let recipe = load_recipe_for_validation(recipe_name)?;
let recipe = validate_recipe_template_from_file(&recipe_file)?;
match recipe_deeplink::encode(&recipe) {
Ok(encoded) => {
let full_url = format!("goose://recipe?config={}", encoded);
Expand Down Expand Up @@ -227,20 +182,6 @@ prompt: "Test prompt content {{ name }}"
instructions: "Test instructions"
"#;

const RECIPE_WITH_INVALID_JSON_SCHEMA: &str = r#"
title: "Test Recipe with Invalid JSON Schema"
description: "A test recipe with invalid JSON schema"
prompt: "Test prompt content"
instructions: "Test instructions"
response:
json_schema:
type: invalid_type
properties:
result:
type: unknown_type
required: "should_be_array_not_string"
"#;

#[test]
fn test_handle_deeplink_valid_recipe() {
let temp_dir = TempDir::new().expect("Failed to create temp directory");
Expand Down Expand Up @@ -305,23 +246,6 @@ response:
assert!(result.is_err());
}

#[test]
fn test_handle_validation_recipe_with_invalid_json_schema() {
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let recipe_path = create_test_recipe_file(
&temp_dir,
"test_recipe.yaml",
RECIPE_WITH_INVALID_JSON_SCHEMA,
);

let result = handle_validate(&recipe_path);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("JSON schema validation failed"));
}

#[test]
fn test_generate_deeplink_valid_recipe() {
let temp_dir = TempDir::new().expect("Failed to create temp directory");
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
Expand Up @@ -331,7 +331,7 @@ fn get_github_recipe_info(repo: &str, dir_name: &str, recipe_filename: &str) ->
.map_err(|e| anyhow!("Failed to convert content to string: {}", e))?;

// Parse the recipe content
let (recipe, _) = parse_recipe_content(&content, format!("{}/{}", repo, dir_name))?;
let (recipe, _) = parse_recipe_content(&content, Some(format!("{}/{}", repo, dir_name)))?;

return Ok(RecipeInfo {
name: dir_name.to_string(),
Expand Down
35 changes: 5 additions & 30 deletions crates/goose-cli/src/recipes/recipe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ use crate::recipes::secret_discovery::{discover_recipe_secrets, SecretRequiremen
use anyhow::Result;
use goose::config::Config;
use goose::recipe::build_recipe::{
apply_values_to_parameters, build_recipe_from_template, validate_recipe_parameters, RecipeError,
apply_values_to_parameters, build_recipe_from_template, RecipeError,
};
use goose::recipe::read_recipe_file_content::RecipeFile;
use goose::recipe::template_recipe::render_recipe_for_preview;
use goose::recipe::validate_recipe::validate_recipe_parameters;
use goose::recipe::Recipe;
use serde_json::Value;
use std::collections::HashMap;

fn create_user_prompt_callback() -> impl Fn(&str, &str) -> Result<String> {
|key: &str, description: &str| -> Result<String> {
Expand Down Expand Up @@ -131,29 +131,11 @@ pub fn render_recipe_as_yaml(recipe_name: &str, params: Vec<(String, String)>) -
}
}

pub fn load_recipe_for_validation(recipe_name: &str) -> Result<Recipe> {
let (recipe_file, recipe_dir_str) = load_recipe_file_with_dir(recipe_name)?;
let recipe_file_content = &recipe_file.content;
validate_recipe_parameters(recipe_file_content, &recipe_dir_str)?;
let recipe = render_recipe_for_preview(
recipe_file_content,
recipe_dir_str.to_string(),
&HashMap::new(),
)?;

if let Some(response) = &recipe.response {
if let Some(json_schema) = &response.json_schema {
validate_json_schema(json_schema)?;
}
}

Ok(recipe)
}

pub fn explain_recipe(recipe_name: &str, params: Vec<(String, String)>) -> Result<()> {
let (recipe_file, recipe_dir_str) = load_recipe_file_with_dir(recipe_name)?;
let recipe_file_content = &recipe_file.content;
let recipe_parameters = validate_recipe_parameters(recipe_file_content, &recipe_dir_str)?;
let recipe_parameters =
validate_recipe_parameters(recipe_file_content, Some(recipe_dir_str.clone()))?;

let (params_for_template, missing_params) = apply_values_to_parameters(
&params,
Expand All @@ -163,7 +145,7 @@ pub fn explain_recipe(recipe_name: &str, params: Vec<(String, String)>) -> Resul
)?;
let recipe = render_recipe_for_preview(
recipe_file_content,
recipe_dir_str.to_string(),
Some(recipe_dir_str.clone()),
&params_for_template,
)?;
print_recipe_explanation(&recipe);
Expand All @@ -172,13 +154,6 @@ pub fn explain_recipe(recipe_name: &str, params: Vec<(String, String)>) -> Resul
Ok(())
}

fn validate_json_schema(schema: &serde_json::Value) -> Result<()> {
match jsonschema::validator_for(schema) {
Ok(_) => Ok(()),
Err(err) => Err(anyhow::anyhow!("JSON schema validation failed: {}", err)),
}
}

#[cfg(test)]
mod tests {
use goose::recipe::{RecipeParameterInputType, RecipeParameterRequirement};
Expand Down
1 change: 1 addition & 0 deletions crates/goose-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ utoipa = { version = "4.1", features = ["axum_extras", "chrono"] }
reqwest = { version = "0.12.9", features = ["json", "rustls-tls", "blocking", "multipart"], default-features = false }
tokio-util = "0.7.15"
uuid = { version = "1.11", features = ["v4"] }
serde_path_to_error = "0.1.20"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh that's cool


[[bin]]
name = "goosed"
Expand Down
Loading
Loading