claude-dev-cli 0.4.0__tar.gz → 0.5.0__tar.gz
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-0.4.0/src/claude_dev_cli.egg-info → claude_dev_cli-0.5.0}/PKG-INFO +1 -1
- {claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/pyproject.toml +1 -1
- {claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/src/claude_dev_cli/cli.py +177 -1
- claude_dev_cli-0.5.0/src/claude_dev_cli/history.py +189 -0
- {claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0/src/claude_dev_cli.egg-info}/PKG-INFO +1 -1
- {claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/src/claude_dev_cli.egg-info/SOURCES.txt +1 -0
- {claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/LICENSE +0 -0
- {claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/MANIFEST.in +0 -0
- {claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/README.md +0 -0
- {claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/setup.cfg +0 -0
- {claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/src/claude_dev_cli/__init__.py +0 -0
- {claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/src/claude_dev_cli/commands.py +0 -0
- {claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/src/claude_dev_cli/config.py +0 -0
- {claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/src/claude_dev_cli/core.py +0 -0
- {claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/src/claude_dev_cli/plugins/__init__.py +0 -0
- {claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/src/claude_dev_cli/plugins/base.py +0 -0
- {claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/src/claude_dev_cli/plugins/diff_editor/__init__.py +0 -0
- {claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/src/claude_dev_cli/plugins/diff_editor/plugin.py +0 -0
- {claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/src/claude_dev_cli/plugins/diff_editor/viewer.py +0 -0
- {claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/src/claude_dev_cli/secure_storage.py +0 -0
- {claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/src/claude_dev_cli/templates.py +0 -0
- {claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/src/claude_dev_cli/toon_utils.py +0 -0
- {claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/src/claude_dev_cli/usage.py +0 -0
- {claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/src/claude_dev_cli.egg-info/dependency_links.txt +0 -0
- {claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/src/claude_dev_cli.egg-info/entry_points.txt +0 -0
- {claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/src/claude_dev_cli.egg-info/requires.txt +0 -0
- {claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/src/claude_dev_cli.egg-info/top_level.txt +0 -0
- {claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/tests/test_cli.py +0 -0
- {claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/tests/test_commands.py +0 -0
- {claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/tests/test_config.py +0 -0
- {claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/tests/test_core.py +0 -0
- {claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/tests/test_diff_editor.py +0 -0
- {claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/tests/test_secure_storage.py +0 -0
- {claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/tests/test_toon_utils.py +0 -0
- {claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/tests/test_usage.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: claude-dev-cli
|
|
3
|
-
Version: 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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "claude-dev-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.0"
|
|
8
8
|
description = "A powerful CLI tool for developers using Claude AI with multi-API routing, test generation, code review, and usage tracking"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -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(
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/src/claude_dev_cli/plugins/diff_editor/__init__.py
RENAMED
|
File without changes
|
{claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/src/claude_dev_cli/plugins/diff_editor/plugin.py
RENAMED
|
File without changes
|
{claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/src/claude_dev_cli/plugins/diff_editor/viewer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_dev_cli-0.4.0 → claude_dev_cli-0.5.0}/src/claude_dev_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|