claude-dev-cli 0.8.1__py3-none-any.whl → 0.8.3__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.8.1"
12
+ __version__ = "0.8.3"
13
13
  __author__ = "Julio"
14
14
  __license__ = "MIT"
15
15
 
claude_dev_cli/cli.py CHANGED
@@ -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."""
@@ -884,6 +1015,107 @@ def git_commit(ctx: click.Context, api: Optional[str], auto_context: bool) -> No
884
1015
  sys.exit(1)
885
1016
 
886
1017
 
1018
+ @main.group()
1019
+ def context() -> None:
1020
+ """Context gathering tools and information."""
1021
+ pass
1022
+
1023
+
1024
+ @context.command('summary')
1025
+ @click.argument('file_path', type=click.Path(exists=True))
1026
+ @click.option('--include-git/--no-git', default=True, help='Include git context')
1027
+ @click.option('--include-deps/--no-deps', default=True, help='Include dependencies')
1028
+ @click.option('--include-tests/--no-tests', default=True, help='Include test files')
1029
+ @click.pass_context
1030
+ def context_summary(
1031
+ ctx: click.Context,
1032
+ file_path: str,
1033
+ include_git: bool,
1034
+ include_deps: bool,
1035
+ include_tests: bool
1036
+ ) -> None:
1037
+ """Show what context would be gathered for a file."""
1038
+ from claude_dev_cli.context import ContextGatherer
1039
+ from rich.table import Table
1040
+
1041
+ console = ctx.obj['console']
1042
+
1043
+ try:
1044
+ file_path_obj = Path(file_path)
1045
+ gatherer = ContextGatherer()
1046
+
1047
+ # Gather context
1048
+ with console.status("[bold blue]Analyzing context..."):
1049
+ context = gatherer.gather_for_review(
1050
+ file_path_obj,
1051
+ include_git=include_git,
1052
+ include_tests=include_tests
1053
+ )
1054
+
1055
+ # Display summary
1056
+ console.print(f"\n[bold cyan]Context Summary for:[/bold cyan] {file_path}\n")
1057
+
1058
+ table = Table(show_header=True, header_style="bold magenta")
1059
+ table.add_column("Type", style="cyan")
1060
+ table.add_column("Content", style="white")
1061
+ table.add_column("Size", justify="right", style="yellow")
1062
+ table.add_column("Lines", justify="right", style="green")
1063
+ table.add_column("Truncated", justify="center", style="red")
1064
+
1065
+ total_chars = 0
1066
+ total_lines = 0
1067
+
1068
+ for item in context.items:
1069
+ lines = len(item.content.split('\n'))
1070
+ chars = len(item.content)
1071
+ truncated = "✓" if item.metadata.get('truncated') else ""
1072
+
1073
+ # Format content preview
1074
+ if item.type == 'file':
1075
+ content = item.metadata.get('path', 'unknown')
1076
+ if item.metadata.get('is_test'):
1077
+ content += " [TEST]"
1078
+ elif item.type == 'git':
1079
+ branch = item.metadata.get('branch', 'unknown')
1080
+ modified = item.metadata.get('modified_count', 0)
1081
+ content = f"Branch: {branch}, {modified} modified files"
1082
+ elif item.type == 'dependency':
1083
+ dep_files = item.metadata.get('dependency_files', [])
1084
+ content = f"{len(dep_files)} dependency files"
1085
+ else:
1086
+ content = item.type
1087
+
1088
+ table.add_row(
1089
+ item.type.title(),
1090
+ content[:60] + "..." if len(content) > 60 else content,
1091
+ f"{chars:,}",
1092
+ f"{lines:,}",
1093
+ truncated
1094
+ )
1095
+
1096
+ total_chars += chars
1097
+ total_lines += lines
1098
+
1099
+ console.print(table)
1100
+
1101
+ # Show totals
1102
+ console.print(f"\n[bold]Total:[/bold]")
1103
+ console.print(f" Characters: [yellow]{total_chars:,}[/yellow]")
1104
+ console.print(f" Lines: [green]{total_lines:,}[/green]")
1105
+ console.print(f" Estimated tokens: [cyan]~{total_chars // 4:,}[/cyan] (rough estimate)")
1106
+
1107
+ # Show any truncation warnings
1108
+ truncated_items = [item for item in context.items if item.metadata.get('truncated')]
1109
+ if truncated_items:
1110
+ console.print(f"\n[yellow]⚠ {len(truncated_items)} item(s) truncated to fit size limits[/yellow]")
1111
+
1112
+ console.print(f"\n[dim]Use --auto-context with commands to include this context[/dim]")
1113
+
1114
+ except Exception as e:
1115
+ console.print(f"[red]Error: {e}[/red]")
1116
+ sys.exit(1)
1117
+
1118
+
887
1119
  @main.command('usage')
888
1120
  @click.option('--days', type=int, help='Filter by days')
889
1121
  @click.option('--api', help='Filter by API config')
claude_dev_cli/config.py CHANGED
@@ -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()
claude_dev_cli/context.py CHANGED
@@ -339,10 +339,25 @@ class DependencyAnalyzer:
339
339
 
340
340
 
341
341
  class ErrorContext:
342
- """Parse and format error context."""
342
+ """Parse and format error context for multiple languages."""
343
343
 
344
344
  @staticmethod
345
- def parse_traceback(error_text: str) -> Dict[str, Any]:
345
+ def detect_language(error_text: str) -> str:
346
+ """Detect programming language from error format."""
347
+ if 'Traceback (most recent call last):' in error_text or 'File "' in error_text:
348
+ return 'python'
349
+ elif 'at ' in error_text and ('.js:' in error_text or '.ts:' in error_text):
350
+ return 'javascript'
351
+ elif 'panic:' in error_text and '.go:' in error_text:
352
+ return 'go'
353
+ elif 'thread' in error_text and 'panicked at' in error_text and '.rs:' in error_text:
354
+ return 'rust'
355
+ elif 'at ' in error_text and '.java:' in error_text:
356
+ return 'java'
357
+ return 'unknown'
358
+
359
+ @staticmethod
360
+ def parse_python_traceback(error_text: str) -> Dict[str, Any]:
346
361
  """Parse Python traceback into structured data."""
347
362
  lines = error_text.split('\n')
348
363
 
@@ -391,24 +406,183 @@ class ErrorContext:
391
406
  'raw': error_text
392
407
  }
393
408
 
394
- return {'frames': frames, 'raw': error_text}
409
+ return {'frames': frames, 'raw': error_text, 'language': 'python'}
410
+
411
+ @staticmethod
412
+ def parse_javascript_stack(error_text: str) -> Dict[str, Any]:
413
+ """Parse JavaScript/TypeScript stack trace."""
414
+ lines = error_text.split('\n')
415
+ frames = []
416
+ error_type = None
417
+ error_message = None
418
+
419
+ for line in lines:
420
+ # Error message usually first: "Error: message" or "TypeError: message"
421
+ if not error_type and (':' in line and not line.strip().startswith('at')):
422
+ parts = line.split(':', 1)
423
+ error_type = parts[0].strip()
424
+ error_message = parts[1].strip() if len(parts) > 1 else ''
425
+ # Stack frame: "at functionName (file.js:line:col)" or "at file.js:line:col"
426
+ elif line.strip().startswith('at '):
427
+ match = re.search(r'at\s+(?:(.+?)\s+)?\(([^)]+)\)|(\S+)$', line)
428
+ if match:
429
+ function = match.group(1) or 'anonymous'
430
+ location = match.group(2) or match.group(3)
431
+ if location and ':' in location:
432
+ parts = location.rsplit(':', 2)
433
+ frames.append({
434
+ 'file': parts[0],
435
+ 'line': int(parts[1]) if len(parts) > 1 and parts[1].isdigit() else None,
436
+ 'column': int(parts[2]) if len(parts) > 2 and parts[2].isdigit() else None,
437
+ 'function': function
438
+ })
439
+
440
+ return {
441
+ 'frames': frames,
442
+ 'error_type': error_type,
443
+ 'error_message': error_message,
444
+ 'raw': error_text,
445
+ 'language': 'javascript'
446
+ }
447
+
448
+ @staticmethod
449
+ def parse_go_panic(error_text: str) -> Dict[str, Any]:
450
+ """Parse Go panic trace."""
451
+ lines = error_text.split('\n')
452
+ frames = []
453
+ error_message = None
454
+
455
+ for line in lines:
456
+ # Panic message: "panic: message"
457
+ if line.startswith('panic:'):
458
+ error_message = line.replace('panic:', '').strip()
459
+ # Stack frame: "function(args)" followed by "\tfile.go:line +0xhex"
460
+ elif '\t' in line and '.go:' in line:
461
+ match = re.search(r'([^/\s]+\.go):(\d+)', line)
462
+ if match:
463
+ frames.append({
464
+ 'file': match.group(1),
465
+ 'line': int(match.group(2)),
466
+ 'function': 'goroutine'
467
+ })
468
+
469
+ return {
470
+ 'frames': frames,
471
+ 'error_type': 'panic',
472
+ 'error_message': error_message,
473
+ 'raw': error_text,
474
+ 'language': 'go'
475
+ }
476
+
477
+ @staticmethod
478
+ def parse_rust_panic(error_text: str) -> Dict[str, Any]:
479
+ """Parse Rust panic message."""
480
+ lines = error_text.split('\n')
481
+ frames = []
482
+ error_message = None
483
+
484
+ for line in lines:
485
+ # Panic message: "thread 'main' panicked at 'message', file.rs:line:col"
486
+ if 'panicked at' in line:
487
+ match = re.search(r"panicked at '([^']+)', ([^:]+):(\d+):(\d+)", line)
488
+ if match:
489
+ error_message = match.group(1)
490
+ frames.append({
491
+ 'file': match.group(2),
492
+ 'line': int(match.group(3)),
493
+ 'column': int(match.group(4)),
494
+ 'function': 'panic'
495
+ })
496
+ # Stack backtrace frames
497
+ elif '.rs:' in line:
498
+ match = re.search(r'([^/\s]+\.rs):(\d+):(\d+)', line)
499
+ if match:
500
+ frames.append({
501
+ 'file': match.group(1),
502
+ 'line': int(match.group(2)),
503
+ 'column': int(match.group(3)),
504
+ 'function': 'unknown'
505
+ })
506
+
507
+ return {
508
+ 'frames': frames,
509
+ 'error_type': 'panic',
510
+ 'error_message': error_message,
511
+ 'raw': error_text,
512
+ 'language': 'rust'
513
+ }
514
+
515
+ @staticmethod
516
+ def parse_java_stack(error_text: str) -> Dict[str, Any]:
517
+ """Parse Java stack trace."""
518
+ lines = error_text.split('\n')
519
+ frames = []
520
+ error_type = None
521
+ error_message = None
522
+
523
+ for line in lines:
524
+ # Exception message: "java.lang.NullPointerException: message"
525
+ if not error_type and 'Exception' in line or 'Error' in line:
526
+ parts = line.split(':', 1)
527
+ error_type = parts[0].strip().split('.')[-1] # Get last part of package
528
+ error_message = parts[1].strip() if len(parts) > 1 else ''
529
+ # Stack frame: "at package.Class.method(File.java:line)"
530
+ elif line.strip().startswith('at '):
531
+ match = re.search(r'at\s+([^(]+)\(([^:]+\.java):(\d+)\)', line)
532
+ if match:
533
+ frames.append({
534
+ 'function': match.group(1),
535
+ 'file': match.group(2),
536
+ 'line': int(match.group(3))
537
+ })
538
+
539
+ return {
540
+ 'frames': frames,
541
+ 'error_type': error_type,
542
+ 'error_message': error_message,
543
+ 'raw': error_text,
544
+ 'language': 'java'
545
+ }
546
+
547
+ @staticmethod
548
+ def parse_traceback(error_text: str) -> Dict[str, Any]:
549
+ """Parse error/traceback with language auto-detection."""
550
+ language = ErrorContext.detect_language(error_text)
551
+
552
+ if language == 'python':
553
+ return ErrorContext.parse_python_traceback(error_text)
554
+ elif language == 'javascript':
555
+ return ErrorContext.parse_javascript_stack(error_text)
556
+ elif language == 'go':
557
+ return ErrorContext.parse_go_panic(error_text)
558
+ elif language == 'rust':
559
+ return ErrorContext.parse_rust_panic(error_text)
560
+ elif language == 'java':
561
+ return ErrorContext.parse_java_stack(error_text)
562
+ else:
563
+ return {'raw': error_text, 'language': 'unknown'}
395
564
 
396
565
  @staticmethod
397
566
  def format_for_ai(error_text: str) -> str:
398
- """Format error for AI consumption."""
567
+ """Format error for AI consumption with language detection."""
399
568
  parsed = ErrorContext.parse_traceback(error_text)
400
569
 
401
570
  if 'error_type' not in parsed:
402
571
  return error_text
403
572
 
573
+ language = parsed.get('language', 'unknown')
404
574
  parts = [
575
+ f"Language: {language.title()}",
405
576
  f"Error Type: {parsed['error_type']}",
406
577
  f"Error Message: {parsed.get('error_message', 'N/A')}",
407
578
  "\nStack Trace:"
408
579
  ]
409
580
 
410
581
  for i, frame in enumerate(parsed.get('frames', []), 1):
411
- parts.append(f" {i}. {frame.get('file', 'unknown')}:{frame.get('line', '?')} in {frame.get('function', 'unknown')}")
582
+ file_loc = f"{frame.get('file', 'unknown')}:{frame.get('line', '?')}"
583
+ if 'column' in frame:
584
+ file_loc += f":{frame.get('column', '?')}"
585
+ parts.append(f" {i}. {file_loc} in {frame.get('function', 'unknown')}")
412
586
  if 'code' in frame:
413
587
  parts.append(f" > {frame['code']}")
414
588
 
claude_dev_cli/history.py CHANGED
@@ -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.1
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
@@ -85,7 +92,8 @@ A powerful command-line tool for developers using Claude AI with multi-API routi
85
92
  - `git commit`, `generate tests`, `generate docs` (v0.8.1)
86
93
  - **Git Integration**: Automatically include branch, commits, modified files
87
94
  - **Dependency Analysis**: Parse imports and include related files
88
- - **Error Parsing**: Structured Python traceback parsing
95
+ - **Multi-Language Error Parsing** (v0.8.2): Python, JavaScript/TypeScript, Go, Rust, Java
96
+ - **Context Summary** (v0.8.2): Preview context before API calls with `cdc context summary`
89
97
  - **Smart Truncation**: Prevent token limits with configurable file size limits
90
98
  - **Project Memory**: Remember preferences per project
91
99
  - **Global Config**: Set context defaults in `~/.claude-dev-cli/config.json`
@@ -98,19 +106,39 @@ A powerful command-line tool for developers using Claude AI with multi-API routi
98
106
 
99
107
  ## Installation
100
108
 
101
- ### Basic Installation
109
+ ### Via Homebrew (macOS/Linux)
102
110
 
103
111
  ```bash
104
- 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
105
120
  ```
106
121
 
107
- ### With TOON Support (Recommended for Cost Savings)
122
+ ### Via pip
108
123
 
109
124
  ```bash
110
- # 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)
111
129
  pip install claude-dev-cli[toon]
112
130
  ```
113
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
+
114
142
  ## Quick Start
115
143
 
116
144
  ### 1. Set Up API Keys
@@ -199,7 +227,7 @@ git add .
199
227
  cdc git commit --auto-context
200
228
  ```
201
229
 
202
- ### 4. Context-Aware Operations (NEW in v0.8.0)
230
+ ### 4. Context-Aware Operations (v0.8.0+)
203
231
 
204
232
  ```bash
205
233
  # Auto-context includes: git info, dependencies, related files
@@ -208,9 +236,16 @@ cdc git commit --auto-context
208
236
  cdc review mymodule.py --auto-context
209
237
  # ✓ Context gathered (git, dependencies, tests)
210
238
 
211
- # Debug with parsed error details
239
+ # Debug with parsed error details (multi-language support)
212
240
  python broken.py 2>&1 | cdc debug -f broken.py --auto-context
241
+ node app.js 2>&1 | cdc debug --auto-context # JavaScript/TypeScript
242
+ go run main.go 2>&1 | cdc debug --auto-context # Go
213
243
  # ✓ Context gathered (error details, git context)
244
+ # Supports: Python, JavaScript, TypeScript, Go, Rust, Java
245
+
246
+ # Preview context before making API calls - NEW in v0.8.2
247
+ cdc context summary mymodule.py
248
+ # Shows: files, sizes, lines, estimated tokens, truncation warnings
214
249
 
215
250
  # Ask questions with file context
216
251
  cdc ask -f mycode.py --auto-context "how can I improve this?"
@@ -258,7 +293,33 @@ cdc template list --user
258
293
  - **explain-code**: Detailed code explanation
259
294
  - **api-design**: API design assistance
260
295
 
261
- ### 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
262
323
 
263
324
  ```bash
264
325
  # View all usage
@@ -1,10 +1,10 @@
1
- claude_dev_cli/__init__.py,sha256=0g1KP2ohJ45KCYW9EkcRcTA885E4Gqm1kDQbmIOdC-k,469
2
- claude_dev_cli/cli.py,sha256=FjVq9QanwxM8WqSZPPM4-b50v9Ycy9ZDHEg9gBo-kCk,51304
1
+ claude_dev_cli/__init__.py,sha256=4-FONrbZl8GAHKPOWIDbvA7AmSZsTv8CRep1rwl1w5Y,469
2
+ claude_dev_cli/cli.py,sha256=hy2HjU7oEWgGwNB7apBKFQmzjhYrxhHxTqtJ18iePZE,61164
3
3
  claude_dev_cli/commands.py,sha256=RKGx2rv56PM6eErvA2uoQ20hY8babuI5jav8nCUyUOk,3964
4
- claude_dev_cli/config.py,sha256=OLx0xWDf1RIK6RIxl5OKVS4aOSMZZOKxBDmzfQX-muk,9745
5
- claude_dev_cli/context.py,sha256=Z3QYq4ZHAqpuv_xPZtXcBeWf0LCelzkybj8cBz2nBAo,19523
4
+ claude_dev_cli/config.py,sha256=hisG91dUfDwuSdBhmYovU-rrGv__nFq-6fp7S288cSw,10471
5
+ claude_dev_cli/context.py,sha256=1TlLzpREFZDEIuU7RAtlkjxARKWZpnxHHvK283sUAZE,26714
6
6
  claude_dev_cli/core.py,sha256=yaLjEixDvPzvUy4fJ2UB7nMpPPLyKACjR-RuM-1OQBY,4780
7
- claude_dev_cli/history.py,sha256=iQlqgTnXCsyCq5q-XaDl7V5MyPKQ3bx7o_k76-xWSAA,6863
7
+ claude_dev_cli/history.py,sha256=v952xORhxZD0ayrrMaIbTLfe6kOW5fYryoeEmQLlQPg,10163
8
8
  claude_dev_cli/secure_storage.py,sha256=TK3WOaU7a0yTOtzdP_t_28fDRp2lovANNAC6MBdm4nQ,7096
9
9
  claude_dev_cli/template_manager.py,sha256=ZFXOtRIoB6hpf8kLSF9TWJfvUPJt9b-PyEv3qTBK7Zs,8600
10
10
  claude_dev_cli/templates.py,sha256=lKxH943ySfUKgyHaWa4W3LVv91SgznKgajRtSRp_4UY,2260
@@ -17,9 +17,9 @@ claude_dev_cli/plugins/base.py,sha256=H4HQet1I-a3WLCfE9F06Lp8NuFvVoIlou7sIgyJFK-
17
17
  claude_dev_cli/plugins/diff_editor/__init__.py,sha256=gqR5S2TyIVuq-sK107fegsutQ7Z-sgAIEbtc71FhXIM,101
18
18
  claude_dev_cli/plugins/diff_editor/plugin.py,sha256=M1bUoqpasD3ZNQo36Fu_8g92uySPZyG_ujMbj5UplsU,3073
19
19
  claude_dev_cli/plugins/diff_editor/viewer.py,sha256=1IOXIKw_01ppJx5C1dQt9Kr6U1TdAHT8_iUT5r_q0NM,17169
20
- claude_dev_cli-0.8.1.dist-info/licenses/LICENSE,sha256=DGueuJwMJtMwgLO5mWlS0TaeBrFwQuNpNZ22PU9J2bw,1062
21
- claude_dev_cli-0.8.1.dist-info/METADATA,sha256=3QFSQUey2f4wMFgtrImtuPgQoD_-Bk2me891eCrCyhY,15223
22
- claude_dev_cli-0.8.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
- claude_dev_cli-0.8.1.dist-info/entry_points.txt,sha256=zymgUIIVpFTARkFmxAuW2A4BQsNITh_L0uU-XunytHg,85
24
- claude_dev_cli-0.8.1.dist-info/top_level.txt,sha256=m7MF6LOIuTe41IT5Fgt0lc-DK1EgM4gUU_IZwWxK0pg,15
25
- claude_dev_cli-0.8.1.dist-info/RECORD,,
20
+ claude_dev_cli-0.8.3.dist-info/licenses/LICENSE,sha256=DGueuJwMJtMwgLO5mWlS0TaeBrFwQuNpNZ22PU9J2bw,1062
21
+ claude_dev_cli-0.8.3.dist-info/METADATA,sha256=zgkMO_8g4hwJ7ccfDqGqXM-d3OfghrhxsWt7JK0evgs,17356
22
+ claude_dev_cli-0.8.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
+ claude_dev_cli-0.8.3.dist-info/entry_points.txt,sha256=zymgUIIVpFTARkFmxAuW2A4BQsNITh_L0uU-XunytHg,85
24
+ claude_dev_cli-0.8.3.dist-info/top_level.txt,sha256=m7MF6LOIuTe41IT5Fgt0lc-DK1EgM4gUU_IZwWxK0pg,15
25
+ claude_dev_cli-0.8.3.dist-info/RECORD,,