claude-dev-cli 0.4.0__py3-none-any.whl → 0.5.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.

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,7 @@ 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
26
28
 
27
29
  console = Console()
28
30
 
@@ -112,11 +114,36 @@ def ask(
112
114
 
113
115
  @main.command()
114
116
  @click.option('-a', '--api', help='API config to use')
117
+ @click.option('--continue', 'continue_conversation', is_flag=True,
118
+ help='Continue the last conversation')
119
+ @click.option('--save/--no-save', default=True, help='Save conversation history')
115
120
  @click.pass_context
116
- def interactive(ctx: click.Context, api: Optional[str]) -> None:
121
+ def interactive(
122
+ ctx: click.Context,
123
+ api: Optional[str],
124
+ continue_conversation: bool,
125
+ save: bool
126
+ ) -> None:
117
127
  """Start interactive chat mode."""
118
128
  console = ctx.obj['console']
119
129
 
130
+ # Setup conversation history
131
+ config = Config()
132
+ history_dir = config.config_dir / "history"
133
+ conv_history = ConversationHistory(history_dir)
134
+
135
+ # Load or create conversation
136
+ if continue_conversation:
137
+ conversation = conv_history.get_latest_conversation()
138
+ if conversation:
139
+ console.print(f"[green]↶ Continuing conversation from {conversation.updated_at.strftime('%Y-%m-%d %H:%M')}[/green]")
140
+ console.print(f"[dim]Messages: {len(conversation.messages)}[/dim]\n")
141
+ else:
142
+ console.print("[yellow]No previous conversation found, starting new one[/yellow]\n")
143
+ conversation = Conversation()
144
+ else:
145
+ conversation = Conversation()
146
+
120
147
  console.print(Panel.fit(
121
148
  "Claude Dev CLI - Interactive Mode\n"
122
149
  "Type 'exit' or 'quit' to end\n"
@@ -127,30 +154,179 @@ def interactive(ctx: click.Context, api: Optional[str]) -> None:
127
154
 
128
155
  try:
129
156
  client = ClaudeClient(api_config_name=api)
157
+ response_buffer = []
130
158
 
131
159
  while True:
132
160
  try:
133
161
  user_input = console.input("\n[bold cyan]You:[/bold cyan] ").strip()
134
162
 
135
163
  if user_input.lower() in ['exit', 'quit']:
164
+ if save and conversation.messages:
165
+ conv_history.save_conversation(conversation)
166
+ console.print(f"\n[dim]💾 Saved conversation: {conversation.conversation_id}[/dim]")
136
167
  break
168
+
169
+ if user_input.lower() == 'clear':
170
+ conversation = Conversation()
171
+ console.print("[yellow]Conversation cleared[/yellow]")
172
+ continue
173
+
137
174
  if not user_input:
138
175
  continue
139
176
 
177
+ # Add user message to history
178
+ conversation.add_message("user", user_input)
179
+
180
+ # Get response
140
181
  console.print("\n[bold green]Claude:[/bold green] ", end='')
182
+ response_buffer = []
141
183
  for chunk in client.call_streaming(user_input):
142
184
  console.print(chunk, end='')
185
+ response_buffer.append(chunk)
143
186
  console.print()
144
187
 
188
+ # Add assistant response to history
189
+ full_response = ''.join(response_buffer)
190
+ conversation.add_message("assistant", full_response)
191
+
192
+ # Auto-save periodically
193
+ if save and len(conversation.messages) % 10 == 0:
194
+ conv_history.save_conversation(conversation)
195
+
145
196
  except KeyboardInterrupt:
146
197
  console.print("\n\n[yellow]Interrupted. Type 'exit' to quit.[/yellow]")
147
198
  continue
148
199
 
149
200
  except Exception as e:
150
201
  console.print(f"[red]Error: {e}[/red]")
202
+ if save and conversation.messages:
203
+ conv_history.save_conversation(conversation)
204
+ sys.exit(1)
205
+
206
+
207
+ @main.group()
208
+ def completion() -> None:
209
+ """Shell completion installation."""
210
+ pass
211
+
212
+
213
+ @completion.command('install')
214
+ @click.option('--shell', type=click.Choice(['bash', 'zsh', 'fish', 'auto']), default='auto',
215
+ help='Shell type (auto-detects if not specified)')
216
+ @click.pass_context
217
+ def completion_install(ctx: click.Context, shell: str) -> None:
218
+ """Install shell completion for cdc command."""
219
+ console = ctx.obj['console']
220
+
221
+ # Auto-detect shell if needed
222
+ if shell == 'auto':
223
+ shell_path = os.environ.get('SHELL', '')
224
+ if 'zsh' in shell_path:
225
+ shell = 'zsh'
226
+ elif 'bash' in shell_path:
227
+ shell = 'bash'
228
+ elif 'fish' in shell_path:
229
+ shell = 'fish'
230
+ else:
231
+ console.print("[red]Could not auto-detect shell[/red]")
232
+ console.print("Please specify: --shell bash|zsh|fish")
233
+ sys.exit(1)
234
+
235
+ console.print(f"[cyan]Installing completion for {shell}...[/cyan]\n")
236
+
237
+ if shell == 'zsh':
238
+ console.print("Add this to your ~/.zshrc:\n")
239
+ console.print("[yellow]eval \"$(_CDC_COMPLETE=zsh_source cdc)\"[/yellow]\n")
240
+ console.print("Then run: [cyan]source ~/.zshrc[/cyan]")
241
+ elif shell == 'bash':
242
+ console.print("Add this to your ~/.bashrc:\n")
243
+ console.print("[yellow]eval \"$(_CDC_COMPLETE=bash_source cdc)\"[/yellow]\n")
244
+ console.print("Then run: [cyan]source ~/.bashrc[/cyan]")
245
+ elif shell == 'fish':
246
+ console.print("Add this to ~/.config/fish/completions/cdc.fish:\n")
247
+ console.print("[yellow]_CDC_COMPLETE=fish_source cdc | source[/yellow]\n")
248
+ console.print("Then reload: [cyan]exec fish[/cyan]")
249
+
250
+ console.print("\n[green]✓[/green] Instructions displayed above")
251
+ console.print("[dim]Completion will provide command and option suggestions[/dim]")
252
+
253
+
254
+ @completion.command('generate')
255
+ @click.option('--shell', type=click.Choice(['bash', 'zsh', 'fish']), required=True,
256
+ help='Shell type')
257
+ @click.pass_context
258
+ def completion_generate(ctx: click.Context, shell: str) -> None:
259
+ """Generate completion script for shell."""
260
+ import subprocess
261
+ import sys
262
+
263
+ env_var = f"_CDC_COMPLETE={shell}_source"
264
+ result = subprocess.run(
265
+ [sys.executable, '-m', 'claude_dev_cli.cli'],
266
+ env={**os.environ, '_CDC_COMPLETE': f'{shell}_source'},
267
+ capture_output=True,
268
+ text=True
269
+ )
270
+
271
+ if result.returncode == 0:
272
+ click.echo(result.stdout)
273
+ else:
274
+ ctx.obj['console'].print(f"[red]Error generating completion: {result.stderr}[/red]")
151
275
  sys.exit(1)
152
276
 
153
277
 
278
+ @main.group()
279
+ def history() -> None:
280
+ """Manage conversation history."""
281
+ pass
282
+
283
+
284
+ @history.command('list')
285
+ @click.option('-n', '--limit', type=int, default=10, help='Number of conversations to show')
286
+ @click.option('-s', '--search', help='Search conversations')
287
+ @click.pass_context
288
+ def history_list(ctx: click.Context, limit: int, search: Optional[str]) -> None:
289
+ """List conversation history."""
290
+ console = ctx.obj['console']
291
+ config = Config()
292
+ conv_history = ConversationHistory(config.config_dir / "history")
293
+
294
+ conversations = conv_history.list_conversations(limit=limit, search_query=search)
295
+
296
+ if not conversations:
297
+ console.print("[yellow]No conversations found[/yellow]")
298
+ return
299
+
300
+ for conv in conversations:
301
+ summary = conv.get_summary(80)
302
+ console.print(f"\n[cyan]{conv.conversation_id}[/cyan]")
303
+ console.print(f"[dim]{conv.updated_at.strftime('%Y-%m-%d %H:%M')} | {len(conv.messages)} messages[/dim]")
304
+ console.print(f" {summary}")
305
+
306
+
307
+ @history.command('export')
308
+ @click.argument('conversation_id')
309
+ @click.option('--format', type=click.Choice(['markdown', 'json']), default='markdown')
310
+ @click.option('-o', '--output', type=click.Path(), help='Output file')
311
+ @click.pass_context
312
+ def history_export(ctx: click.Context, conversation_id: str, format: str, output: Optional[str]) -> None:
313
+ """Export a conversation."""
314
+ console = ctx.obj['console']
315
+ config = Config()
316
+ conv_history = ConversationHistory(config.config_dir / "history")
317
+
318
+ content = conv_history.export_conversation(conversation_id, format)
319
+ if not content:
320
+ console.print(f"[red]Conversation {conversation_id} not found[/red]")
321
+ sys.exit(1)
322
+
323
+ if output:
324
+ Path(output).write_text(content)
325
+ console.print(f"[green]✓[/green] Exported to {output}")
326
+ else:
327
+ click.echo(content)
328
+
329
+
154
330
  @main.group()
155
331
  def config() -> None:
156
332
  """Manage configuration."""
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-dev-cli
3
- Version: 0.4.0
3
+ Version: 0.5.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
@@ -1,8 +1,9 @@
1
1
  claude_dev_cli/__init__.py,sha256=2ulyIQ3E-s6wBTKyeXAlqHMVA73zUGdaaNUsFiJ-nqs,469
2
- claude_dev_cli/cli.py,sha256=KeBRudNvKU7RzO_m1w50WyiKwAMEp3U9LNal0R_FEGk,18194
2
+ claude_dev_cli/cli.py,sha256=WBJdA1KKA0scT-gOE5Sd39HhtJr7uyc3qfn_-rYXTGc,25019
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
7
8
  claude_dev_cli/templates.py,sha256=lKxH943ySfUKgyHaWa4W3LVv91SgznKgajRtSRp_4UY,2260
8
9
  claude_dev_cli/toon_utils.py,sha256=S3px2UvmNEaltmTa5K-h21n2c0CPvYjZc9mc7kHGqNQ,2828
@@ -12,9 +13,9 @@ claude_dev_cli/plugins/base.py,sha256=H4HQet1I-a3WLCfE9F06Lp8NuFvVoIlou7sIgyJFK-
12
13
  claude_dev_cli/plugins/diff_editor/__init__.py,sha256=gqR5S2TyIVuq-sK107fegsutQ7Z-sgAIEbtc71FhXIM,101
13
14
  claude_dev_cli/plugins/diff_editor/plugin.py,sha256=M1bUoqpasD3ZNQo36Fu_8g92uySPZyG_ujMbj5UplsU,3073
14
15
  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,,
16
+ claude_dev_cli-0.5.0.dist-info/licenses/LICENSE,sha256=DGueuJwMJtMwgLO5mWlS0TaeBrFwQuNpNZ22PU9J2bw,1062
17
+ claude_dev_cli-0.5.0.dist-info/METADATA,sha256=GCboIRGV9N6gFFiwotWuCFMqMbhJZs4Gk6pyztZX984,11288
18
+ claude_dev_cli-0.5.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
19
+ claude_dev_cli-0.5.0.dist-info/entry_points.txt,sha256=zymgUIIVpFTARkFmxAuW2A4BQsNITh_L0uU-XunytHg,85
20
+ claude_dev_cli-0.5.0.dist-info/top_level.txt,sha256=m7MF6LOIuTe41IT5Fgt0lc-DK1EgM4gUU_IZwWxK0pg,15
21
+ claude_dev_cli-0.5.0.dist-info/RECORD,,