claude-dev-cli 0.4.0__py3-none-any.whl → 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of claude-dev-cli might be problematic. Click here for more details.

@@ -9,7 +9,7 @@ Features:
9
9
  - Interactive and single-shot modes
10
10
  """
11
11
 
12
- __version__ = "0.2.0"
12
+ __version__ = "0.6.0"
13
13
  __author__ = "Julio"
14
14
  __license__ = "MIT"
15
15
 
claude_dev_cli/cli.py CHANGED
@@ -1,5 +1,6 @@
1
1
  """Command-line interface for Claude Dev CLI."""
2
2
 
3
+ import os
3
4
  import sys
4
5
  from pathlib import Path
5
6
  from typing import Optional
@@ -23,6 +24,8 @@ from claude_dev_cli.commands import (
23
24
  from claude_dev_cli.usage import UsageTracker
24
25
  from claude_dev_cli import toon_utils
25
26
  from claude_dev_cli.plugins import load_plugins
27
+ from claude_dev_cli.history import ConversationHistory, Conversation
28
+ from claude_dev_cli.template_manager import TemplateManager, Template
26
29
 
27
30
  console = Console()
28
31
 
@@ -112,11 +115,36 @@ def ask(
112
115
 
113
116
  @main.command()
114
117
  @click.option('-a', '--api', help='API config to use')
118
+ @click.option('--continue', 'continue_conversation', is_flag=True,
119
+ help='Continue the last conversation')
120
+ @click.option('--save/--no-save', default=True, help='Save conversation history')
115
121
  @click.pass_context
116
- def interactive(ctx: click.Context, api: Optional[str]) -> None:
122
+ def interactive(
123
+ ctx: click.Context,
124
+ api: Optional[str],
125
+ continue_conversation: bool,
126
+ save: bool
127
+ ) -> None:
117
128
  """Start interactive chat mode."""
118
129
  console = ctx.obj['console']
119
130
 
131
+ # Setup conversation history
132
+ config = Config()
133
+ history_dir = config.config_dir / "history"
134
+ conv_history = ConversationHistory(history_dir)
135
+
136
+ # Load or create conversation
137
+ if continue_conversation:
138
+ conversation = conv_history.get_latest_conversation()
139
+ if conversation:
140
+ console.print(f"[green]↶ Continuing conversation from {conversation.updated_at.strftime('%Y-%m-%d %H:%M')}[/green]")
141
+ console.print(f"[dim]Messages: {len(conversation.messages)}[/dim]\n")
142
+ else:
143
+ console.print("[yellow]No previous conversation found, starting new one[/yellow]\n")
144
+ conversation = Conversation()
145
+ else:
146
+ conversation = Conversation()
147
+
120
148
  console.print(Panel.fit(
121
149
  "Claude Dev CLI - Interactive Mode\n"
122
150
  "Type 'exit' or 'quit' to end\n"
@@ -127,28 +155,177 @@ def interactive(ctx: click.Context, api: Optional[str]) -> None:
127
155
 
128
156
  try:
129
157
  client = ClaudeClient(api_config_name=api)
158
+ response_buffer = []
130
159
 
131
160
  while True:
132
161
  try:
133
162
  user_input = console.input("\n[bold cyan]You:[/bold cyan] ").strip()
134
163
 
135
164
  if user_input.lower() in ['exit', 'quit']:
165
+ if save and conversation.messages:
166
+ conv_history.save_conversation(conversation)
167
+ console.print(f"\n[dim]💾 Saved conversation: {conversation.conversation_id}[/dim]")
136
168
  break
169
+
170
+ if user_input.lower() == 'clear':
171
+ conversation = Conversation()
172
+ console.print("[yellow]Conversation cleared[/yellow]")
173
+ continue
174
+
137
175
  if not user_input:
138
176
  continue
139
177
 
178
+ # Add user message to history
179
+ conversation.add_message("user", user_input)
180
+
181
+ # Get response
140
182
  console.print("\n[bold green]Claude:[/bold green] ", end='')
183
+ response_buffer = []
141
184
  for chunk in client.call_streaming(user_input):
142
185
  console.print(chunk, end='')
186
+ response_buffer.append(chunk)
143
187
  console.print()
144
188
 
189
+ # Add assistant response to history
190
+ full_response = ''.join(response_buffer)
191
+ conversation.add_message("assistant", full_response)
192
+
193
+ # Auto-save periodically
194
+ if save and len(conversation.messages) % 10 == 0:
195
+ conv_history.save_conversation(conversation)
196
+
145
197
  except KeyboardInterrupt:
146
198
  console.print("\n\n[yellow]Interrupted. Type 'exit' to quit.[/yellow]")
147
199
  continue
148
200
 
149
201
  except Exception as e:
150
202
  console.print(f"[red]Error: {e}[/red]")
203
+ if save and conversation.messages:
204
+ conv_history.save_conversation(conversation)
205
+ sys.exit(1)
206
+
207
+
208
+ @main.group()
209
+ def completion() -> None:
210
+ """Shell completion installation."""
211
+ pass
212
+
213
+
214
+ @completion.command('install')
215
+ @click.option('--shell', type=click.Choice(['bash', 'zsh', 'fish', 'auto']), default='auto',
216
+ help='Shell type (auto-detects if not specified)')
217
+ @click.pass_context
218
+ def completion_install(ctx: click.Context, shell: str) -> None:
219
+ """Install shell completion for cdc command."""
220
+ console = ctx.obj['console']
221
+
222
+ # Auto-detect shell if needed
223
+ if shell == 'auto':
224
+ shell_path = os.environ.get('SHELL', '')
225
+ if 'zsh' in shell_path:
226
+ shell = 'zsh'
227
+ elif 'bash' in shell_path:
228
+ shell = 'bash'
229
+ elif 'fish' in shell_path:
230
+ shell = 'fish'
231
+ else:
232
+ console.print("[red]Could not auto-detect shell[/red]")
233
+ console.print("Please specify: --shell bash|zsh|fish")
234
+ sys.exit(1)
235
+
236
+ console.print(f"[cyan]Installing completion for {shell}...[/cyan]\n")
237
+
238
+ if shell == 'zsh':
239
+ console.print("Add this to your ~/.zshrc:\n")
240
+ console.print("[yellow]eval \"$(_CDC_COMPLETE=zsh_source cdc)\"[/yellow]\n")
241
+ console.print("Then run: [cyan]source ~/.zshrc[/cyan]")
242
+ elif shell == 'bash':
243
+ console.print("Add this to your ~/.bashrc:\n")
244
+ console.print("[yellow]eval \"$(_CDC_COMPLETE=bash_source cdc)\"[/yellow]\n")
245
+ console.print("Then run: [cyan]source ~/.bashrc[/cyan]")
246
+ elif shell == 'fish':
247
+ console.print("Add this to ~/.config/fish/completions/cdc.fish:\n")
248
+ console.print("[yellow]_CDC_COMPLETE=fish_source cdc | source[/yellow]\n")
249
+ console.print("Then reload: [cyan]exec fish[/cyan]")
250
+
251
+ console.print("\n[green]✓[/green] Instructions displayed above")
252
+ console.print("[dim]Completion will provide command and option suggestions[/dim]")
253
+
254
+
255
+ @completion.command('generate')
256
+ @click.option('--shell', type=click.Choice(['bash', 'zsh', 'fish']), required=True,
257
+ help='Shell type')
258
+ @click.pass_context
259
+ def completion_generate(ctx: click.Context, shell: str) -> None:
260
+ """Generate completion script for shell."""
261
+ import subprocess
262
+ import sys
263
+
264
+ env_var = f"_CDC_COMPLETE={shell}_source"
265
+ result = subprocess.run(
266
+ [sys.executable, '-m', 'claude_dev_cli.cli'],
267
+ env={**os.environ, '_CDC_COMPLETE': f'{shell}_source'},
268
+ capture_output=True,
269
+ text=True
270
+ )
271
+
272
+ if result.returncode == 0:
273
+ click.echo(result.stdout)
274
+ else:
275
+ ctx.obj['console'].print(f"[red]Error generating completion: {result.stderr}[/red]")
276
+ sys.exit(1)
277
+
278
+
279
+ @main.group()
280
+ def history() -> None:
281
+ """Manage conversation history."""
282
+ pass
283
+
284
+
285
+ @history.command('list')
286
+ @click.option('-n', '--limit', type=int, default=10, help='Number of conversations to show')
287
+ @click.option('-s', '--search', help='Search conversations')
288
+ @click.pass_context
289
+ def history_list(ctx: click.Context, limit: int, search: Optional[str]) -> None:
290
+ """List conversation history."""
291
+ console = ctx.obj['console']
292
+ config = Config()
293
+ conv_history = ConversationHistory(config.config_dir / "history")
294
+
295
+ conversations = conv_history.list_conversations(limit=limit, search_query=search)
296
+
297
+ if not conversations:
298
+ console.print("[yellow]No conversations found[/yellow]")
299
+ return
300
+
301
+ for conv in conversations:
302
+ summary = conv.get_summary(80)
303
+ console.print(f"\n[cyan]{conv.conversation_id}[/cyan]")
304
+ console.print(f"[dim]{conv.updated_at.strftime('%Y-%m-%d %H:%M')} | {len(conv.messages)} messages[/dim]")
305
+ console.print(f" {summary}")
306
+
307
+
308
+ @history.command('export')
309
+ @click.argument('conversation_id')
310
+ @click.option('--format', type=click.Choice(['markdown', 'json']), default='markdown')
311
+ @click.option('-o', '--output', type=click.Path(), help='Output file')
312
+ @click.pass_context
313
+ def history_export(ctx: click.Context, conversation_id: str, format: str, output: Optional[str]) -> None:
314
+ """Export a conversation."""
315
+ console = ctx.obj['console']
316
+ config = Config()
317
+ conv_history = ConversationHistory(config.config_dir / "history")
318
+
319
+ content = conv_history.export_conversation(conversation_id, format)
320
+ if not content:
321
+ console.print(f"[red]Conversation {conversation_id} not found[/red]")
151
322
  sys.exit(1)
323
+
324
+ if output:
325
+ Path(output).write_text(content)
326
+ console.print(f"[green]✓[/green] Exported to {output}")
327
+ else:
328
+ click.echo(content)
152
329
 
153
330
 
154
331
  @main.group()
@@ -577,5 +754,208 @@ def toon_info(ctx: click.Context) -> None:
577
754
  console.print("• Same data, fewer tokens")
578
755
 
579
756
 
757
+ @main.group()
758
+ def template() -> None:
759
+ """Manage custom prompt templates."""
760
+ pass
761
+
762
+
763
+ @template.command('list')
764
+ @click.option('-c', '--category', help='Filter by category')
765
+ @click.option('--builtin', is_flag=True, help='Show only built-in templates')
766
+ @click.option('--user', is_flag=True, help='Show only user templates')
767
+ @click.pass_context
768
+ def template_list(
769
+ ctx: click.Context,
770
+ category: Optional[str],
771
+ builtin: bool,
772
+ user: bool
773
+ ) -> None:
774
+ """List available templates."""
775
+ console = ctx.obj['console']
776
+ config = Config()
777
+ manager = TemplateManager(config.config_dir)
778
+
779
+ templates = manager.list_templates(
780
+ category=category,
781
+ builtin_only=builtin,
782
+ user_only=user
783
+ )
784
+
785
+ if not templates:
786
+ console.print("[yellow]No templates found.[/yellow]")
787
+ return
788
+
789
+ from rich.table import Table
790
+
791
+ table = Table(show_header=True, header_style="bold magenta")
792
+ table.add_column("Name", style="cyan")
793
+ table.add_column("Category", style="green")
794
+ table.add_column("Variables", style="yellow")
795
+ table.add_column("Type", style="blue")
796
+ table.add_column("Description")
797
+
798
+ for tmpl in templates:
799
+ vars_display = ", ".join(tmpl.variables) if tmpl.variables else "-"
800
+ type_display = "🔒 Built-in" if tmpl.builtin else "📝 User"
801
+ table.add_row(
802
+ tmpl.name,
803
+ tmpl.category,
804
+ vars_display,
805
+ type_display,
806
+ tmpl.description
807
+ )
808
+
809
+ console.print(table)
810
+
811
+ # Show categories
812
+ categories = manager.get_categories()
813
+ console.print(f"\n[dim]Categories: {', '.join(categories)}[/dim]")
814
+
815
+
816
+ @template.command('show')
817
+ @click.argument('name')
818
+ @click.pass_context
819
+ def template_show(ctx: click.Context, name: str) -> None:
820
+ """Show template details."""
821
+ console = ctx.obj['console']
822
+ config = Config()
823
+ manager = TemplateManager(config.config_dir)
824
+
825
+ tmpl = manager.get_template(name)
826
+ if not tmpl:
827
+ console.print(f"[red]Template not found: {name}[/red]")
828
+ sys.exit(1)
829
+
830
+ console.print(Panel(
831
+ f"[bold]{tmpl.name}[/bold]\n\n"
832
+ f"[dim]{tmpl.description}[/dim]\n\n"
833
+ f"Category: [green]{tmpl.category}[/green]\n"
834
+ f"Type: {'🔒 Built-in' if tmpl.builtin else '📝 User'}\n"
835
+ f"Variables: [yellow]{', '.join(tmpl.variables) if tmpl.variables else 'None'}[/yellow]",
836
+ title="Template Info",
837
+ border_style="blue"
838
+ ))
839
+
840
+ console.print("\n[bold]Content:[/bold]\n")
841
+ console.print(Panel(tmpl.content, border_style="dim"))
842
+
843
+
844
+ @template.command('add')
845
+ @click.argument('name')
846
+ @click.option('-c', '--content', help='Template content (or use stdin)')
847
+ @click.option('-d', '--description', help='Template description')
848
+ @click.option('--category', default='general', help='Template category')
849
+ @click.pass_context
850
+ def template_add(
851
+ ctx: click.Context,
852
+ name: str,
853
+ content: Optional[str],
854
+ description: Optional[str],
855
+ category: str
856
+ ) -> None:
857
+ """Add a new template."""
858
+ console = ctx.obj['console']
859
+ config = Config()
860
+ manager = TemplateManager(config.config_dir)
861
+
862
+ # Get content from stdin if not provided
863
+ if not content:
864
+ if sys.stdin.isatty():
865
+ console.print("[yellow]Enter template content (Ctrl+D to finish):[/yellow]")
866
+ content = sys.stdin.read().strip()
867
+
868
+ if not content:
869
+ console.print("[red]Error: No content provided[/red]")
870
+ sys.exit(1)
871
+
872
+ try:
873
+ tmpl = Template(
874
+ name=name,
875
+ content=content,
876
+ description=description,
877
+ category=category
878
+ )
879
+ manager.add_template(tmpl)
880
+
881
+ console.print(f"[green]✓[/green] Template added: {name}")
882
+ if tmpl.variables:
883
+ console.print(f"[dim]Variables: {', '.join(tmpl.variables)}[/dim]")
884
+
885
+ except ValueError as e:
886
+ console.print(f"[red]Error: {e}[/red]")
887
+ sys.exit(1)
888
+
889
+
890
+ @template.command('delete')
891
+ @click.argument('name')
892
+ @click.pass_context
893
+ def template_delete(ctx: click.Context, name: str) -> None:
894
+ """Delete a user template."""
895
+ console = ctx.obj['console']
896
+ config = Config()
897
+ manager = TemplateManager(config.config_dir)
898
+
899
+ try:
900
+ if manager.delete_template(name):
901
+ console.print(f"[green]✓[/green] Template deleted: {name}")
902
+ else:
903
+ console.print(f"[red]Template not found: {name}[/red]")
904
+ sys.exit(1)
905
+
906
+ except ValueError as e:
907
+ console.print(f"[red]Error: {e}[/red]")
908
+ sys.exit(1)
909
+
910
+
911
+ @template.command('use')
912
+ @click.argument('name')
913
+ @click.option('-a', '--api', help='API config to use')
914
+ @click.option('-m', '--model', help='Claude model to use')
915
+ @click.pass_context
916
+ def template_use(ctx: click.Context, name: str, api: Optional[str], model: Optional[str]) -> None:
917
+ """Use a template with interactive variable input."""
918
+ console = ctx.obj['console']
919
+ config = Config()
920
+ manager = TemplateManager(config.config_dir)
921
+
922
+ tmpl = manager.get_template(name)
923
+ if not tmpl:
924
+ console.print(f"[red]Template not found: {name}[/red]")
925
+ sys.exit(1)
926
+
927
+ # Get variable values
928
+ variables = {}
929
+ if tmpl.variables:
930
+ console.print(f"\n[bold]Template: {name}[/bold]")
931
+ console.print(f"[dim]{tmpl.description}[/dim]\n")
932
+
933
+ for var in tmpl.variables:
934
+ value = console.input(f"[cyan]{var}:[/cyan] ").strip()
935
+ variables[var] = value
936
+
937
+ # Check for missing variables
938
+ missing = tmpl.get_missing_variables(**variables)
939
+ if missing:
940
+ console.print(f"[red]Missing required variables: {', '.join(missing)}[/red]")
941
+ sys.exit(1)
942
+
943
+ # Render template
944
+ prompt = tmpl.render(**variables)
945
+
946
+ # Call Claude
947
+ try:
948
+ client = ClaudeClient(api_config_name=api)
949
+
950
+ console.print("\n[bold green]Claude:[/bold green] ", end='')
951
+ for chunk in client.call_streaming(prompt, model=model):
952
+ console.print(chunk, end='')
953
+ console.print()
954
+
955
+ except Exception as e:
956
+ console.print(f"\n[red]Error: {e}[/red]")
957
+ sys.exit(1)
958
+
959
+
580
960
  if __name__ == '__main__':
581
961
  main(obj={})
@@ -0,0 +1,189 @@
1
+ """Conversation history management for interactive mode."""
2
+
3
+ import json
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import List, Optional, Dict, Any
7
+
8
+
9
+ class Message:
10
+ """Represents a single message in a conversation."""
11
+
12
+ def __init__(self, role: str, content: str, timestamp: Optional[datetime] = None):
13
+ self.role = role # "user" or "assistant"
14
+ self.content = content
15
+ self.timestamp = timestamp or datetime.utcnow()
16
+
17
+ def to_dict(self) -> Dict[str, Any]:
18
+ """Convert to dictionary for storage."""
19
+ return {
20
+ "role": self.role,
21
+ "content": self.content,
22
+ "timestamp": self.timestamp.isoformat()
23
+ }
24
+
25
+ @classmethod
26
+ def from_dict(cls, data: Dict[str, Any]) -> "Message":
27
+ """Create from dictionary."""
28
+ return cls(
29
+ role=data["role"],
30
+ content=data["content"],
31
+ timestamp=datetime.fromisoformat(data["timestamp"])
32
+ )
33
+
34
+
35
+ class Conversation:
36
+ """Represents a conversation with messages."""
37
+
38
+ def __init__(
39
+ self,
40
+ conversation_id: Optional[str] = None,
41
+ created_at: Optional[datetime] = None,
42
+ updated_at: Optional[datetime] = None
43
+ ):
44
+ self.conversation_id = conversation_id or datetime.utcnow().strftime("%Y%m%d_%H%M%S")
45
+ self.created_at = created_at or datetime.utcnow()
46
+ self.updated_at = updated_at or datetime.utcnow()
47
+ self.messages: List[Message] = []
48
+
49
+ def add_message(self, role: str, content: str) -> None:
50
+ """Add a message to the conversation."""
51
+ message = Message(role, content)
52
+ self.messages.append(message)
53
+ self.updated_at = datetime.utcnow()
54
+
55
+ def get_summary(self, max_length: int = 100) -> str:
56
+ """Get a summary of the conversation (first user message)."""
57
+ for msg in self.messages:
58
+ if msg.role == "user":
59
+ summary = msg.content[:max_length]
60
+ if len(msg.content) > max_length:
61
+ summary += "..."
62
+ return summary
63
+ return "(empty conversation)"
64
+
65
+ def to_dict(self) -> Dict[str, Any]:
66
+ """Convert to dictionary for storage."""
67
+ return {
68
+ "conversation_id": self.conversation_id,
69
+ "created_at": self.created_at.isoformat(),
70
+ "updated_at": self.updated_at.isoformat(),
71
+ "messages": [msg.to_dict() for msg in self.messages]
72
+ }
73
+
74
+ @classmethod
75
+ def from_dict(cls, data: Dict[str, Any]) -> "Conversation":
76
+ """Create from dictionary."""
77
+ conv = cls(
78
+ conversation_id=data["conversation_id"],
79
+ created_at=datetime.fromisoformat(data["created_at"]),
80
+ updated_at=datetime.fromisoformat(data["updated_at"])
81
+ )
82
+ conv.messages = [Message.from_dict(msg) for msg in data.get("messages", [])]
83
+ return conv
84
+
85
+
86
+ class ConversationHistory:
87
+ """Manages conversation history storage and retrieval."""
88
+
89
+ def __init__(self, history_dir: Path):
90
+ self.history_dir = history_dir
91
+ self.history_dir.mkdir(parents=True, exist_ok=True)
92
+
93
+ def _get_conversation_file(self, conversation_id: str) -> Path:
94
+ """Get the file path for a conversation."""
95
+ return self.history_dir / f"{conversation_id}.json"
96
+
97
+ def save_conversation(self, conversation: Conversation) -> None:
98
+ """Save a conversation to disk."""
99
+ file_path = self._get_conversation_file(conversation.conversation_id)
100
+ with open(file_path, 'w') as f:
101
+ json.dump(conversation.to_dict(), f, indent=2)
102
+
103
+ def load_conversation(self, conversation_id: str) -> Optional[Conversation]:
104
+ """Load a conversation from disk."""
105
+ file_path = self._get_conversation_file(conversation_id)
106
+ if not file_path.exists():
107
+ return None
108
+
109
+ try:
110
+ with open(file_path, 'r') as f:
111
+ data = json.load(f)
112
+ return Conversation.from_dict(data)
113
+ except Exception:
114
+ return None
115
+
116
+ def list_conversations(
117
+ self,
118
+ limit: Optional[int] = None,
119
+ search_query: Optional[str] = None
120
+ ) -> List[Conversation]:
121
+ """List all conversations, optionally filtered and limited."""
122
+ conversations = []
123
+
124
+ for file_path in sorted(self.history_dir.glob("*.json"), reverse=True):
125
+ try:
126
+ with open(file_path, 'r') as f:
127
+ data = json.load(f)
128
+ conv = Conversation.from_dict(data)
129
+
130
+ # Apply search filter if provided
131
+ if search_query:
132
+ search_lower = search_query.lower()
133
+ found = False
134
+ for msg in conv.messages:
135
+ if search_lower in msg.content.lower():
136
+ found = True
137
+ break
138
+ if not found:
139
+ continue
140
+
141
+ conversations.append(conv)
142
+
143
+ if limit and len(conversations) >= limit:
144
+ break
145
+ except Exception:
146
+ continue
147
+
148
+ return conversations
149
+
150
+ def delete_conversation(self, conversation_id: str) -> bool:
151
+ """Delete a conversation."""
152
+ file_path = self._get_conversation_file(conversation_id)
153
+ if file_path.exists():
154
+ file_path.unlink()
155
+ return True
156
+ return False
157
+
158
+ def get_latest_conversation(self) -> Optional[Conversation]:
159
+ """Get the most recent conversation."""
160
+ conversations = self.list_conversations(limit=1)
161
+ return conversations[0] if conversations else None
162
+
163
+ def export_conversation(
164
+ self,
165
+ conversation_id: str,
166
+ output_format: str = "markdown"
167
+ ) -> Optional[str]:
168
+ """Export a conversation to a specific format."""
169
+ conv = self.load_conversation(conversation_id)
170
+ if not conv:
171
+ return None
172
+
173
+ if output_format == "markdown":
174
+ lines = [f"# Conversation: {conv.conversation_id}"]
175
+ lines.append(f"\nCreated: {conv.created_at.strftime('%Y-%m-%d %H:%M:%S')}")
176
+ lines.append(f"Updated: {conv.updated_at.strftime('%Y-%m-%d %H:%M:%S')}\n")
177
+
178
+ for msg in conv.messages:
179
+ role_display = "**You:**" if msg.role == "user" else "**Claude:**"
180
+ lines.append(f"\n## {role_display}\n")
181
+ lines.append(msg.content)
182
+ lines.append("")
183
+
184
+ return "\n".join(lines)
185
+
186
+ elif output_format == "json":
187
+ return json.dumps(conv.to_dict(), indent=2)
188
+
189
+ return None
@@ -0,0 +1,288 @@
1
+ """Template management for reusable prompts."""
2
+
3
+ import json
4
+ import re
5
+ from pathlib import Path
6
+ from typing import Dict, List, Optional, Any
7
+
8
+
9
+ class Template:
10
+ """Represents a reusable prompt template."""
11
+
12
+ def __init__(
13
+ self,
14
+ name: str,
15
+ content: str,
16
+ description: Optional[str] = None,
17
+ variables: Optional[List[str]] = None,
18
+ category: Optional[str] = None,
19
+ builtin: bool = False
20
+ ):
21
+ self.name = name
22
+ self.content = content
23
+ self.description = description or ""
24
+ self.variables = variables or self._extract_variables(content)
25
+ self.category = category or "general"
26
+ self.builtin = builtin
27
+
28
+ @staticmethod
29
+ def _extract_variables(content: str) -> List[str]:
30
+ """Extract {{variable}} placeholders from content."""
31
+ return list(set(re.findall(r'\{\{(\w+)\}\}', content)))
32
+
33
+ def render(self, **kwargs: str) -> str:
34
+ """Render template with provided variables."""
35
+ result = self.content
36
+ for var, value in kwargs.items():
37
+ result = result.replace(f'{{{{{var}}}}}', value)
38
+ return result
39
+
40
+ def get_missing_variables(self, **kwargs: str) -> List[str]:
41
+ """Get list of required variables not provided."""
42
+ return [var for var in self.variables if var not in kwargs]
43
+
44
+ def to_dict(self) -> Dict[str, Any]:
45
+ """Convert to dictionary for storage."""
46
+ return {
47
+ "name": self.name,
48
+ "content": self.content,
49
+ "description": self.description,
50
+ "variables": self.variables,
51
+ "category": self.category,
52
+ "builtin": self.builtin
53
+ }
54
+
55
+ @classmethod
56
+ def from_dict(cls, data: Dict[str, Any]) -> "Template":
57
+ """Create from dictionary."""
58
+ return cls(
59
+ name=data["name"],
60
+ content=data["content"],
61
+ description=data.get("description", ""),
62
+ variables=data.get("variables", []),
63
+ category=data.get("category", "general"),
64
+ builtin=data.get("builtin", False)
65
+ )
66
+
67
+
68
+ class TemplateManager:
69
+ """Manages template storage and retrieval."""
70
+
71
+ # Built-in templates
72
+ BUILTIN_TEMPLATES = [
73
+ Template(
74
+ name="code-review",
75
+ content="""Review this code for:
76
+ - Security vulnerabilities
77
+ - Performance issues
78
+ - Best practices
79
+ - Potential bugs
80
+ - Code clarity
81
+
82
+ {{code}}
83
+
84
+ Focus on: {{focus}}""",
85
+ description="Comprehensive code review with customizable focus",
86
+ category="review",
87
+ builtin=True
88
+ ),
89
+ Template(
90
+ name="code-review-security",
91
+ content="""Perform a security-focused code review of this code:
92
+
93
+ {{code}}
94
+
95
+ Check for:
96
+ - SQL injection vulnerabilities
97
+ - XSS vulnerabilities
98
+ - Authentication/authorization issues
99
+ - Data validation problems
100
+ - Sensitive data exposure
101
+ - CSRF vulnerabilities""",
102
+ description="Security-focused code review",
103
+ category="review",
104
+ builtin=True
105
+ ),
106
+ Template(
107
+ name="test-strategy",
108
+ content="""Generate a comprehensive test strategy for this {{language}} code:
109
+
110
+ {{code}}
111
+
112
+ Include:
113
+ - Unit tests for core functionality
114
+ - Edge cases and error handling
115
+ - Integration test scenarios
116
+ - Mock/stub suggestions
117
+ - Test data examples""",
118
+ description="Generate testing strategy and test cases",
119
+ category="testing",
120
+ builtin=True
121
+ ),
122
+ Template(
123
+ name="debug-error",
124
+ content="""Help me debug this error:
125
+
126
+ Error: {{error}}
127
+
128
+ Code context:
129
+ {{code}}
130
+
131
+ Please:
132
+ 1. Explain what's causing the error
133
+ 2. Suggest fixes with code examples
134
+ 3. Explain how to prevent similar errors""",
135
+ description="Debug error with context",
136
+ category="debugging",
137
+ builtin=True
138
+ ),
139
+ Template(
140
+ name="optimize-performance",
141
+ content="""Analyze this code for performance optimization:
142
+
143
+ {{code}}
144
+
145
+ Consider:
146
+ - Time complexity improvements
147
+ - Memory usage optimization
148
+ - Algorithm efficiency
149
+ - Database query optimization (if applicable)
150
+ - Caching opportunities
151
+
152
+ Provide specific code improvements.""",
153
+ description="Performance optimization analysis",
154
+ category="optimization",
155
+ builtin=True
156
+ ),
157
+ Template(
158
+ name="refactor-clean",
159
+ content="""Refactor this code to improve:
160
+ - Readability
161
+ - Maintainability
162
+ - Code organization
163
+ - Naming conventions
164
+ - {{language}} idioms
165
+
166
+ {{code}}
167
+
168
+ Provide the refactored version with explanations.""",
169
+ description="Clean code refactoring",
170
+ category="refactoring",
171
+ builtin=True
172
+ ),
173
+ Template(
174
+ name="explain-code",
175
+ content="""Explain this code in detail:
176
+
177
+ {{code}}
178
+
179
+ Include:
180
+ - What it does (high-level)
181
+ - How it works (step-by-step)
182
+ - Why certain approaches were used
183
+ - Potential improvements
184
+
185
+ Audience level: {{level}}""",
186
+ description="Detailed code explanation",
187
+ category="documentation",
188
+ builtin=True
189
+ ),
190
+ Template(
191
+ name="api-design",
192
+ content="""Design a {{style}} API for:
193
+
194
+ {{description}}
195
+
196
+ Include:
197
+ - Endpoint definitions
198
+ - Request/response formats
199
+ - Error handling
200
+ - Authentication approach
201
+ - Rate limiting considerations""",
202
+ description="API design assistance",
203
+ category="design",
204
+ builtin=True
205
+ ),
206
+ ]
207
+
208
+ def __init__(self, templates_dir: Path):
209
+ self.templates_dir = templates_dir
210
+ self.templates_file = templates_dir / "templates.json"
211
+ self.templates_dir.mkdir(parents=True, exist_ok=True)
212
+ self._load_templates()
213
+
214
+ def _load_templates(self) -> None:
215
+ """Load templates from disk."""
216
+ self.templates: Dict[str, Template] = {}
217
+
218
+ # Load built-in templates
219
+ for template in self.BUILTIN_TEMPLATES:
220
+ self.templates[template.name] = template
221
+
222
+ # Load user templates
223
+ if self.templates_file.exists():
224
+ try:
225
+ with open(self.templates_file, 'r') as f:
226
+ data = json.load(f)
227
+ for template_data in data.get("templates", []):
228
+ template = Template.from_dict(template_data)
229
+ self.templates[template.name] = template
230
+ except Exception:
231
+ pass
232
+
233
+ def _save_templates(self) -> None:
234
+ """Save user templates to disk."""
235
+ # Only save non-builtin templates
236
+ user_templates = [
237
+ t.to_dict() for t in self.templates.values() if not t.builtin
238
+ ]
239
+
240
+ with open(self.templates_file, 'w') as f:
241
+ json.dump({"templates": user_templates}, f, indent=2)
242
+
243
+ def add_template(self, template: Template) -> None:
244
+ """Add or update a template."""
245
+ if template.name in self.templates and self.templates[template.name].builtin:
246
+ raise ValueError(f"Cannot override builtin template: {template.name}")
247
+
248
+ self.templates[template.name] = template
249
+ self._save_templates()
250
+
251
+ def get_template(self, name: str) -> Optional[Template]:
252
+ """Get a template by name."""
253
+ return self.templates.get(name)
254
+
255
+ def list_templates(
256
+ self,
257
+ category: Optional[str] = None,
258
+ builtin_only: bool = False,
259
+ user_only: bool = False
260
+ ) -> List[Template]:
261
+ """List templates with optional filters."""
262
+ templates = list(self.templates.values())
263
+
264
+ if category:
265
+ templates = [t for t in templates if t.category == category]
266
+
267
+ if builtin_only:
268
+ templates = [t for t in templates if t.builtin]
269
+ elif user_only:
270
+ templates = [t for t in templates if not t.builtin]
271
+
272
+ return sorted(templates, key=lambda t: (t.category, t.name))
273
+
274
+ def delete_template(self, name: str) -> bool:
275
+ """Delete a template (cannot delete builtins)."""
276
+ if name not in self.templates:
277
+ return False
278
+
279
+ if self.templates[name].builtin:
280
+ raise ValueError(f"Cannot delete builtin template: {name}")
281
+
282
+ del self.templates[name]
283
+ self._save_templates()
284
+ return True
285
+
286
+ def get_categories(self) -> List[str]:
287
+ """Get list of all template categories."""
288
+ return sorted(set(t.category for t in self.templates.values()))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-dev-cli
3
- Version: 0.4.0
3
+ Version: 0.6.0
4
4
  Summary: A powerful CLI tool for developers using Claude AI with multi-API routing, test generation, code review, and usage tracking
5
5
  Author-email: Julio <thinmanj@users.noreply.github.com>
6
6
  License: MIT
@@ -72,6 +72,12 @@ A powerful command-line tool for developers using Claude AI with multi-API routi
72
72
  - **Interactive**: Chat mode with conversation history
73
73
  - **Streaming**: Real-time responses
74
74
 
75
+ ### 📝 Custom Templates
76
+ - **Built-in Templates**: 8 pre-built templates for common tasks (code review, testing, debugging, etc.)
77
+ - **User Templates**: Create and manage your own reusable prompt templates
78
+ - **Variable Substitution**: Use {{variable}} placeholders for dynamic content
79
+ - **Categories**: Organize templates by category (review, testing, debugging, etc.)
80
+
75
81
  ### 🎒 TOON Format Support (Optional)
76
82
  - **Token Reduction**: 30-60% fewer tokens than JSON
77
83
  - **Cost Savings**: Reduce API costs significantly
@@ -156,7 +162,44 @@ git add .
156
162
  cdc git commit
157
163
  ```
158
164
 
159
- ### 4. Usage Tracking
165
+ ### 5. Custom Templates
166
+
167
+ ```bash
168
+ # List all templates (built-in and user)
169
+ cdc template list
170
+
171
+ # Show template details
172
+ cdc template show code-review
173
+
174
+ # Add a custom template
175
+ cdc template add my-review -c "Review this code for {{focus}}: {{code}}" \
176
+ -d "Custom review template" --category review
177
+
178
+ # Use a template (interactive variable input)
179
+ cdc template use debug-error
180
+
181
+ # Delete a user template
182
+ cdc template delete my-review
183
+
184
+ # Filter by category
185
+ cdc template list --category review
186
+
187
+ # Show only user templates
188
+ cdc template list --user
189
+ ```
190
+
191
+ #### Built-in Templates
192
+
193
+ - **code-review**: Comprehensive code review with customizable focus
194
+ - **code-review-security**: Security-focused code review
195
+ - **test-strategy**: Generate testing strategy and test cases
196
+ - **debug-error**: Debug error with context
197
+ - **optimize-performance**: Performance optimization analysis
198
+ - **refactor-clean**: Clean code refactoring
199
+ - **explain-code**: Detailed code explanation
200
+ - **api-design**: API design assistance
201
+
202
+ ### 6. Usage Tracking
160
203
 
161
204
  ```bash
162
205
  # View all usage
@@ -169,7 +212,7 @@ cdc usage --days 7
169
212
  cdc usage --api client
170
213
  ```
171
214
 
172
- ### 5. TOON Format (Optional - Reduces Tokens by 30-60%)
215
+ ### 7. TOON Format (Optional - Reduces Tokens by 30-60%)
173
216
 
174
217
  ```bash
175
218
  # Check if TOON is installed
@@ -322,6 +365,16 @@ When using a client's Enterprise API:
322
365
  | `cdc debug -f <file> -e <error>` | Debug code |
323
366
  | `cdc refactor <file>` | Refactoring suggestions |
324
367
 
368
+ ### Templates
369
+
370
+ | Command | Description |
371
+ |---------|-------------|
372
+ | `cdc template list` | List all templates |
373
+ | `cdc template show <name>` | Show template details |
374
+ | `cdc template add <name>` | Add new template |
375
+ | `cdc template delete <name>` | Delete user template |
376
+ | `cdc template use <name>` | Use template interactively |
377
+
325
378
  ### Git Helpers
326
379
 
327
380
  | Command | Description |
@@ -1,9 +1,11 @@
1
- claude_dev_cli/__init__.py,sha256=2ulyIQ3E-s6wBTKyeXAlqHMVA73zUGdaaNUsFiJ-nqs,469
2
- claude_dev_cli/cli.py,sha256=KeBRudNvKU7RzO_m1w50WyiKwAMEp3U9LNal0R_FEGk,18194
1
+ claude_dev_cli/__init__.py,sha256=8WXldNvP94ff1oD0MSxSxb5h0bEF-eJZyVHPopW8gq8,469
2
+ claude_dev_cli/cli.py,sha256=KJbUDQl_hA7jgLGxXQs01MW6inzevQk6d7_mUvvAAB8,31296
3
3
  claude_dev_cli/commands.py,sha256=RKGx2rv56PM6eErvA2uoQ20hY8babuI5jav8nCUyUOk,3964
4
4
  claude_dev_cli/config.py,sha256=RGX0sKplHUsrJJmU-4FuWWjoTbQVgWaMT8DgRUofrR4,8134
5
5
  claude_dev_cli/core.py,sha256=yaLjEixDvPzvUy4fJ2UB7nMpPPLyKACjR-RuM-1OQBY,4780
6
+ claude_dev_cli/history.py,sha256=iQlqgTnXCsyCq5q-XaDl7V5MyPKQ3bx7o_k76-xWSAA,6863
6
7
  claude_dev_cli/secure_storage.py,sha256=TK3WOaU7a0yTOtzdP_t_28fDRp2lovANNAC6MBdm4nQ,7096
8
+ claude_dev_cli/template_manager.py,sha256=ZFXOtRIoB6hpf8kLSF9TWJfvUPJt9b-PyEv3qTBK7Zs,8600
7
9
  claude_dev_cli/templates.py,sha256=lKxH943ySfUKgyHaWa4W3LVv91SgznKgajRtSRp_4UY,2260
8
10
  claude_dev_cli/toon_utils.py,sha256=S3px2UvmNEaltmTa5K-h21n2c0CPvYjZc9mc7kHGqNQ,2828
9
11
  claude_dev_cli/usage.py,sha256=32rs0_dUn6ihha3vCfT3rwnvel_-sED7jvLpO7gu-KQ,7446
@@ -12,9 +14,9 @@ claude_dev_cli/plugins/base.py,sha256=H4HQet1I-a3WLCfE9F06Lp8NuFvVoIlou7sIgyJFK-
12
14
  claude_dev_cli/plugins/diff_editor/__init__.py,sha256=gqR5S2TyIVuq-sK107fegsutQ7Z-sgAIEbtc71FhXIM,101
13
15
  claude_dev_cli/plugins/diff_editor/plugin.py,sha256=M1bUoqpasD3ZNQo36Fu_8g92uySPZyG_ujMbj5UplsU,3073
14
16
  claude_dev_cli/plugins/diff_editor/viewer.py,sha256=1IOXIKw_01ppJx5C1dQt9Kr6U1TdAHT8_iUT5r_q0NM,17169
15
- claude_dev_cli-0.4.0.dist-info/licenses/LICENSE,sha256=DGueuJwMJtMwgLO5mWlS0TaeBrFwQuNpNZ22PU9J2bw,1062
16
- claude_dev_cli-0.4.0.dist-info/METADATA,sha256=tfugybDPK3KyZTBcm8PHlEeb_ZidSWtRaKAwIBfIn70,11288
17
- claude_dev_cli-0.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
18
- claude_dev_cli-0.4.0.dist-info/entry_points.txt,sha256=zymgUIIVpFTARkFmxAuW2A4BQsNITh_L0uU-XunytHg,85
19
- claude_dev_cli-0.4.0.dist-info/top_level.txt,sha256=m7MF6LOIuTe41IT5Fgt0lc-DK1EgM4gUU_IZwWxK0pg,15
20
- claude_dev_cli-0.4.0.dist-info/RECORD,,
17
+ claude_dev_cli-0.6.0.dist-info/licenses/LICENSE,sha256=DGueuJwMJtMwgLO5mWlS0TaeBrFwQuNpNZ22PU9J2bw,1062
18
+ claude_dev_cli-0.6.0.dist-info/METADATA,sha256=d1aptyTDwcmtYPMrSgTk4j5pHCPgVriDnyjx--JjDCk,12984
19
+ claude_dev_cli-0.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
+ claude_dev_cli-0.6.0.dist-info/entry_points.txt,sha256=zymgUIIVpFTARkFmxAuW2A4BQsNITh_L0uU-XunytHg,85
21
+ claude_dev_cli-0.6.0.dist-info/top_level.txt,sha256=m7MF6LOIuTe41IT5Fgt0lc-DK1EgM4gUU_IZwWxK0pg,15
22
+ claude_dev_cli-0.6.0.dist-info/RECORD,,