Skip to main content

subcog/mcp/tools/handlers/
prompts.rs

1//! Prompt tool execution handlers.
2//!
3//! Contains handlers for prompt management operations:
4//! save, list, get, run, delete.
5//!
6//! Provides both consolidated (`execute_prompts`) and legacy handlers.
7
8use std::collections::HashMap;
9use std::path::Path;
10
11use crate::mcp::tool_types::{
12    PromptDeleteArgs, PromptGetArgs, PromptListArgs, PromptRunArgs, PromptSaveArgs, PromptsArgs,
13    domain_scope_to_display, find_missing_required_variables, format_variable_info,
14    parse_domain_scope,
15};
16use crate::models::{PromptTemplate, substitute_variables};
17use crate::services::{
18    PromptFilter, PromptParser, PromptService, ServiceContainer, prompt_service_for_repo,
19};
20use crate::{Error, Result};
21use serde_json::Value;
22
23use super::super::{ToolContent, ToolResult};
24
25/// Creates a properly configured `PromptService` with storage settings from config.
26///
27/// Delegates to the canonical factory function in the services module to avoid
28/// layer violations (MCP layer should not directly construct services).
29fn create_prompt_service(repo_path: &Path) -> PromptService {
30    prompt_service_for_repo(repo_path)
31}
32
33/// Formats a field value for display, returning "(none)" if empty.
34fn format_field_or_none(value: &str) -> String {
35    if value.is_empty() {
36        "(none)".to_string()
37    } else {
38        value.to_string()
39    }
40}
41
42/// Formats a list of items for display, returning "(none)" if empty.
43fn format_list_or_none(items: &[String]) -> String {
44    if items.is_empty() {
45        "(none)".to_string()
46    } else {
47        items.join(", ")
48    }
49}
50
51/// Executes the prompt.save tool.
52pub fn execute_prompt_save(arguments: Value) -> Result<ToolResult> {
53    use crate::services::{EnrichmentStatus, PartialMetadata, SaveOptions};
54
55    let args: PromptSaveArgs =
56        serde_json::from_value(arguments).map_err(|e| Error::InvalidInput(e.to_string()))?;
57
58    // Parse domain scope
59    let domain = parse_domain_scope(args.domain.as_deref());
60
61    // Get content either directly or from file
62    let (content, mut base_template) = if let Some(content) = args.content {
63        (content.clone(), PromptTemplate::new(&args.name, &content))
64    } else if let Some(file_path) = args.file_path {
65        let template = PromptParser::from_file(&file_path)?;
66        (template.content.clone(), template)
67    } else {
68        return Err(Error::InvalidInput(
69            "Either 'content' or 'file_path' must be provided".to_string(),
70        ));
71    };
72
73    // Build partial metadata from user-provided values
74    let mut existing = PartialMetadata::new();
75    if let Some(desc) = args.description {
76        existing = existing.with_description(desc);
77    }
78    if let Some(tags) = args.tags {
79        existing = existing.with_tags(tags);
80    }
81    if let Some(vars) = args.variables {
82        use crate::models::PromptVariable;
83        let variables: Vec<PromptVariable> = vars
84            .into_iter()
85            .map(|v| PromptVariable {
86                name: v.name,
87                description: v.description,
88                default: v.default,
89                required: v.required.unwrap_or(true),
90            })
91            .collect();
92        existing = existing.with_variables(variables);
93    } else if !base_template.variables.is_empty() {
94        existing = existing.with_variables(std::mem::take(&mut base_template.variables));
95    }
96
97    // Configure save options
98    let options = SaveOptions::new().with_skip_enrichment(args.skip_enrichment);
99
100    // Get repo path and create service (works in both project and user scope)
101    let services = ServiceContainer::from_current_dir_or_user()?;
102    let mut prompt_service = if let Some(repo_path) = services.repo_path() {
103        create_prompt_service(repo_path)
104    } else {
105        // User-scope: create prompt service with user data directory
106        let user_dir = crate::storage::get_user_data_dir()?;
107        create_prompt_service(&user_dir)
108    };
109
110    // Use save_with_enrichment (no LLM provider for now - fallback mode)
111    let result = prompt_service.save_with_enrichment::<crate::llm::OllamaClient>(
112        &args.name,
113        &content,
114        domain,
115        &options,
116        None, // No LLM provider - uses fallback
117        if existing.is_empty() {
118            None
119        } else {
120            Some(existing)
121        },
122    )?;
123
124    // Format enrichment status
125    let enrichment_str = match result.enrichment_status {
126        EnrichmentStatus::Full => "LLM-enhanced",
127        EnrichmentStatus::Fallback => "Basic (LLM unavailable)",
128        EnrichmentStatus::Skipped => "Skipped",
129    };
130
131    let var_names: Vec<String> = result
132        .template
133        .variables
134        .iter()
135        .map(|v| v.name.clone())
136        .collect();
137    Ok(ToolResult {
138        content: vec![ToolContent::Text {
139            text: format!(
140                "Prompt saved successfully!\n\n\
141                 Name: {}\n\
142                 ID: {}\n\
143                 Domain: {}\n\
144                 Enrichment: {}\n\
145                 Description: {}\n\
146                 Tags: {}\n\
147                 Variables: {}",
148                result.template.name,
149                result.id,
150                domain_scope_to_display(domain),
151                enrichment_str,
152                format_field_or_none(&result.template.description),
153                format_list_or_none(&result.template.tags),
154                format_list_or_none(&var_names),
155            ),
156        }],
157        is_error: false,
158    })
159}
160
161/// Executes the prompt.list tool.
162pub fn execute_prompt_list(arguments: Value) -> Result<ToolResult> {
163    let args: PromptListArgs =
164        serde_json::from_value(arguments).map_err(|e| Error::InvalidInput(e.to_string()))?;
165
166    // Build filter
167    let mut filter = PromptFilter::new();
168    if let Some(domain) = args.domain {
169        filter = filter.with_domain(parse_domain_scope(Some(&domain)));
170    }
171    if let Some(tags) = args.tags {
172        filter = filter.with_tags(tags);
173    }
174    if let Some(pattern) = args.name_pattern {
175        filter = filter.with_name_pattern(pattern);
176    }
177    if let Some(limit) = args.limit {
178        filter = filter.with_limit(limit);
179    } else {
180        filter = filter.with_limit(20);
181    }
182
183    // Get prompts (works in both project and user scope)
184    let services = ServiceContainer::from_current_dir_or_user()?;
185    let mut prompt_service = if let Some(repo_path) = services.repo_path() {
186        create_prompt_service(repo_path)
187    } else {
188        let user_dir = crate::storage::get_user_data_dir()?;
189        create_prompt_service(&user_dir)
190    };
191    let prompts = prompt_service.list(&filter)?;
192
193    if prompts.is_empty() {
194        return Ok(ToolResult {
195            content: vec![ToolContent::Text {
196                text: "No prompts found matching the filter.".to_string(),
197            }],
198            is_error: false,
199        });
200    }
201
202    let mut output = format!("Found {} prompt(s):\n\n", prompts.len());
203    for (i, prompt) in prompts.iter().enumerate() {
204        let tags_display = if prompt.tags.is_empty() {
205            String::new()
206        } else {
207            format!(" [{}]", prompt.tags.join(", "))
208        };
209
210        let vars_count = prompt.variables.len();
211        let usage_info = if prompt.usage_count > 0 {
212            format!(" (used {} times)", prompt.usage_count)
213        } else {
214            String::new()
215        };
216
217        output.push_str(&format!(
218            "{}. **{}**{}{}\n   {}\n   Variables: {}\n\n",
219            i + 1,
220            prompt.name,
221            tags_display,
222            usage_info,
223            if prompt.description.is_empty() {
224                "(no description)"
225            } else {
226                &prompt.description
227            },
228            if vars_count == 0 {
229                "none".to_string()
230            } else {
231                format!(
232                    "{} ({})",
233                    vars_count,
234                    prompt
235                        .variables
236                        .iter()
237                        .map(|v| v.name.clone())
238                        .collect::<Vec<_>>()
239                        .join(", ")
240                )
241            }
242        ));
243    }
244
245    Ok(ToolResult {
246        content: vec![ToolContent::Text { text: output }],
247        is_error: false,
248    })
249}
250
251/// Executes the prompt.get tool.
252pub fn execute_prompt_get(arguments: Value) -> Result<ToolResult> {
253    let args: PromptGetArgs =
254        serde_json::from_value(arguments).map_err(|e| Error::InvalidInput(e.to_string()))?;
255
256    let domain = args.domain.map(|d| parse_domain_scope(Some(&d)));
257
258    // Works in both project and user scope
259    let services = ServiceContainer::from_current_dir_or_user()?;
260    let mut prompt_service = if let Some(repo_path) = services.repo_path() {
261        create_prompt_service(repo_path)
262    } else {
263        let user_dir = crate::storage::get_user_data_dir()?;
264        create_prompt_service(&user_dir)
265    };
266    let prompt = prompt_service.get(&args.name, domain)?;
267
268    match prompt {
269        Some(p) => {
270            let vars_info: Vec<String> = p.variables.iter().map(format_variable_info).collect();
271
272            Ok(ToolResult {
273                content: vec![ToolContent::Text {
274                    text: format!(
275                        "**{}**\n\n\
276                         {}\n\n\
277                         **Variables:**\n{}\n\n\
278                         **Content:**\n```\n{}\n```\n\n\
279                         Tags: {}\n\
280                         Usage count: {}",
281                        p.name,
282                        if p.description.is_empty() {
283                            "(no description)".to_string()
284                        } else {
285                            p.description.clone()
286                        },
287                        if vars_info.is_empty() {
288                            "none".to_string()
289                        } else {
290                            vars_info.join("\n")
291                        },
292                        p.content,
293                        if p.tags.is_empty() {
294                            "none".to_string()
295                        } else {
296                            p.tags.join(", ")
297                        },
298                        p.usage_count
299                    ),
300                }],
301                is_error: false,
302            })
303        },
304        None => Ok(ToolResult {
305            content: vec![ToolContent::Text {
306                text: format!("Prompt '{}' not found.", args.name),
307            }],
308            is_error: true,
309        }),
310    }
311}
312
313/// Executes the prompt.run tool.
314pub fn execute_prompt_run(arguments: Value) -> Result<ToolResult> {
315    let args: PromptRunArgs =
316        serde_json::from_value(arguments).map_err(|e| Error::InvalidInput(e.to_string()))?;
317
318    let domain = args.domain.map(|d| parse_domain_scope(Some(&d)));
319
320    // Works in both project and user scope
321    let services = ServiceContainer::from_current_dir_or_user()?;
322    let mut prompt_service = if let Some(repo_path) = services.repo_path() {
323        create_prompt_service(repo_path)
324    } else {
325        let user_dir = crate::storage::get_user_data_dir()?;
326        create_prompt_service(&user_dir)
327    };
328    let prompt = prompt_service.get(&args.name, domain)?;
329
330    match prompt {
331        Some(p) => {
332            // Convert variables to HashMap
333            let values: HashMap<String, String> = args.variables.unwrap_or_default();
334
335            // Check for missing required variables
336            let missing: Vec<&str> = find_missing_required_variables(&p.variables, &values);
337
338            if !missing.is_empty() {
339                return Ok(ToolResult {
340                    content: vec![ToolContent::Text {
341                        text: format!(
342                            "Missing required variables: {}\n\n\
343                             Use the 'variables' parameter to provide values:\n\
344                             ```json\n{{\n  \"variables\": {{\n{}\n  }}\n}}\n```",
345                            missing.join(", "),
346                            missing
347                                .iter()
348                                .map(|n| format!("    \"{n}\": \"<value>\""))
349                                .collect::<Vec<_>>()
350                                .join(",\n")
351                        ),
352                    }],
353                    is_error: true,
354                });
355            }
356
357            // Substitute variables
358            let result = substitute_variables(&p.content, &values, &p.variables)?;
359
360            // Increment usage count (best effort)
361            if let Some(scope) = domain {
362                let _ = prompt_service.increment_usage(&args.name, scope);
363            }
364
365            Ok(ToolResult {
366                content: vec![ToolContent::Text {
367                    text: format!(
368                        "**Prompt: {}**\n\n{}\n\n---\n_Variables substituted: {}_",
369                        p.name,
370                        result,
371                        if values.is_empty() {
372                            "none (defaults used)".to_string()
373                        } else {
374                            values.keys().cloned().collect::<Vec<_>>().join(", ")
375                        }
376                    ),
377                }],
378                is_error: false,
379            })
380        },
381        None => Ok(ToolResult {
382            content: vec![ToolContent::Text {
383                text: format!("Prompt '{}' not found.", args.name),
384            }],
385            is_error: true,
386        }),
387    }
388}
389
390/// Executes the prompt.delete tool.
391pub fn execute_prompt_delete(arguments: Value) -> Result<ToolResult> {
392    let args: PromptDeleteArgs =
393        serde_json::from_value(arguments).map_err(|e| Error::InvalidInput(e.to_string()))?;
394
395    let domain = parse_domain_scope(Some(&args.domain));
396
397    // Works in both project and user scope
398    let services = ServiceContainer::from_current_dir_or_user()?;
399    let mut prompt_service = if let Some(repo_path) = services.repo_path() {
400        create_prompt_service(repo_path)
401    } else {
402        let user_dir = crate::storage::get_user_data_dir()?;
403        create_prompt_service(&user_dir)
404    };
405    let deleted = prompt_service.delete(&args.name, domain)?;
406
407    if deleted {
408        Ok(ToolResult {
409            content: vec![ToolContent::Text {
410                text: format!(
411                    "Prompt '{}' deleted from {} scope.",
412                    args.name,
413                    domain_scope_to_display(domain)
414                ),
415            }],
416            is_error: false,
417        })
418    } else {
419        Ok(ToolResult {
420            content: vec![ToolContent::Text {
421                text: format!(
422                    "Prompt '{}' not found in {} scope.",
423                    args.name,
424                    domain_scope_to_display(domain)
425                ),
426            }],
427            is_error: true,
428        })
429    }
430}
431
432// =============================================================================
433// Consolidated Prompt Handler
434// =============================================================================
435
436/// Executes the consolidated `subcog_prompts` tool.
437///
438/// Dispatches to the appropriate action handler based on the `action` field.
439/// Valid actions: save, list, get, run, delete.
440pub fn execute_prompts(arguments: Value) -> Result<ToolResult> {
441    let args: PromptsArgs =
442        serde_json::from_value(arguments).map_err(|e| Error::InvalidInput(e.to_string()))?;
443
444    match args.action.as_str() {
445        "save" => execute_prompts_save(&args),
446        "list" => execute_prompts_list(&args),
447        "get" => execute_prompts_get(&args),
448        "run" => execute_prompts_run(&args),
449        "delete" => execute_prompts_delete(&args),
450        _ => Err(Error::InvalidInput(format!(
451            "Unknown prompts action: '{}'. Valid actions: save, list, get, run, delete",
452            args.action
453        ))),
454    }
455}
456
457/// Handles the `save` action for `subcog_prompts`.
458fn execute_prompts_save(args: &PromptsArgs) -> Result<ToolResult> {
459    use crate::services::{EnrichmentStatus, PartialMetadata, SaveOptions};
460
461    let name = args
462        .name
463        .as_ref()
464        .ok_or_else(|| Error::InvalidInput("'name' is required for save action".to_string()))?;
465
466    // Parse domain scope
467    let domain = parse_domain_scope(args.domain.as_deref());
468
469    // Get content either directly or from file
470    let (content, mut base_template) = if let Some(content) = &args.content {
471        (content.clone(), PromptTemplate::new(name, content))
472    } else if let Some(file_path) = &args.file_path {
473        let template = PromptParser::from_file(file_path)?;
474        (template.content.clone(), template)
475    } else {
476        return Err(Error::InvalidInput(
477            "Either 'content' or 'file_path' must be provided for save action".to_string(),
478        ));
479    };
480
481    // Build partial metadata from user-provided values
482    let mut existing = PartialMetadata::new();
483    if let Some(desc) = &args.description {
484        existing = existing.with_description(desc.clone());
485    }
486    if let Some(tags) = &args.tags {
487        existing = existing.with_tags(tags.clone());
488    }
489    if let Some(vars) = &args.variables_def {
490        use crate::models::PromptVariable;
491        let variables: Vec<PromptVariable> = vars
492            .iter()
493            .map(|v| PromptVariable {
494                name: v.name.clone(),
495                description: v.description.clone(),
496                default: v.default.clone(),
497                required: v.required.unwrap_or(true),
498            })
499            .collect();
500        existing = existing.with_variables(variables);
501    } else if !base_template.variables.is_empty() {
502        existing = existing.with_variables(std::mem::take(&mut base_template.variables));
503    }
504
505    // Configure save options
506    let options = SaveOptions::new().with_skip_enrichment(args.skip_enrichment);
507
508    // Get repo path and create service
509    let services = ServiceContainer::from_current_dir_or_user()?;
510    let mut prompt_service = if let Some(repo_path) = services.repo_path() {
511        create_prompt_service(repo_path)
512    } else {
513        let user_dir = crate::storage::get_user_data_dir()?;
514        create_prompt_service(&user_dir)
515    };
516
517    // Use save_with_enrichment
518    let result = prompt_service.save_with_enrichment::<crate::llm::OllamaClient>(
519        name,
520        &content,
521        domain,
522        &options,
523        None,
524        if existing.is_empty() {
525            None
526        } else {
527            Some(existing)
528        },
529    )?;
530
531    // Format enrichment status
532    let enrichment_str = match result.enrichment_status {
533        EnrichmentStatus::Full => "LLM-enhanced",
534        EnrichmentStatus::Fallback => "Basic (LLM unavailable)",
535        EnrichmentStatus::Skipped => "Skipped",
536    };
537
538    let var_names: Vec<String> = result
539        .template
540        .variables
541        .iter()
542        .map(|v| v.name.clone())
543        .collect();
544
545    Ok(ToolResult {
546        content: vec![ToolContent::Text {
547            text: format!(
548                "Prompt saved successfully!\n\n\
549                 Name: {}\n\
550                 ID: {}\n\
551                 Domain: {}\n\
552                 Enrichment: {}\n\
553                 Description: {}\n\
554                 Tags: {}\n\
555                 Variables: {}",
556                result.template.name,
557                result.id,
558                domain_scope_to_display(domain),
559                enrichment_str,
560                format_field_or_none(&result.template.description),
561                format_list_or_none(&result.template.tags),
562                format_list_or_none(&var_names),
563            ),
564        }],
565        is_error: false,
566    })
567}
568
569/// Handles the `list` action for `subcog_prompts`.
570fn execute_prompts_list(args: &PromptsArgs) -> Result<ToolResult> {
571    // Build filter
572    let mut filter = PromptFilter::new();
573    if let Some(domain) = &args.domain {
574        filter = filter.with_domain(parse_domain_scope(Some(domain)));
575    }
576    if let Some(tags) = &args.tags {
577        filter = filter.with_tags(tags.clone());
578    }
579    if let Some(pattern) = &args.name_pattern {
580        filter = filter.with_name_pattern(pattern.clone());
581    }
582    if let Some(limit) = args.limit {
583        filter = filter.with_limit(limit);
584    } else {
585        filter = filter.with_limit(20);
586    }
587
588    // Get prompts
589    let services = ServiceContainer::from_current_dir_or_user()?;
590    let mut prompt_service = if let Some(repo_path) = services.repo_path() {
591        create_prompt_service(repo_path)
592    } else {
593        let user_dir = crate::storage::get_user_data_dir()?;
594        create_prompt_service(&user_dir)
595    };
596    let prompts = prompt_service.list(&filter)?;
597
598    if prompts.is_empty() {
599        return Ok(ToolResult {
600            content: vec![ToolContent::Text {
601                text: "No prompts found matching the filter.".to_string(),
602            }],
603            is_error: false,
604        });
605    }
606
607    let mut output = format!("Found {} prompt(s):\n\n", prompts.len());
608    for (i, prompt) in prompts.iter().enumerate() {
609        let tags_display = if prompt.tags.is_empty() {
610            String::new()
611        } else {
612            format!(" [{}]", prompt.tags.join(", "))
613        };
614
615        let vars_count = prompt.variables.len();
616        let usage_info = if prompt.usage_count > 0 {
617            format!(" (used {} times)", prompt.usage_count)
618        } else {
619            String::new()
620        };
621
622        output.push_str(&format!(
623            "{}. **{}**{}{}\n   {}\n   Variables: {}\n\n",
624            i + 1,
625            prompt.name,
626            tags_display,
627            usage_info,
628            if prompt.description.is_empty() {
629                "(no description)"
630            } else {
631                &prompt.description
632            },
633            if vars_count == 0 {
634                "none".to_string()
635            } else {
636                format!(
637                    "{} ({})",
638                    vars_count,
639                    prompt
640                        .variables
641                        .iter()
642                        .map(|v| v.name.clone())
643                        .collect::<Vec<_>>()
644                        .join(", ")
645                )
646            }
647        ));
648    }
649
650    Ok(ToolResult {
651        content: vec![ToolContent::Text { text: output }],
652        is_error: false,
653    })
654}
655
656/// Handles the `get` action for `subcog_prompts`.
657fn execute_prompts_get(args: &PromptsArgs) -> Result<ToolResult> {
658    let name = args
659        .name
660        .as_ref()
661        .ok_or_else(|| Error::InvalidInput("'name' is required for get action".to_string()))?;
662
663    let domain = args.domain.as_ref().map(|d| parse_domain_scope(Some(d)));
664
665    let services = ServiceContainer::from_current_dir_or_user()?;
666    let mut prompt_service = if let Some(repo_path) = services.repo_path() {
667        create_prompt_service(repo_path)
668    } else {
669        let user_dir = crate::storage::get_user_data_dir()?;
670        create_prompt_service(&user_dir)
671    };
672    let prompt = prompt_service.get(name, domain)?;
673
674    match prompt {
675        Some(p) => {
676            let vars_info: Vec<String> = p.variables.iter().map(format_variable_info).collect();
677
678            Ok(ToolResult {
679                content: vec![ToolContent::Text {
680                    text: format!(
681                        "**{}**\n\n\
682                         {}\n\n\
683                         **Variables:**\n{}\n\n\
684                         **Content:**\n```\n{}\n```\n\n\
685                         Tags: {}\n\
686                         Usage count: {}",
687                        p.name,
688                        if p.description.is_empty() {
689                            "(no description)".to_string()
690                        } else {
691                            p.description.clone()
692                        },
693                        if vars_info.is_empty() {
694                            "none".to_string()
695                        } else {
696                            vars_info.join("\n")
697                        },
698                        p.content,
699                        if p.tags.is_empty() {
700                            "none".to_string()
701                        } else {
702                            p.tags.join(", ")
703                        },
704                        p.usage_count
705                    ),
706                }],
707                is_error: false,
708            })
709        },
710        None => Ok(ToolResult {
711            content: vec![ToolContent::Text {
712                text: format!("Prompt '{name}' not found."),
713            }],
714            is_error: true,
715        }),
716    }
717}
718
719/// Handles the `run` action for `subcog_prompts`.
720fn execute_prompts_run(args: &PromptsArgs) -> Result<ToolResult> {
721    let name = args
722        .name
723        .as_ref()
724        .ok_or_else(|| Error::InvalidInput("'name' is required for run action".to_string()))?;
725
726    let domain = args.domain.as_ref().map(|d| parse_domain_scope(Some(d)));
727
728    let services = ServiceContainer::from_current_dir_or_user()?;
729    let mut prompt_service = if let Some(repo_path) = services.repo_path() {
730        create_prompt_service(repo_path)
731    } else {
732        let user_dir = crate::storage::get_user_data_dir()?;
733        create_prompt_service(&user_dir)
734    };
735    let prompt = prompt_service.get(name, domain)?;
736
737    match prompt {
738        Some(p) => {
739            let values: HashMap<String, String> = args.variables.clone().unwrap_or_default();
740
741            // Check for missing required variables
742            let missing: Vec<&str> = find_missing_required_variables(&p.variables, &values);
743
744            if !missing.is_empty() {
745                return Ok(ToolResult {
746                    content: vec![ToolContent::Text {
747                        text: format!(
748                            "Missing required variables: {}\n\n\
749                             Use the 'variables' parameter to provide values:\n\
750                             ```json\n{{\n  \"variables\": {{\n{}\n  }}\n}}\n```",
751                            missing.join(", "),
752                            missing
753                                .iter()
754                                .map(|n| format!("    \"{n}\": \"<value>\""))
755                                .collect::<Vec<_>>()
756                                .join(",\n")
757                        ),
758                    }],
759                    is_error: true,
760                });
761            }
762
763            // Substitute variables
764            let result = substitute_variables(&p.content, &values, &p.variables)?;
765
766            // Increment usage count (best effort)
767            if let Some(scope) = domain {
768                let _ = prompt_service.increment_usage(name, scope);
769            }
770
771            Ok(ToolResult {
772                content: vec![ToolContent::Text {
773                    text: format!(
774                        "**Prompt: {}**\n\n{}\n\n---\n_Variables substituted: {}_",
775                        p.name,
776                        result,
777                        if values.is_empty() {
778                            "none (defaults used)".to_string()
779                        } else {
780                            values.keys().cloned().collect::<Vec<_>>().join(", ")
781                        }
782                    ),
783                }],
784                is_error: false,
785            })
786        },
787        None => Ok(ToolResult {
788            content: vec![ToolContent::Text {
789                text: format!("Prompt '{name}' not found."),
790            }],
791            is_error: true,
792        }),
793    }
794}
795
796/// Handles the `delete` action for `subcog_prompts`.
797fn execute_prompts_delete(args: &PromptsArgs) -> Result<ToolResult> {
798    let name = args
799        .name
800        .as_ref()
801        .ok_or_else(|| Error::InvalidInput("'name' is required for delete action".to_string()))?;
802
803    let domain_str = args.domain.as_ref().ok_or_else(|| {
804        Error::InvalidInput("'domain' is required for delete action (for safety)".to_string())
805    })?;
806
807    let domain = parse_domain_scope(Some(domain_str));
808
809    let services = ServiceContainer::from_current_dir_or_user()?;
810    let mut prompt_service = if let Some(repo_path) = services.repo_path() {
811        create_prompt_service(repo_path)
812    } else {
813        let user_dir = crate::storage::get_user_data_dir()?;
814        create_prompt_service(&user_dir)
815    };
816    let deleted = prompt_service.delete(name, domain)?;
817
818    if deleted {
819        Ok(ToolResult {
820            content: vec![ToolContent::Text {
821                text: format!(
822                    "Prompt '{}' deleted from {} scope.",
823                    name,
824                    domain_scope_to_display(domain)
825                ),
826            }],
827            is_error: false,
828        })
829    } else {
830        Ok(ToolResult {
831            content: vec![ToolContent::Text {
832                text: format!(
833                    "Prompt '{}' not found in {} scope.",
834                    name,
835                    domain_scope_to_display(domain)
836                ),
837            }],
838            is_error: true,
839        })
840    }
841}