1212
1313
1414class TemplateManager :
15- """Manage prompt templates from ~/.prompts/ directory ."""
15+ """Manage prompt templates from multiple directories with priority ."""
1616
1717 def __init__ (self , prompts_dir : Optional [Path ] = None ):
1818 """
1919 Initialize template manager.
2020
2121 Args:
22- prompts_dir: Directory containing templates (defaults to ~/.prompts/)
22+ prompts_dir: Base prompts directory (defaults to ~/.prompts/)
23+ This is used for backward compatibility and initialization.
2324 """
24- self .prompts_dir = prompts_dir or Path .home () / ".prompts"
25+ base_prompts_dir = prompts_dir or Path .home () / ".prompts"
26+
27+ # Template directories in priority order (first = highest priority)
28+ # Priority: ~/.prompts > ./.claude/commands > ~/.claude/commands
29+ self .template_dirs = [
30+ base_prompts_dir , # Legacy/base (highest priority)
31+ Path .cwd () / ".claude" / "commands" , # Project-specific (medium)
32+ Path .home () / ".claude" / "commands" , # User global (lowest priority)
33+ ]
34+
35+ # Keep prompts_dir for backward compatibility (used for initialization)
36+ self .prompts_dir = base_prompts_dir
2537 self ._initialize_templates ()
2638
2739 def _initialize_templates (self ):
@@ -173,7 +185,10 @@ def _create_sample_template(self, name: str, content: str):
173185
174186 def load_template (self , template_name : str , input_text : str = "" ) -> Optional [str ]:
175187 """
176- Load a prompt template from ~/.prompts/{template_name}.md
188+ Load a prompt template, checking directories in priority order.
189+
190+ Checks in order: ~/.prompts, ./.claude/commands, ~/.claude/commands
191+ Returns the first match found (highest priority wins).
177192
178193 Args:
179194 template_name: Name of the template (without .md extension)
@@ -182,70 +197,85 @@ def load_template(self, template_name: str, input_text: str = "") -> Optional[st
182197 Returns:
183198 Processed template text, or None if template not found
184199 """
185- template_path = self .prompts_dir / f"{ template_name } .md"
200+ # Check directories in priority order (highest priority first)
201+ for template_dir in self .template_dirs :
202+ template_path = template_dir / f"{ template_name } .md"
186203
187- if not template_path .exists ():
188- return None
204+ if not template_path .exists ():
205+ continue
189206
190- try :
191- with open (template_path , encoding = "utf-8" ) as f :
192- template = f .read ()
207+ try :
208+ with open (template_path , encoding = "utf-8" ) as f :
209+ template = f .read ()
193210
194- # Replace {input} placeholder with provided text
195- if "{input}" in template :
196- template = template .replace ("{input}" , input_text )
197- elif input_text :
198- # If no {input} placeholder but input provided, append it
199- template = f"{ template } \n \n { input_text } "
211+ # Replace {input} placeholder with provided text
212+ if "{input}" in template :
213+ template = template .replace ("{input}" , input_text )
214+ elif input_text :
215+ # If no {input} placeholder but input provided, append it
216+ template = f"{ template } \n \n { input_text } "
200217
201- return template
218+ return template
202219
203- except Exception as e :
204- logger .error (f"Error loading template { template_name } : { e } " )
205- return None
220+ except Exception as e :
221+ logger .error (f"Error loading template { template_name } : { e } " )
222+ continue
223+
224+ return None
206225
207226 def list_templates (self ) -> list [str ]:
208227 """
209- List available prompt templates from ~/.prompts/
228+ List all available templates from all directories (deduplicated).
210229
211230 Returns:
212- List of template names (without .md extension)
231+ Sorted list of unique template names (without .md extension)
213232 """
214- if not self .prompts_dir .exists ():
215- return []
233+ templates = set ()
216234
217- templates = []
218- for file in self .prompts_dir .glob ("*.md" ):
219- templates .append (file .stem )
235+ for template_dir in self .template_dirs :
236+ if not template_dir .exists ():
237+ continue
238+
239+ for file in template_dir .glob ("*.md" ):
240+ templates .add (file .stem )
220241
221242 return sorted (templates )
222243
223- def get_template_info (self , template_name : str ) -> Optional [tuple [str , str ]]:
244+ def get_template_info (
245+ self , template_name : str , template_dir : Optional [Path ] = None
246+ ) -> Optional [tuple [str , str ]]:
224247 """
225248 Get template description from first line.
226249
227250 Args:
228251 template_name: Name of the template
252+ template_dir: Specific directory to check (if None, uses priority order)
229253
230254 Returns:
231255 Tuple of (name, description) or None if not found
232256 """
233- template_path = self .prompts_dir / f"{ template_name } .md"
257+ # If specific directory provided, check only that one
258+ dirs_to_check = [template_dir ] if template_dir else self .template_dirs
234259
235- if not template_path . exists () :
236- return None
260+ for dir_path in dirs_to_check :
261+ template_path = dir_path / f" { template_name } .md"
237262
238- try :
239- with open (template_path , encoding = "utf-8" ) as f :
240- first_line = f .readline ().strip ()
241- # Extract description from markdown heading
242- if first_line .startswith ("# " ):
243- description = first_line [2 :].strip ()
244- else :
245- description = template_name
246- return (template_name , description )
247- except Exception :
248- return (template_name , template_name )
263+ if not template_path .exists ():
264+ continue
265+
266+ try :
267+ with open (template_path , encoding = "utf-8" ) as f :
268+ first_line = f .readline ().strip ()
269+ # Extract description from markdown heading
270+ if first_line .startswith ("# " ):
271+ description = first_line [2 :].strip ()
272+ else :
273+ description = template_name
274+ return (template_name , description )
275+ except Exception :
276+ return (template_name , template_name )
277+
278+ return None
249279
250280 def list_templates_with_descriptions (self ) -> list [tuple [str , str ]]:
251281 """
@@ -260,3 +290,29 @@ def list_templates_with_descriptions(self) -> list[tuple[str, str]]:
260290 if info :
261291 templates .append (info )
262292 return templates
293+
294+ def list_templates_grouped (self ) -> list [tuple [Path , list [tuple [str , str ]]]]:
295+ """
296+ List templates grouped by source directory.
297+
298+ Returns:
299+ List of (directory_path, templates) tuples where templates is a list
300+ of (name, description) tuples. Directories are returned in priority order.
301+ """
302+ grouped = []
303+
304+ for template_dir in self .template_dirs :
305+ if not template_dir .exists ():
306+ continue
307+
308+ templates_in_dir = []
309+ for file in template_dir .glob ("*.md" ):
310+ template_name = file .stem
311+ info = self .get_template_info (template_name , template_dir )
312+ if info :
313+ templates_in_dir .append (info )
314+
315+ if templates_in_dir :
316+ grouped .append ((template_dir , sorted (templates_in_dir )))
317+
318+ return grouped
0 commit comments