1use 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
25fn create_prompt_service(repo_path: &Path) -> PromptService {
30 prompt_service_for_repo(repo_path)
31}
32
33fn 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
42fn format_list_or_none(items: &[String]) -> String {
44 if items.is_empty() {
45 "(none)".to_string()
46 } else {
47 items.join(", ")
48 }
49}
50
51pub 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 let domain = parse_domain_scope(args.domain.as_deref());
60
61 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 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 let options = SaveOptions::new().with_skip_enrichment(args.skip_enrichment);
99
100 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 let user_dir = crate::storage::get_user_data_dir()?;
107 create_prompt_service(&user_dir)
108 };
109
110 let result = prompt_service.save_with_enrichment::<crate::llm::OllamaClient>(
112 &args.name,
113 &content,
114 domain,
115 &options,
116 None, if existing.is_empty() {
118 None
119 } else {
120 Some(existing)
121 },
122 )?;
123
124 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
161pub 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 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 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
251pub 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 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
313pub 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 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 let values: HashMap<String, String> = args.variables.unwrap_or_default();
334
335 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 let result = substitute_variables(&p.content, &values, &p.variables)?;
359
360 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
390pub 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 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
432pub 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
457fn 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 let domain = parse_domain_scope(args.domain.as_deref());
468
469 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 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 let options = SaveOptions::new().with_skip_enrichment(args.skip_enrichment);
507
508 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 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 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
569fn execute_prompts_list(args: &PromptsArgs) -> Result<ToolResult> {
571 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 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
656fn 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
719fn 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 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 let result = substitute_variables(&p.content, &values, &p.variables)?;
765
766 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
796fn 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}