claude-dev-cli 0.8.2__tar.gz → 0.8.3__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.

Files changed (42) hide show
  1. {claude_dev_cli-0.8.2/src/claude_dev_cli.egg-info → claude_dev_cli-0.8.3}/PKG-INFO +59 -6
  2. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/README.md +58 -5
  3. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/pyproject.toml +1 -1
  4. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/src/claude_dev_cli/__init__.py +1 -1
  5. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/src/claude_dev_cli/cli.py +131 -0
  6. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/src/claude_dev_cli/config.py +15 -0
  7. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/src/claude_dev_cli/history.py +90 -4
  8. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3/src/claude_dev_cli.egg-info}/PKG-INFO +59 -6
  9. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/src/claude_dev_cli.egg-info/SOURCES.txt +1 -0
  10. claude_dev_cli-0.8.3/tests/test_history.py +488 -0
  11. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/LICENSE +0 -0
  12. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/MANIFEST.in +0 -0
  13. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/setup.cfg +0 -0
  14. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/src/claude_dev_cli/commands.py +0 -0
  15. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/src/claude_dev_cli/context.py +0 -0
  16. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/src/claude_dev_cli/core.py +0 -0
  17. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/src/claude_dev_cli/plugins/__init__.py +0 -0
  18. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/src/claude_dev_cli/plugins/base.py +0 -0
  19. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/src/claude_dev_cli/plugins/diff_editor/__init__.py +0 -0
  20. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/src/claude_dev_cli/plugins/diff_editor/plugin.py +0 -0
  21. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/src/claude_dev_cli/plugins/diff_editor/viewer.py +0 -0
  22. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/src/claude_dev_cli/secure_storage.py +0 -0
  23. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/src/claude_dev_cli/template_manager.py +0 -0
  24. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/src/claude_dev_cli/templates.py +0 -0
  25. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/src/claude_dev_cli/toon_utils.py +0 -0
  26. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/src/claude_dev_cli/usage.py +0 -0
  27. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/src/claude_dev_cli/warp_integration.py +0 -0
  28. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/src/claude_dev_cli/workflows.py +0 -0
  29. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/src/claude_dev_cli.egg-info/dependency_links.txt +0 -0
  30. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/src/claude_dev_cli.egg-info/entry_points.txt +0 -0
  31. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/src/claude_dev_cli.egg-info/requires.txt +0 -0
  32. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/src/claude_dev_cli.egg-info/top_level.txt +0 -0
  33. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/tests/test_cli.py +0 -0
  34. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/tests/test_commands.py +0 -0
  35. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/tests/test_config.py +0 -0
  36. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/tests/test_context.py +0 -0
  37. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/tests/test_core.py +0 -0
  38. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/tests/test_diff_editor.py +0 -0
  39. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/tests/test_secure_storage.py +0 -0
  40. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/tests/test_template_manager.py +0 -0
  41. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/tests/test_toon_utils.py +0 -0
  42. {claude_dev_cli-0.8.2 → claude_dev_cli-0.8.3}/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.8.2
3
+ Version: 0.8.3
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
@@ -44,6 +44,13 @@ Dynamic: license-file
44
44
 
45
45
  # Claude Dev CLI
46
46
 
