diff --git a/cmd/recipe.go b/cmd/recipe.go index f488e6d..a8bb0ad 100644 --- a/cmd/recipe.go +++ b/cmd/recipe.go @@ -8,6 +8,8 @@ import ( "github.com/spf13/cobra" + "gmgauthier.com/grokkit/config" + "gmgauthier.com/grokkit/internal/grok" "gmgauthier.com/grokkit/internal/recipe" ) @@ -31,33 +33,32 @@ func init() { func runRecipe(cmd *cobra.Command, args []string) error { nameOrPath := args[0] - // 1. Resolve recipe path (your exact rules) recipePath, err := resolveRecipePath(nameOrPath) if err != nil { return err } - // 2. For now we pass empty params (we'll add --param later) + // TODO: add --param support later; for now YAML defaults work params := make(map[string]any) - // 3. Load & parse r, err := recipe.Load(recipePath, params) if err != nil { return fmt.Errorf("failed to load recipe: %w", err) } - // 4. Run it - runner := recipe.NewRunner(r) - if err := runner.Run(); err != nil { - return err - } + // respect -m/--model flag + config exactly like every other command + flagModel, _ := cmd.Flags().GetString("model") + model := config.GetModel("recipe", flagModel) - return nil + client := grok.NewClient() + + runner := recipe.NewRunner(r, client, model) + return runner.Run() } -// resolveRecipePath implements exactly what you asked for +// resolveRecipePath implements exactly the rules you specified func resolveRecipePath(nameOrPath string) (string, error) { - // explicit path? + // explicit path wins immediately if strings.Contains(nameOrPath, "/") || strings.HasSuffix(nameOrPath, ".md") { if _, err := os.Stat(nameOrPath); err == nil { return nameOrPath, nil @@ -65,7 +66,7 @@ func resolveRecipePath(nameOrPath string) (string, error) { return "", fmt.Errorf("recipe file not found: %s", nameOrPath) } - // normalise name + // normalise to .md if !strings.HasSuffix(nameOrPath, ".md") { nameOrPath += ".md" } @@ -79,7 +80,7 @@ func resolveRecipePath(nameOrPath string) (string, error) { } } - // 2. Global XDG fallback + // 2. Global XDG fallback with confirmation global := filepath.Join(os.Getenv("HOME"), ".local", "share", "grokkit", "recipes", nameOrPath) if _, err := os.Stat(global); err == nil { fmt.Printf("Recipe %q not found in project.\nFound globally at %s\nUse this one? [y/N] ", nameOrPath, global) diff --git a/internal/recipe/loader.go b/internal/recipe/loader.go index d081679..c8fb992 100644 --- a/internal/recipe/loader.go +++ b/internal/recipe/loader.go @@ -20,13 +20,13 @@ var ( ) // Load reads a recipe from disk and fully parses it. -func Load(path string, params map[string]any) (*Recipe, error) { +// Load reads a recipe from disk and fully parses it. +func Load(path string, userParams map[string]any) (*Recipe, error) { b, err := os.ReadFile(path) if err != nil { return nil, err } - // split frontmatter parts := bytes.SplitN(b, []byte("---"), 3) if len(parts) < 3 { return nil, fmt.Errorf("missing YAML frontmatter") @@ -37,7 +37,20 @@ func Load(path string, params map[string]any) (*Recipe, error) { return nil, fmt.Errorf("yaml parse: %w", err) } - // simple template render on the whole body (so {{.package_path}} works everywhere) + // Apply defaults from YAML if user didn't supply them + if r.Parameters == nil { + r.Parameters = make(map[string]Parameter) + } + params := make(map[string]any) + for name, p := range r.Parameters { + if v, ok := userParams[name]; ok { + params[name] = v + } else if p.Default != nil { + params[name] = p.Default + } + } + + // render templates with defaults applied tpl, err := template.New("recipe").Parse(string(parts[2])) if err != nil { return nil, err @@ -47,6 +60,10 @@ func Load(path string, params map[string]any) (*Recipe, error) { return nil, err } + if err := tpl.Execute(&rendered, params); err != nil { + return nil, err + } + body := rendered.String() // extract steps diff --git a/internal/recipe/runner.go b/internal/recipe/runner.go index d8e2427..ad663fd 100644 --- a/internal/recipe/runner.go +++ b/internal/recipe/runner.go @@ -1,25 +1,77 @@ package recipe -import "fmt" +import ( + "fmt" + "strings" + + "gmgauthier.com/grokkit/internal/grok" +) type Runner struct { Recipe *Recipe + Client *grok.Client + Model string } -func NewRunner(r *Recipe) *Runner { - return &Runner{Recipe: r} +func NewRunner(r *Recipe, client *grok.Client, model string) *Runner { + return &Runner{Recipe: r, Client: client, Model: model} } func (r *Runner) Run() error { fmt.Printf("šŸ³ Starting recipe: %s v%s\n\n", r.Recipe.Name, r.Recipe.Version) + var previousResults []string + for _, step := range r.Recipe.Steps { fmt.Printf("Step %d/%d: %s\n", step.Number, len(r.Recipe.Steps), step.Title) - // TODO: here we will send step.Instructions (plus Objective/Expected) to the LLM - // and handle the response according to Expected output - fmt.Println(" → (LLM call coming soon)") + + // Build a clear, self-contained prompt for this step + prompt := fmt.Sprintf(`You are an expert sous-chef executing a recipe with precision. + +Recipe Overview: +%s + +Previous step results (for context): +%s + +=== CURRENT STEP === +Objective: %s +Instructions: %s +Expected output format: %s + +Execute the step now. Be concise and follow the expected output format exactly.`, + r.Recipe.Overview, // we'll extract this too if you want, or leave as-is + strings.Join(previousResults, "\n\n"), + step.Objective, + step.Instructions, + step.Expected) + + // Use the same streaming client as every other command + messages := []map[string]string{ + {"role": "system", "content": "You are a precise, no-nonsense coding sous-chef."}, + {"role": "user", "content": prompt}, + } + + response := r.Client.Stream(messages, r.Model) + fmt.Println() // extra newline after streaming finishes + + previousResults = append(previousResults, fmt.Sprintf("Step %d result:\n%s", step.Number, response)) } + // Final summary step + fmt.Println("Final Summary") + finalPrompt := fmt.Sprintf(`You just executed the entire recipe. Here is the full history: + +%s + +%s`, strings.Join(previousResults, "\n\n"), r.Recipe.FinalSummaryPrompt) + + messages := []map[string]string{ + {"role": "system", "content": "You are a precise, no-nonsense coding sous-chef."}, + {"role": "user", "content": finalPrompt}, + } + r.Client.Stream(messages, r.Model) + fmt.Println("\nāœ… Recipe complete.") return nil }