47
+ [![PyPI version](https://badge.fury.io/py/claude-dev-cli.svg)](https://badge.fury.io/py/claude-dev-cli)
48
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
49
+ [![Tests](https://img.shields.io/badge/tests-257%20passing-brightgreen.svg)](https://github.com/thinmanj/claude-dev-cli)
50
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
51
+ [![Homebrew](https://img.shields.io/badge/homebrew-available-orange.svg)](https://github.com/thinmanj/homebrew-tap)
52
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
53
+
47
54
  A powerful command-line tool for developers using Claude AI with multi-API routing, test generation, code review, and comprehensive usage tracking.
48
55
 
49
56
  ## Features
@@ -99,19 +106,39 @@ A powerful command-line tool for developers using Claude AI with multi-API routi
99
106
 
100
107
  ## Installation
101
108
 
102
- ### Basic Installation
109
+ ### Via Homebrew (macOS/Linux)
103
110
 
104
111
  ```bash
105
- pip install claude-dev-cli
112
+ # Add the tap
113
+ brew tap thinmanj/tap
114
+
115
+ # Install
116
+ brew install claude-dev-cli
117
+
118
+ # Or in one command
119
+ brew install thinmanj/tap/claude-dev-cli
106
120
  ```
107
121
 
108
- ### With TOON Support (Recommended for Cost Savings)
122
+ ### Via pip
109
123
 
110
124
  ```bash
111
- # Install with TOON format support for 30-60% token reduction
125
+ # Basic installation
126
+ pip install claude-dev-cli
127
+
128
+ # With TOON support (30-60% token reduction)
112
129
  pip install claude-dev-cli[toon]
113
130
  ```
114
131
 
132
+ ### Via pipx (Recommended for CLI tools)
133
+
134
+ ```bash
135
+ # Isolated installation
136
+ pipx install claude-dev-cli
137
+
138
+ # With TOON support
139
+ pipx install claude-dev-cli[toon]
140
+ ```
141
+
115
142
  ## Quick Start
116
143
 
117
144
  ### 1. Set Up API Keys
@@ -266,7 +293,33 @@ cdc template list --user
266
293
  - **explain-code**: Detailed code explanation
267
294
  - **api-design**: API design assistance
268
295
 
269
- ### 6. Usage Tracking
296
+ ### 6. Conversation History & Summarization (NEW in v0.8.3)
297
+
298
+ ```bash
299
+ # List recent conversations
300
+ cdc history list
301
+
302
+ # Search conversations
303
+ cdc history list --search "python decorators"
304
+
305
+ # Export conversation
306
+ cdc history export 20240109_143022 -o chat.md
307
+
308
+ # Summarize to reduce token usage
309
+ cdc history summarize --latest
310
+ cdc history summarize 20240109_143022 --keep-recent 6
311
+
312
+ # Delete old conversations
313
+ cdc history delete 20240109_143022
314
+ ```
315
+
316
+ **Auto-Summarization** in interactive mode:
317
+ - Automatically triggers when conversation exceeds 8,000 tokens
318
+ - Keeps recent messages (default: 4 pairs), summarizes older ones
319
+ - Reduces API costs by ~30-50% in long conversations
320
+ - Shows token savings after summarization
321
+
322
+ ### 7. Usage Tracking
270
323
 
271
324
  ```bash
272
325
  # View all usage
@@ -1,5 +1,12 @@
1
1
  # Claude Dev CLI
2
2
 
3
+ [![PyPI version](https://badge.fury.io/py/claude-dev-cli.svg)](https://badge.fury.io/py/claude-dev-cli)
4
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
5
+ [![Tests](https://img.shields.io/badge/tests-257%20passing-brightgreen.svg)](https://github.com/thinmanj/claude-dev-cli)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![Homebrew](https://img.shields.io/badge/homebrew-available-orange.svg)](https://github.com/thinmanj/homebrew-tap)
8
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
9
+
3
10
  A powerful command-line tool for developers using Claude AI with multi-API routing, test generation, code review, and comprehensive usage tracking.
4
11
 
5
12
  ## Features
@@ -55,19 +62,39 @@ A powerful command-line tool for developers using Claude AI with multi-API routi
55
62
 
56
63
  ## Installation
57
64
 
58
- ### Basic Installation
65
+ ### Via Homebrew (macOS/Linux)
59
66
 
60
67
  ```bash
61
- pip install claude-dev-cli
68
+ # Add the tap
69
+ brew tap thinmanj/tap
70
+
71
+ # Install
72
+ brew install claude-dev-cli
73
+
74
+ # Or in one command
75
+ brew install thinmanj/tap/claude-dev-cli
62
76
  ```
63
77
 
64
- ### With TOON Support (Recommended for Cost Savings)
78
+ ### Via pip
65
79
 
66
80
  ```bash
67
- # Install with TOON format support for 30-60% token reduction
81
+ # Basic installation
82
+ pip install claude-dev-cli
83
+
84
+ # With TOON support (30-60% token reduction)
68
85
  pip install claude-dev-cli[toon]
69
86
  ```
70
87
 
88
+ ### Via pipx (Recommended for CLI tools)
89
+
90
+ ```bash
91
+ # Isolated installation
92
+ pipx install claude-dev-cli
93
+
94
+ # With TOON support
95
+ pipx install claude-dev-cli[toon]
96
+ ```
97
+
71
98
  ## Quick Start
72
99
 
73
100
  ### 1. Set Up API Keys
@@ -222,7 +249,33 @@ cdc template list --user
222
249
  - **explain-code**: Detailed code explanation
223
250
  - **api-design**: API design assistance
224
251
 
225
- ### 6. Usage Tracking
252
+ ### 6. Conversation History & Summarization (NEW in v0.8.3)
253
+
254
+ ```bash
255
+ # List recent conversations
256
+ cdc history list
257
+
258
+ # Search conversations
259
+ cdc history list --search "python decorators"
260
+
261
+ # Export conversation
262
+ cdc history export 20240109_143022 -o chat.md
263
+
264
+ # Summarize to reduce token usage
265
+ cdc history summarize --latest
266
+ cdc history summarize 20240109_143022 --keep-recent 6
267
+
268
+ # Delete old conversations
269
+ cdc history delete 20240109_143022
270
+ ```
271
+
272
+ **Auto-Summarization** in interactive mode:
273
+ - Automatically triggers when conversation exceeds 8,000 tokens
274
+ - Keeps recent messages (default: 4 pairs), summarizes older ones
275
+ - Reduces API costs by ~30-50% in long conversations
276
+ - Shows token savings after summarization
277
+
278
+ ### 7. Usage Tracking
226
279
 
227
280
  ```bash
228
281
  # View all usage
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "claude-dev-cli"
7
- version = "0.8.2"
7
+ version = "0.8.3"
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"
@@ -9,7 +9,7 @@ Features:
9
9
  - Interactive and single-shot modes
10
10
  """
11
11
 
12
- __version__ = "0.8.2"
12
+ __version__ = "0.8.3"
13
13
  __author__ = "Julio"
14
14
  __license__ = "MIT"
15
15
 
@@ -145,6 +145,7 @@ def interactive(
145
145
  config = Config()
146
146
  history_dir = config.config_dir / "history"
147
147
  conv_history = ConversationHistory(history_dir)
148
+ summ_config = config.get_summarization_config()
148
149
 
149
150
  # Load or create conversation
150
151
  if continue_conversation:
@@ -203,6 +204,42 @@ def interactive(
203
204
  full_response = ''.join(response_buffer)
204
205
  conversation.add_message("assistant", full_response)
205
206
 
207
+ # Check if auto-summarization is needed
208
+ if summ_config.auto_summarize and conversation.should_summarize(threshold_tokens=summ_config.threshold_tokens):
209
+ console.print("\n[yellow]⚠ Conversation getting long, summarizing older messages...[/yellow]")
210
+ old_messages, recent_messages = conversation.compress_messages(keep_recent=summ_config.keep_recent_messages)
211
+
212
+ if old_messages:
213
+ # Build summary prompt
214
+ conversation_text = []
215
+ if conversation.summary:
216
+ conversation_text.append(f"Previous summary:\n{conversation.summary}\n\n")
217
+
218
+ conversation_text.append("Conversation to summarize:\n")
219
+ for msg in old_messages:
220
+ role_name = "User" if msg.role == "user" else "Assistant"
221
+ conversation_text.append(f"{role_name}: {msg.content}\n")
222
+
223
+ summary_prompt = (
224
+ "Please provide a concise summary of this conversation that captures:"
225
+ "\n1. Main topics discussed"
226
+ "\n2. Key questions asked and answers provided"
227
+ "\n3. Important decisions or conclusions"
228
+ "\n4. Any action items or follow-ups mentioned"
229
+ "\n\nKeep the summary under 300 words but retain all important context."
230
+ "\n\n" + "".join(conversation_text)
231
+ )
232
+
233
+ # Get summary (use same client)
234
+ new_summary = client.call(summary_prompt)
235
+
236
+ # Update conversation
237
+ conversation.summary = new_summary
238
+ conversation.messages = recent_messages
239
+
240
+ tokens_saved = len("".join(conversation_text)) // 4
241
+ console.print(f"[dim]✓ Summarized older messages (~{tokens_saved:,} tokens saved)[/dim]")
242
+
206
243
  # Auto-save periodically
207
244
  if save and len(conversation.messages) % 10 == 0:
208
245
  conv_history.save_conversation(conversation)
@@ -341,6 +378,100 @@ def history_export(ctx: click.Context, conversation_id: str, format: str, output
341
378
  click.echo(content)
342
379
 
343
380
 
381
+ @history.command('summarize')
382
+ @click.argument('conversation_id', required=False)
383
+ @click.option('--keep-recent', type=int, default=4, help='Number of recent message pairs to keep')
384
+ @click.option('--latest', is_flag=True, help='Summarize the latest conversation')
385
+ @click.pass_context
386
+ def history_summarize(
387
+ ctx: click.Context,
388
+ conversation_id: Optional[str],
389
+ keep_recent: int,
390
+ latest: bool
391
+ ) -> None:
392
+ """Summarize a conversation to reduce token usage.
393
+
394
+ Older messages are compressed into an AI-generated summary while keeping
395
+ recent messages intact. This reduces context window usage and costs.
396
+
397
+ Examples:
398
+ cdc history summarize 20240109_143022
399
+ cdc history summarize --latest
400
+ cdc history summarize --latest --keep-recent 6
401
+ """
402
+ console = ctx.obj['console']
403
+ config = Config()
404
+ conv_history = ConversationHistory(config.config_dir / "history")
405
+
406
+ # Determine which conversation to summarize
407
+ if latest:
408
+ conv = conv_history.get_latest_conversation()
409
+ if not conv:
410
+ console.print("[red]No conversations found[/red]")
411
+ sys.exit(1)
412
+ conversation_id = conv.conversation_id
413
+ elif not conversation_id:
414
+ console.print("[red]Error: Provide conversation_id or use --latest[/red]")
415
+ console.print("\nUsage: cdc history summarize CONVERSATION_ID")
416
+ console.print(" or: cdc history summarize --latest")
417
+ sys.exit(1)
418
+
419
+ # Load conversation to show stats
420
+ conv = conv_history.load_conversation(conversation_id)
421
+ if not conv:
422
+ console.print(f"[red]Conversation {conversation_id} not found[/red]")
423
+ sys.exit(1)
424
+
425
+ # Show before stats
426
+ tokens_before = conv.estimate_tokens()
427
+ messages_before = len(conv.messages)
428
+ console.print(f"\n[cyan]Conversation:[/cyan] {conversation_id}")
429
+ console.print(f"[dim]Messages: {messages_before} | Estimated tokens: {tokens_before:,}[/dim]\n")
430
+
431
+ if messages_before <= keep_recent:
432
+ console.print(f"[yellow]Too few messages to summarize ({messages_before} <= {keep_recent})[/yellow]")
433
+ return
434
+
435
+ # Summarize
436
+ with console.status("[bold blue]Generating summary..."):
437
+ summary = conv_history.summarize_conversation(conversation_id, keep_recent)
438
+
439
+ if not summary:
440
+ console.print("[red]Failed to generate summary[/red]")
441
+ sys.exit(1)
442
+
443
+ # Reload and show after stats
444
+ conv = conv_history.load_conversation(conversation_id)
445
+ if conv:
446
+ tokens_after = conv.estimate_tokens()
447
+ messages_after = len(conv.messages)
448
+
449
+ token_savings = tokens_before - tokens_after
450
+ savings_percent = (token_savings / tokens_before * 100) if tokens_before > 0 else 0
451
+
452
+ console.print("[green]✓ Conversation summarized[/green]\n")
453
+ console.print(f"[dim]Messages:[/dim] {messages_before} → {messages_after}")
454
+ console.print(f"[dim]Tokens:[/dim] {tokens_before:,} → {tokens_after:,} ({token_savings:,} saved, {savings_percent:.1f}%)\n")
455
+ console.print("[bold]Summary:[/bold]")
456
+ console.print(Panel(summary, border_style="blue"))
457
+
458
+
459
+ @history.command('delete')
460
+ @click.argument('conversation_id')
461
+ @click.pass_context
462
+ def history_delete(ctx: click.Context, conversation_id: str) -> None:
463
+ """Delete a conversation."""
464
+ console = ctx.obj['console']
465
+ config = Config()
466
+ conv_history = ConversationHistory(config.config_dir / "history")
467
+
468
+ if conv_history.delete_conversation(conversation_id):
469
+ console.print(f"[green]✓[/green] Deleted conversation: {conversation_id}")
470
+ else:
471
+ console.print(f"[red]Conversation {conversation_id} not found[/red]")
472
+ sys.exit(1)
473
+
474
+
344
475
  @main.group()
345
476
  def config() -> None:
346
477
  """Manage configuration."""
@@ -21,6 +21,15 @@ class ContextConfig(BaseModel):
21
21
  include_tests: bool = True # Include test files by default
22
22
 
23
23
 
24
+ class SummarizationConfig(BaseModel):
25
+ """Conversation summarization configuration."""
26
+
27
+ auto_summarize: bool = True # Enable automatic summarization
28
+ threshold_tokens: int = 8000 # Token threshold for auto-summarization
29
+ keep_recent_messages: int = 4 # Number of recent message pairs to keep
30
+ summary_max_words: int = 300 # Maximum words in generated summary
31
+
32
+
24
33
  class APIConfig(BaseModel):
25
34
  """Configuration for a Claude API key."""
26
35
 
@@ -87,6 +96,7 @@ class Config:
87
96
  "default_model": "claude-3-5-sonnet-20241022",
88
97
  "max_tokens": 4096,
89
98
  "context": ContextConfig().model_dump(),
99
+ "summarization": SummarizationConfig().model_dump(),
90
100
  }
91
101
  self._save_config(default_config)
92
102
  return default_config
@@ -263,3 +273,8 @@ class Config:
263
273
  """Get context gathering configuration."""
264
274
  context_data = self._data.get("context", {})
265
275
  return ContextConfig(**context_data) if context_data else ContextConfig()
276
+
277
+ def get_summarization_config(self) -> SummarizationConfig:
278
+ """Get conversation summarization configuration."""
279
+ summ_data = self._data.get("summarization", {})
280
+ return SummarizationConfig(**summ_data) if summ_data else SummarizationConfig()
@@ -39,11 +39,13 @@ class Conversation:
39
39
  self,
40
40
  conversation_id: Optional[str] = None,
41
41
  created_at: Optional[datetime] = None,
42
- updated_at: Optional[datetime] = None
42
+ updated_at: Optional[datetime] = None,
43
+ summary: Optional[str] = None
43
44
  ):
44
- self.conversation_id = conversation_id or datetime.utcnow().strftime("%Y%m%d_%H%M%S")
45
+ self.conversation_id = conversation_id or datetime.utcnow().strftime("%Y%m%d_%H%M%S_%f")
45
46
  self.created_at = created_at or datetime.utcnow()
46
47
  self.updated_at = updated_at or datetime.utcnow()
48
+ self.summary = summary # AI-generated summary of older messages
47
49
  self.messages: List[Message] = []
48
50
 
49
51
  def add_message(self, role: str, content: str) -> None:
@@ -62,14 +64,37 @@ class Conversation:
62
64
  return summary
63
65
  return "(empty conversation)"
64
66
 
67
+ def estimate_tokens(self) -> int:
68
+ """Estimate token count for the conversation."""
69
+ # Rough estimation: ~4 characters per token
70
+ total_chars = len(self.summary or "")
71
+ for msg in self.messages:
72
+ total_chars += len(msg.content)
73
+ return total_chars // 4
74
+
75
+ def should_summarize(self, threshold_tokens: int = 8000) -> bool:
76
+ """Check if conversation should be summarized."""
77
+ return self.estimate_tokens() > threshold_tokens and len(self.messages) > 4
78
+
79
+ def compress_messages(self, keep_recent: int = 4) -> tuple[List[Message], List[Message]]:
80
+ """Split messages into old (to summarize) and recent (to keep)."""
81
+ if len(self.messages) <= keep_recent:
82
+ return [], self.messages
83
+
84
+ split_point = len(self.messages) - keep_recent
85
+ return self.messages[:split_point], self.messages[split_point:]
86
+
65
87
  def to_dict(self) -> Dict[str, Any]:
66
88
  """Convert to dictionary for storage."""
67
- return {
89
+ data = {
68
90
  "conversation_id": self.conversation_id,
69
91
  "created_at": self.created_at.isoformat(),
70
92
  "updated_at": self.updated_at.isoformat(),
71
93
  "messages": [msg.to_dict() for msg in self.messages]
72
94
  }
95
+ if self.summary:
96
+ data["summary"] = self.summary
97
+ return data
73
98
 
74
99
  @classmethod
75
100
  def from_dict(cls, data: Dict[str, Any]) -> "Conversation":
@@ -77,7 +102,8 @@ class Conversation:
77
102
  conv = cls(
78
103
  conversation_id=data["conversation_id"],
79
104
  created_at=datetime.fromisoformat(data["created_at"]),
80
- updated_at=datetime.fromisoformat(data["updated_at"])
105
+ updated_at=datetime.fromisoformat(data["updated_at"]),
106
+ summary=data.get("summary")
81
107
  )
82
108
  conv.messages = [Message.from_dict(msg) for msg in data.get("messages", [])]
83
109
  return conv
@@ -187,3 +213,63 @@ class ConversationHistory:
187
213
  return json.dumps(conv.to_dict(), indent=2)
188
214
 
189
215
  return None
216
+
217
+ def summarize_conversation(
218
+ self,
219
+ conversation_id: str,
220
+ keep_recent: int = 4
221
+ ) -> Optional[str]:
222
+ """Summarize older messages in a conversation, keeping recent ones.
223
+
224
+ Args:
225
+ conversation_id: The conversation to summarize
226
+ keep_recent: Number of recent message pairs to keep unsummarized
227
+
228
+ Returns:
229
+ The generated summary or None if conversation not found
230
+ """
231
+ from claude_dev_cli.core import ClaudeClient
232
+
233
+ conv = self.load_conversation(conversation_id)
234
+ if not conv:
235
+ return None
236
+
237
+ # Split messages
238
+ old_messages, recent_messages = conv.compress_messages(keep_recent)
239
+
240
+ if not old_messages:
241
+ return "No messages to summarize (too few messages)"
242
+
243
+ # Build summary prompt
244
+ conversation_text = []
245
+ if conv.summary:
246
+ conversation_text.append(f"Previous summary:\n{conv.summary}\n\n")
247
+
248
+ conversation_text.append("Conversation to summarize:\n")
249
+ for msg in old_messages:
250
+ role_name = "User" if msg.role == "user" else "Assistant"
251
+ conversation_text.append(f"{role_name}: {msg.content}\n")
252
+
253
+ prompt = (
254
+ "Please provide a concise summary of this conversation that captures:"
255
+ "\n1. Main topics discussed"
256
+ "\n2. Key questions asked and answers provided"
257
+ "\n3. Important decisions or conclusions"
258
+ "\n4. Any action items or follow-ups mentioned"
259
+ "\n\nKeep the summary under 300 words but retain all important context."
260
+ "\n\n" + "".join(conversation_text)
261
+ )
262
+
263
+ # Get summary from Claude
264
+ client = ClaudeClient()
265
+ new_summary = client.call(prompt)
266
+
267
+ # Update conversation
268
+ conv.summary = new_summary
269
+ conv.messages = recent_messages
270
+ conv.updated_at = datetime.utcnow()
271
+
272
+ # Save updated conversation
273
+ self.save_conversation(conv)
274
+
275
+ return new_summary
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-dev-cli
3
- Version: 0.8.2
3
+ Version: 0.8.3
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
@@ -44,6 +44,13 @@ Dynamic: license-file
44
44
 
45
45
  # Claude Dev CLI
46
46
 
47
+ [![PyPI version](https://badge.fury.io/py/claude-dev-cli.svg)](https://badge.fury.io/py/claude-dev-cli)
48
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
49
+ [![Tests](https://img.shields.io/badge/tests-257%20passing-brightgreen.svg)](https://github.com/thinmanj/claude-dev-cli)
50
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
51
+ [![Homebrew](https://img.shields.io/badge/homebrew-available-orange.svg)](https://github.com/thinmanj/homebrew-tap)
52
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
53
+
47
54
  A powerful command-line tool for developers using Claude AI with multi-API routing, test generation, code review, and comprehensive usage tracking.
48
55
 
49
56
  ## Features
@@ -99,19 +106,39 @@ A powerful command-line tool for developers using Claude AI with multi-API routi
99
106
 
100
107
  ## Installation
101
108
 
102
- ### Basic Installation
109
+ ### Via Homebrew (macOS/Linux)
103
110
 
104
111
  ```bash
105
- pip install claude-dev-cli
112
+ # Add the tap
113
+ brew tap thinmanj/tap
114
+
115
+ # Install
116
+ brew install claude-dev-cli
117
+
118
+ # Or in one command
119
+ brew install thinmanj/tap/claude-dev-cli
106
120
  ```
107
121
 
108
- ### With TOON Support (Recommended for Cost Savings)
122
+ ### Via pip
109
123
 
110
124
  ```bash
111
- # Install with TOON format support for 30-60% token reduction
125
+ # Basic installation
126
+ pip install claude-dev-cli
127
+
128
+ # With TOON support (30-60% token reduction)
112
129
  pip install claude-dev-cli[toon]
113
130
  ```
114
131
 
132
+ ### Via pipx (Recommended for CLI tools)
133
+
134
+ ```bash
135
+ # Isolated installation
136
+ pipx install claude-dev-cli
137
+
138
+ # With TOON support
139
+ pipx install claude-dev-cli[toon]
140
+ ```
141
+
115
142
  ## Quick Start
116
143
 
117
144
  ### 1. Set Up API Keys
@@ -266,7 +293,33 @@ cdc template list --user
266
293
  - **explain-code**: Detailed code explanation
267
294
  - **api-design**: API design assistance
268
295
 
269
- ### 6. Usage Tracking
296
+ ### 6. Conversation History & Summarization (NEW in v0.8.3)
297
+
298
+ ```bash
299
+ # List recent conversations
300
+ cdc history list
301
+
302
+ # Search conversations
303
+ cdc history list --search "python decorators"
304
+
305
+ # Export conversation
306
+ cdc history export 20240109_143022 -o chat.md
307
+
308
+ # Summarize to reduce token usage
309
+ cdc history summarize --latest
310
+ cdc history summarize 20240109_143022 --keep-recent 6
311
+
312
+ # Delete old conversations
313
+ cdc history delete 20240109_143022
314
+ ```
315
+
316
+ **Auto-Summarization** in interactive mode:
317
+ - Automatically triggers when conversation exceeds 8,000 tokens
318
+ - Keeps recent messages (default: 4 pairs), summarizes older ones
319
+ - Reduces API costs by ~30-50% in long conversations
320
+ - Shows token savings after summarization
321
+
322
+ ### 7. Usage Tracking
270
323
 
271
324
  ```bash
272
325
  # View all usage
@@ -33,6 +33,7 @@ tests/test_config.py
33
33
  tests/test_context.py
34
34
  tests/test_core.py
35
35
  tests/test_diff_editor.py
36
+ tests/test_history.py
36
37
  tests/test_secure_storage.py
37
38
  tests/test_template_manager.py
38
39
  tests/test_toon_utils.py
@@ -0,0 +1,488 @@
1
+ """Tests for conversation history and summarization."""
2
+
3
+ import json
4
+ import pytest
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+ from unittest.mock import Mock, patch, MagicMock
8
+
9
+ from claude_dev_cli.history import Message, Conversation, ConversationHistory
10
+
11
+
12
+ class TestMessage:
13
+ """Tests for Message class."""
14
+
15
+ def test_message_creation(self):
16
+ """Test creating a message."""
17
+ msg = Message("user", "Hello, Claude!")
18
+ assert msg.role == "user"
19
+ assert msg.content == "Hello, Claude!"
20
+ assert isinstance(msg.timestamp, datetime)
21
+
22
+ def test_message_to_dict(self):
23
+ """Test converting message to dictionary."""
24
+ msg = Message("assistant", "Hello! How can I help?")
25
+ data = msg.to_dict()
26
+
27
+ assert data["role"] == "assistant"
28
+ assert data["content"] == "Hello! How can I help?"
29
+ assert "timestamp" in data
30
+
31
+ def test_message_from_dict(self):
32
+ """Test creating message from dictionary."""
33
+ data = {
34
+ "role": "user",
35
+ "content": "Test message",
36
+ "timestamp": "2024-01-09T12:00:00"
37
+ }
38
+ msg = Message.from_dict(data)
39
+
40
+ assert msg.role == "user"
41
+ assert msg.content == "Test message"
42
+ assert isinstance(msg.timestamp, datetime)
43
+
44
+
45
+ class TestConversation:
46
+ """Tests for Conversation class."""
47
+
48
+ def test_conversation_creation(self):
49
+ """Test creating a conversation."""
50
+ conv = Conversation()
51
+ assert conv.conversation_id is not None
52
+ assert isinstance(conv.created_at, datetime)
53
+ assert isinstance(conv.updated_at, datetime)
54
+ assert len(conv.messages) == 0
55
+ assert conv.summary is None
56
+
57
+ def test_conversation_with_summary(self):
58
+ """Test creating conversation with summary."""
59
+ summary = "This is a test summary"
60
+ conv = Conversation(summary=summary)
61
+ assert conv.summary == summary
62
+
63
+ def test_add_message(self):
64
+ """Test adding messages to conversation."""
65
+ conv = Conversation()
66
+ conv.add_message("user", "First message")
67
+ conv.add_message("assistant", "First response")
68
+
69
+ assert len(conv.messages) == 2
70
+ assert conv.messages[0].role == "user"
71
+ assert conv.messages[0].content == "First message"
72
+ assert conv.messages[1].role == "assistant"
73
+
74
+ def test_get_summary_with_messages(self):
75
+ """Test getting conversation summary."""
76
+ conv = Conversation()
77
+ conv.add_message("user", "What is Python?")
78
+ conv.add_message("assistant", "Python is a programming language...")
79
+
80
+ summary = conv.get_summary(20)
81
+ assert summary == "What is Python?"
82
+
83
+ def test_get_summary_truncated(self):
84
+ """Test summary truncation."""
85
+ conv = Conversation()
86
+ long_message = "a" * 150
87
+ conv.add_message("user", long_message)
88
+
89
+ summary = conv.get_summary(100)
90
+ assert len(summary) <= 103 # 100 + "..."
91
+ assert summary.endswith("...")
92
+
93
+ def test_get_summary_empty(self):
94
+ """Test summary of empty conversation."""
95
+ conv = Conversation()
96
+ summary = conv.get_summary()
97
+ assert summary == "(empty conversation)"
98
+
99
+ def test_estimate_tokens_no_messages(self):
100
+ """Test token estimation for empty conversation."""
101
+ conv = Conversation()
102
+ tokens = conv.estimate_tokens()
103
+ assert tokens == 0
104
+
105
+ def test_estimate_tokens_with_messages(self):
106
+ """Test token estimation with messages."""
107
+ conv = Conversation()
108
+ # Each character ~= 0.25 tokens, so 400 chars ~= 100 tokens
109
+ conv.add_message("user", "a" * 200)
110
+ conv.add_message("assistant", "b" * 200)
111
+
112
+ tokens = conv.estimate_tokens()
113
+ assert tokens == 100 # 400 chars / 4
114
+
115
+ def test_estimate_tokens_with_summary(self):
116
+ """Test token estimation includes summary."""
117
+ conv = Conversation(summary="Summary text here" * 10) # 170 chars
118
+ conv.add_message("user", "a" * 230) # 230 chars
119
+
120
+ # Total: 400 chars / 4 = 100 tokens
121
+ tokens = conv.estimate_tokens()
122
+ assert tokens == 100
123
+
124
+ def test_should_summarize_below_threshold(self):
125
+ """Test should_summarize returns False below threshold."""
126
+ conv = Conversation()
127
+ conv.add_message("user", "a" * 100)
128
+ conv.add_message("assistant", "b" * 100)
129
+
130
+ # 200 chars = 50 tokens, below 8000 threshold
131
+ assert not conv.should_summarize(threshold_tokens=8000)
132
+
133
+ def test_should_summarize_above_threshold(self):
134
+ """Test should_summarize returns True above threshold."""
135
+ conv = Conversation()
136
+ # Add enough messages to exceed 1000 token threshold
137
+ for i in range(10):
138
+ conv.add_message("user", "a" * 200) # 200 chars = 50 tokens
139
+ conv.add_message("assistant", "b" * 200) # 200 chars = 50 tokens
140
+
141
+ # Total: 2000 chars = 500 tokens, above 400 threshold
142
+ assert conv.should_summarize(threshold_tokens=400)
143
+
144
+ def test_should_summarize_too_few_messages(self):
145
+ """Test should_summarize returns False with too few messages."""
146
+ conv = Conversation()
147
+ # Even with many tokens, need minimum message count
148
+ conv.add_message("user", "a" * 4000) # 1000 tokens
149
+
150
+ assert not conv.should_summarize(threshold_tokens=500)
151
+
152
+ def test_compress_messages_few_messages(self):
153
+ """Test compress_messages with too few messages."""
154
+ conv = Conversation()
155
+ conv.add_message("user", "message 1")
156
+ conv.add_message("assistant", "response 1")
157
+
158
+ old, recent = conv.compress_messages(keep_recent=4)
159
+ assert len(old) == 0
160
+ assert len(recent) == 2
161
+
162
+ def test_compress_messages_split(self):
163
+ """Test compress_messages splits correctly."""
164
+ conv = Conversation()
165
+ for i in range(10):
166
+ conv.add_message("user", f"message {i}")
167
+ conv.add_message("assistant", f"response {i}")
168
+
169
+ # Total: 20 messages, keep 4 recent
170
+ old, recent = conv.compress_messages(keep_recent=4)
171
+ assert len(old) == 16
172
+ assert len(recent) == 4
173
+ assert recent[0].content == "message 8"
174
+ assert recent[-1].content == "response 9"
175
+
176
+ def test_to_dict(self):
177
+ """Test converting conversation to dictionary."""
178
+ conv = Conversation()
179
+ conv.add_message("user", "test")
180
+ data = conv.to_dict()
181
+
182
+ assert "conversation_id" in data
183
+ assert "created_at" in data
184
+ assert "updated_at" in data
185
+ assert "messages" in data
186
+ assert len(data["messages"]) == 1
187
+
188
+ def test_to_dict_with_summary(self):
189
+ """Test to_dict includes summary."""
190
+ conv = Conversation(summary="Test summary")
191
+ data = conv.to_dict()
192
+
193
+ assert data["summary"] == "Test summary"
194
+
195
+ def test_from_dict(self):
196
+ """Test creating conversation from dictionary."""
197
+ data = {
198
+ "conversation_id": "test_123",
199
+ "created_at": "2024-01-09T12:00:00",
200
+ "updated_at": "2024-01-09T12:05:00",
201
+ "summary": "Test summary",
202
+ "messages": [
203
+ {
204
+ "role": "user",
205
+ "content": "Hello",
206
+ "timestamp": "2024-01-09T12:01:00"
207
+ }
208
+ ]
209
+ }
210
+
211
+ conv = Conversation.from_dict(data)
212
+ assert conv.conversation_id == "test_123"
213
+ assert conv.summary == "Test summary"
214
+ assert len(conv.messages) == 1
215
+ assert conv.messages[0].content == "Hello"
216
+
217
+
218
+ class TestConversationHistory:
219
+ """Tests for ConversationHistory class."""
220
+
221
+ @pytest.fixture
222
+ def temp_history_dir(self, tmp_path):
223
+ """Create a temporary history directory."""
224
+ return tmp_path / "history"
225
+
226
+ @pytest.fixture
227
+ def conv_history(self, temp_history_dir):
228
+ """Create a ConversationHistory instance."""
229
+ return ConversationHistory(temp_history_dir)
230
+
231
+ def test_init_creates_directory(self, temp_history_dir):
232
+ """Test initialization creates directory."""
233
+ ConversationHistory(temp_history_dir)
234
+ assert temp_history_dir.exists()
235
+
236
+ def test_save_conversation(self, conv_history, temp_history_dir):
237
+ """Test saving a conversation."""
238
+ conv = Conversation()
239
+ conv.add_message("user", "test")
240
+
241
+ conv_history.save_conversation(conv)
242
+
243
+ # Check file exists
244
+ file_path = temp_history_dir / f"{conv.conversation_id}.json"
245
+ assert file_path.exists()
246
+
247
+ # Check content
248
+ with open(file_path) as f:
249
+ data = json.load(f)
250
+ assert data["conversation_id"] == conv.conversation_id
251
+ assert len(data["messages"]) == 1
252
+
253
+ def test_load_conversation(self, conv_history):
254
+ """Test loading a conversation."""
255
+ # Save first
256
+ conv = Conversation()
257
+ conv.add_message("user", "test message")
258
+ conv_history.save_conversation(conv)
259
+
260
+ # Load
261
+ loaded = conv_history.load_conversation(conv.conversation_id)
262
+ assert loaded is not None
263
+ assert loaded.conversation_id == conv.conversation_id
264
+ assert len(loaded.messages) == 1
265
+ assert loaded.messages[0].content == "test message"
266
+
267
+ def test_load_nonexistent_conversation(self, conv_history):
268
+ """Test loading a conversation that doesn't exist."""
269
+ loaded = conv_history.load_conversation("nonexistent_id")
270
+ assert loaded is None
271
+
272
+ def test_list_conversations(self, conv_history):
273
+ """Test listing conversations."""
274
+ import time
275
+ # Create multiple conversations with small delays to ensure ordering
276
+ for i in range(3):
277
+ conv = Conversation()
278
+ conv.add_message("user", f"message {i}")
279
+ conv_history.save_conversation(conv)
280
+ time.sleep(0.01) # Small delay to ensure different timestamps
281
+
282
+ conversations = conv_history.list_conversations()
283
+ assert len(conversations) == 3
284
+
285
+ def test_list_conversations_with_limit(self, conv_history):
286
+ """Test listing conversations with limit."""
287
+ import time
288
+ for i in range(5):
289
+ conv = Conversation()
290
+ conv.add_message("user", f"message {i}")
291
+ conv_history.save_conversation(conv)
292
+ time.sleep(0.01)
293
+
294
+ conversations = conv_history.list_conversations(limit=2)
295
+ assert len(conversations) == 2
296
+
297
+ def test_list_conversations_with_search(self, conv_history):
298
+ """Test searching conversations."""
299
+ import time
300
+ conv1 = Conversation()
301
+ conv1.add_message("user", "Python programming")
302
+ conv_history.save_conversation(conv1)
303
+ time.sleep(0.01)
304
+
305
+ conv2 = Conversation()
306
+ conv2.add_message("user", "JavaScript coding")
307
+ conv_history.save_conversation(conv2)
308
+
309
+ # Search for Python
310
+ results = conv_history.list_conversations(search_query="Python")
311
+ assert len(results) == 1
312
+ assert "Python" in results[0].messages[0].content
313
+
314
+ def test_delete_conversation(self, conv_history, temp_history_dir):
315
+ """Test deleting a conversation."""
316
+ conv = Conversation()
317
+ conv_history.save_conversation(conv)
318
+
319
+ # Verify it exists
320
+ file_path = temp_history_dir / f"{conv.conversation_id}.json"
321
+ assert file_path.exists()
322
+
323
+ # Delete
324
+ result = conv_history.delete_conversation(conv.conversation_id)
325
+ assert result is True
326
+ assert not file_path.exists()
327
+
328
+ def test_delete_nonexistent_conversation(self, conv_history):
329
+ """Test deleting a conversation that doesn't exist."""
330
+ result = conv_history.delete_conversation("nonexistent")
331
+ assert result is False
332
+
333
+ def test_get_latest_conversation(self, conv_history):
334
+ """Test getting the latest conversation."""
335
+ # Create conversations with delay to ensure ordering
336
+ conv1 = Conversation()
337
+ conv1.add_message("user", "first")
338
+ conv_history.save_conversation(conv1)
339
+
340
+ import time
341
+ time.sleep(0.01) # Small delay
342
+
343
+ conv2 = Conversation()
344
+ conv2.add_message("user", "second")
345
+ conv_history.save_conversation(conv2)
346
+
347
+ latest = conv_history.get_latest_conversation()
348
+ assert latest is not None
349
+ assert latest.messages[0].content == "second"
350
+
351
+ def test_export_conversation_markdown(self, conv_history):
352
+ """Test exporting conversation as markdown."""
353
+ conv = Conversation()
354
+ conv.add_message("user", "Hello")
355
+ conv.add_message("assistant", "Hi there!")
356
+ conv_history.save_conversation(conv)
357
+
358
+ markdown = conv_history.export_conversation(conv.conversation_id, "markdown")
359
+ assert markdown is not None
360
+ assert "Hello" in markdown
361
+ assert "Hi there!" in markdown
362
+ assert "**You:**" in markdown
363
+ assert "**Claude:**" in markdown
364
+
365
+ def test_export_conversation_json(self, conv_history):
366
+ """Test exporting conversation as JSON."""
367
+ conv = Conversation()
368
+ conv.add_message("user", "Test")
369
+ conv_history.save_conversation(conv)
370
+
371
+ json_str = conv_history.export_conversation(conv.conversation_id, "json")
372
+ assert json_str is not None
373
+
374
+ data = json.loads(json_str)
375
+ assert data["conversation_id"] == conv.conversation_id
376
+
377
+ def test_export_nonexistent_conversation(self, conv_history):
378
+ """Test exporting a conversation that doesn't exist."""
379
+ result = conv_history.export_conversation("nonexistent", "markdown")
380
+ assert result is None
381
+
382
+ @patch('claude_dev_cli.core.ClaudeClient')
383
+ def test_summarize_conversation(self, mock_client_class, conv_history):
384
+ """Test summarizing a conversation."""
385
+ # Setup mock
386
+ mock_client = Mock()
387
+ mock_client.call.return_value = "This is a test summary of the conversation."
388
+ mock_client_class.return_value = mock_client
389
+
390
+ # Create conversation with enough messages
391
+ conv = Conversation()
392
+ for i in range(6):
393
+ conv.add_message("user", f"Question {i}")
394
+ conv.add_message("assistant", f"Answer {i}")
395
+ conv_history.save_conversation(conv)
396
+
397
+ # Summarize
398
+ summary = conv_history.summarize_conversation(conv.conversation_id, keep_recent=4)
399
+
400
+ assert summary is not None
401
+ assert "test summary" in summary
402
+
403
+ # Verify conversation was updated
404
+ updated_conv = conv_history.load_conversation(conv.conversation_id)
405
+ assert updated_conv.summary == summary
406
+ assert len(updated_conv.messages) == 4 # Only recent messages kept
407
+
408
+ @patch('claude_dev_cli.core.ClaudeClient')
409
+ def test_summarize_with_previous_summary(self, mock_client_class, conv_history):
410
+ """Test summarizing with existing summary."""
411
+ mock_client = Mock()
412
+ mock_client.call.return_value = "Updated summary"
413
+ mock_client_class.return_value = mock_client
414
+
415
+ # Create conversation with existing summary
416
+ conv = Conversation(summary="Previous summary")
417
+ for i in range(6):
418
+ conv.add_message("user", f"Message {i}")
419
+ conv.add_message("assistant", f"Response {i}")
420
+ conv_history.save_conversation(conv)
421
+
422
+ # Summarize
423
+ summary = conv_history.summarize_conversation(conv.conversation_id, keep_recent=2)
424
+
425
+ # Check that call included previous summary
426
+ call_args = mock_client.call.call_args[0][0]
427
+ assert "Previous summary" in call_args
428
+
429
+ def test_summarize_too_few_messages(self, conv_history):
430
+ """Test summarizing with too few messages."""
431
+ conv = Conversation()
432
+ conv.add_message("user", "Only one message")
433
+ conv_history.save_conversation(conv)
434
+
435
+ result = conv_history.summarize_conversation(conv.conversation_id, keep_recent=4)
436
+ assert "too few messages" in result.lower()
437
+
438
+ def test_summarize_nonexistent_conversation(self, conv_history):
439
+ """Test summarizing a conversation that doesn't exist."""
440
+ result = conv_history.summarize_conversation("nonexistent")
441
+ assert result is None
442
+
443
+
444
+ class TestConversationPersistence:
445
+ """Tests for conversation persistence and data integrity."""
446
+
447
+ def test_roundtrip_conversation(self, tmp_path):
448
+ """Test saving and loading preserves all data."""
449
+ history = ConversationHistory(tmp_path)
450
+
451
+ # Create conversation with all features
452
+ conv = Conversation(summary="Initial summary")
453
+ conv.add_message("user", "First question")
454
+ conv.add_message("assistant", "First answer")
455
+ conv.add_message("user", "Second question")
456
+
457
+ # Save
458
+ history.save_conversation(conv)
459
+
460
+ # Load
461
+ loaded = history.load_conversation(conv.conversation_id)
462
+
463
+ # Verify everything matches
464
+ assert loaded.conversation_id == conv.conversation_id
465
+ assert loaded.summary == conv.summary
466
+ assert len(loaded.messages) == len(conv.messages)
467
+ assert loaded.messages[0].content == conv.messages[0].content
468
+ assert loaded.messages[1].role == conv.messages[1].role
469
+
470
+ def test_conversation_update(self, tmp_path):
471
+ """Test updating a conversation."""
472
+ history = ConversationHistory(tmp_path)
473
+
474
+ # Create and save
475
+ conv = Conversation()
476
+ conv.add_message("user", "Original message")
477
+ history.save_conversation(conv)
478
+
479
+ # Load, modify, save
480
+ loaded = history.load_conversation(conv.conversation_id)
481
+ loaded.add_message("assistant", "New response")
482
+ loaded.summary = "Added summary"
483
+ history.save_conversation(loaded)
484
+
485
+ # Load again and verify
486
+ final = history.load_conversation(conv.conversation_id)
487
+ assert len(final.messages) == 2
488
+ assert final.summary == "Added summary"
File without changes
File without changes