claude-dev-cli 0.8.0__py3-none-any.whl → 0.8.2__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.
- claude_dev_cli/__init__.py +1 -1
- claude_dev_cli/cli.py +161 -9
- claude_dev_cli/config.py +25 -0
- claude_dev_cli/context.py +262 -23
- {claude_dev_cli-0.8.0.dist-info → claude_dev_cli-0.8.2.dist-info}/METADATA +28 -6
- {claude_dev_cli-0.8.0.dist-info → claude_dev_cli-0.8.2.dist-info}/RECORD +10 -10
- {claude_dev_cli-0.8.0.dist-info → claude_dev_cli-0.8.2.dist-info}/WHEEL +0 -0
- {claude_dev_cli-0.8.0.dist-info → claude_dev_cli-0.8.2.dist-info}/entry_points.txt +0 -0
- {claude_dev_cli-0.8.0.dist-info → claude_dev_cli-0.8.2.dist-info}/licenses/LICENSE +0 -0
- {claude_dev_cli-0.8.0.dist-info → claude_dev_cli-0.8.2.dist-info}/top_level.txt +0 -0
claude_dev_cli/__init__.py
CHANGED
claude_dev_cli/cli.py
CHANGED
|
@@ -461,20 +461,37 @@ def generate() -> None:
|
|
|
461
461
|
@click.option('-o', '--output', type=click.Path(), help='Output file path')
|
|
462
462
|
@click.option('-a', '--api', help='API config to use')
|
|
463
463
|
@click.option('-i', '--interactive', is_flag=True, help='Interactive refinement mode')
|
|
464
|
+
@click.option('--auto-context', is_flag=True, help='Include dependencies and related files')
|
|
464
465
|
@click.pass_context
|
|
465
466
|
def gen_tests(
|
|
466
467
|
ctx: click.Context,
|
|
467
468
|
file_path: str,
|
|
468
469
|
output: Optional[str],
|
|
469
470
|
api: Optional[str],
|
|
470
|
-
interactive: bool
|
|
471
|
+
interactive: bool,
|
|
472
|
+
auto_context: bool
|
|
471
473
|
) -> None:
|
|
472
474
|
"""Generate pytest tests for a Python file."""
|
|
473
475
|
console = ctx.obj['console']
|
|
474
476
|
|
|
475
477
|
try:
|
|
476
|
-
|
|
477
|
-
|
|
478
|
+
if auto_context:
|
|
479
|
+
from claude_dev_cli.context import ContextGatherer
|
|
480
|
+
|
|
481
|
+
with console.status("[bold blue]Gathering context..."):
|
|
482
|
+
gatherer = ContextGatherer()
|
|
483
|
+
context = gatherer.gather_for_file(Path(file_path), include_git=False)
|
|
484
|
+
context_info = context.format_for_prompt()
|
|
485
|
+
|
|
486
|
+
console.print("[dim]✓ Context gathered (dependencies, related files)[/dim]")
|
|
487
|
+
|
|
488
|
+
# Use context-aware test generation
|
|
489
|
+
client = ClaudeClient(api_config_name=api)
|
|
490
|
+
enhanced_prompt = f"{context_info}\n\nPlease generate comprehensive pytest tests for the main file, including fixtures, edge cases, and proper mocking where needed."
|
|
491
|
+
result = client.call(enhanced_prompt)
|
|
492
|
+
else:
|
|
493
|
+
with console.status("[bold blue]Generating tests..."):
|
|
494
|
+
result = generate_tests(file_path, api_config_name=api)
|
|
478
495
|
|
|
479
496
|
if interactive:
|
|
480
497
|
# Show initial result
|
|
@@ -530,20 +547,37 @@ def gen_tests(
|
|
|
530
547
|
@click.option('-o', '--output', type=click.Path(), help='Output file path')
|
|
531
548
|
@click.option('-a', '--api', help='API config to use')
|
|
532
549
|
@click.option('-i', '--interactive', is_flag=True, help='Interactive refinement mode')
|
|
550
|
+
@click.option('--auto-context', is_flag=True, help='Include dependencies and related files')
|
|
533
551
|
@click.pass_context
|
|
534
552
|
def gen_docs(
|
|
535
553
|
ctx: click.Context,
|
|
536
554
|
file_path: str,
|
|
537
555
|
output: Optional[str],
|
|
538
556
|
api: Optional[str],
|
|
539
|
-
interactive: bool
|
|
557
|
+
interactive: bool,
|
|
558
|
+
auto_context: bool
|
|
540
559
|
) -> None:
|
|
541
560
|
"""Generate documentation for a Python file."""
|
|
542
561
|
console = ctx.obj['console']
|
|
543
562
|
|
|
544
563
|
try:
|
|
545
|
-
|
|
546
|
-
|
|
564
|
+
if auto_context:
|
|
565
|
+
from claude_dev_cli.context import ContextGatherer
|
|
566
|
+
|
|
567
|
+
with console.status("[bold blue]Gathering context..."):
|
|
568
|
+
gatherer = ContextGatherer()
|
|
569
|
+
context = gatherer.gather_for_file(Path(file_path), include_git=False)
|
|
570
|
+
context_info = context.format_for_prompt()
|
|
571
|
+
|
|
572
|
+
console.print("[dim]✓ Context gathered (dependencies, related files)[/dim]")
|
|
573
|
+
|
|
574
|
+
# Use context-aware documentation generation
|
|
575
|
+
client = ClaudeClient(api_config_name=api)
|
|
576
|
+
enhanced_prompt = f"{context_info}\n\nPlease generate comprehensive documentation for the main file, including API reference, usage examples, and integration notes."
|
|
577
|
+
result = client.call(enhanced_prompt)
|
|
578
|
+
else:
|
|
579
|
+
with console.status("[bold blue]Generating documentation..."):
|
|
580
|
+
result = generate_docs(file_path, api_config_name=api)
|
|
547
581
|
|
|
548
582
|
if interactive:
|
|
549
583
|
console.print("\n[bold]Initial Documentation:[/bold]\n")
|
|
@@ -816,14 +850,31 @@ def git() -> None:
|
|
|
816
850
|
|
|
817
851
|
@git.command('commit')
|
|
818
852
|
@click.option('-a', '--api', help='API config to use')
|
|
853
|
+
@click.option('--auto-context', is_flag=True, help='Include git history and branch context')
|
|
819
854
|
@click.pass_context
|
|
820
|
-
def git_commit(ctx: click.Context, api: Optional[str]) -> None:
|
|
855
|
+
def git_commit(ctx: click.Context, api: Optional[str], auto_context: bool) -> None:
|
|
821
856
|
"""Generate commit message from staged changes."""
|
|
822
857
|
console = ctx.obj['console']
|
|
823
858
|
|
|
824
859
|
try:
|
|
825
|
-
|
|
826
|
-
|
|
860
|
+
if auto_context:
|
|
861
|
+
from claude_dev_cli.context import ContextGatherer
|
|
862
|
+
|
|
863
|
+
with console.status("[bold blue]Gathering context..."):
|
|
864
|
+
gatherer = ContextGatherer()
|
|
865
|
+
# Get git context with recent commits and branch info
|
|
866
|
+
git_context = gatherer.git.gather(include_diff=True)
|
|
867
|
+
context_info = git_context.format_for_prompt()
|
|
868
|
+
|
|
869
|
+
console.print("[dim]✓ Context gathered (branch, commits, diff)[/dim]")
|
|
870
|
+
|
|
871
|
+
# Use context-aware commit message generation
|
|
872
|
+
client = ClaudeClient(api_config_name=api)
|
|
873
|
+
enhanced_prompt = f"{context_info}\n\nPlease generate a concise, conventional commit message for the staged changes. Follow best practices: imperative mood, clear scope, explain what and why."
|
|
874
|
+
result = client.call(enhanced_prompt)
|
|
875
|
+
else:
|
|
876
|
+
with console.status("[bold blue]Analyzing changes..."):
|
|
877
|
+
result = git_commit_message(api_config_name=api)
|
|
827
878
|
|
|
828
879
|
console.print("\n[bold green]Suggested commit message:[/bold green]")
|
|
829
880
|
console.print(Panel(result, border_style="green"))
|
|
@@ -833,6 +884,107 @@ def git_commit(ctx: click.Context, api: Optional[str]) -> None:
|
|
|
833
884
|
sys.exit(1)
|
|
834
885
|
|
|
835
886
|
|
|
887
|
+
@main.group()
|
|
888
|
+
def context() -> None:
|
|
889
|
+
"""Context gathering tools and information."""
|
|
890
|
+
pass
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
@context.command('summary')
|
|
894
|
+
@click.argument('file_path', type=click.Path(exists=True))
|
|
895
|
+
@click.option('--include-git/--no-git', default=True, help='Include git context')
|
|
896
|
+
@click.option('--include-deps/--no-deps', default=True, help='Include dependencies')
|
|
897
|
+
@click.option('--include-tests/--no-tests', default=True, help='Include test files')
|
|
898
|
+
@click.pass_context
|
|
899
|
+
def context_summary(
|
|
900
|
+
ctx: click.Context,
|
|
901
|
+
file_path: str,
|
|
902
|
+
include_git: bool,
|
|
903
|
+
include_deps: bool,
|
|
904
|
+
include_tests: bool
|
|
905
|
+
) -> None:
|
|
906
|
+
"""Show what context would be gathered for a file."""
|
|
907
|
+
from claude_dev_cli.context import ContextGatherer
|
|
908
|
+
from rich.table import Table
|
|
909
|
+
|
|
910
|
+
console = ctx.obj['console']
|
|
911
|
+
|
|
912
|
+
try:
|
|
913
|
+
file_path_obj = Path(file_path)
|
|
914
|
+
gatherer = ContextGatherer()
|
|
915
|
+
|
|
916
|
+
# Gather context
|
|
917
|
+
with console.status("[bold blue]Analyzing context..."):
|
|
918
|
+
context = gatherer.gather_for_review(
|
|
919
|
+
file_path_obj,
|
|
920
|
+
include_git=include_git,
|
|
921
|
+
include_tests=include_tests
|
|
922
|
+
)
|
|
923
|
+
|
|
924
|
+
# Display summary
|
|
925
|
+
console.print(f"\n[bold cyan]Context Summary for:[/bold cyan] {file_path}\n")
|
|
926
|
+
|
|
927
|
+
table = Table(show_header=True, header_style="bold magenta")
|
|
928
|
+
table.add_column("Type", style="cyan")
|
|
929
|
+
table.add_column("Content", style="white")
|
|
930
|
+
table.add_column("Size", justify="right", style="yellow")
|
|
931
|
+
table.add_column("Lines", justify="right", style="green")
|
|
932
|
+
table.add_column("Truncated", justify="center", style="red")
|
|
933
|
+
|
|
934
|
+
total_chars = 0
|
|
935
|
+
total_lines = 0
|
|
936
|
+
|
|
937
|
+
for item in context.items:
|
|
938
|
+
lines = len(item.content.split('\n'))
|
|
939
|
+
chars = len(item.content)
|
|
940
|
+
truncated = "✓" if item.metadata.get('truncated') else ""
|
|
941
|
+
|
|
942
|
+
# Format content preview
|
|
943
|
+
if item.type == 'file':
|
|
944
|
+
content = item.metadata.get('path', 'unknown')
|
|
945
|
+
if item.metadata.get('is_test'):
|
|
946
|
+
content += " [TEST]"
|
|
947
|
+
elif item.type == 'git':
|
|
948
|
+
branch = item.metadata.get('branch', 'unknown')
|
|
949
|
+
modified = item.metadata.get('modified_count', 0)
|
|
950
|
+
content = f"Branch: {branch}, {modified} modified files"
|
|
951
|
+
elif item.type == 'dependency':
|
|
952
|
+
dep_files = item.metadata.get('dependency_files', [])
|
|
953
|
+
content = f"{len(dep_files)} dependency files"
|
|
954
|
+
else:
|
|
955
|
+
content = item.type
|
|
956
|
+
|
|
957
|
+
table.add_row(
|
|
958
|
+
item.type.title(),
|
|
959
|
+
content[:60] + "..." if len(content) > 60 else content,
|
|
960
|
+
f"{chars:,}",
|
|
961
|
+
f"{lines:,}",
|
|
962
|
+
truncated
|
|
963
|
+
)
|
|
964
|
+
|
|
965
|
+
total_chars += chars
|
|
966
|
+
total_lines += lines
|
|
967
|
+
|
|
968
|
+
console.print(table)
|
|
969
|
+
|
|
970
|
+
# Show totals
|
|
971
|
+
console.print(f"\n[bold]Total:[/bold]")
|
|
972
|
+
console.print(f" Characters: [yellow]{total_chars:,}[/yellow]")
|
|
973
|
+
console.print(f" Lines: [green]{total_lines:,}[/green]")
|
|
974
|
+
console.print(f" Estimated tokens: [cyan]~{total_chars // 4:,}[/cyan] (rough estimate)")
|
|
975
|
+
|
|
976
|
+
# Show any truncation warnings
|
|
977
|
+
truncated_items = [item for item in context.items if item.metadata.get('truncated')]
|
|
978
|
+
if truncated_items:
|
|
979
|
+
console.print(f"\n[yellow]⚠ {len(truncated_items)} item(s) truncated to fit size limits[/yellow]")
|
|
980
|
+
|
|
981
|
+
console.print(f"\n[dim]Use --auto-context with commands to include this context[/dim]")
|
|
982
|
+
|
|
983
|
+
except Exception as e:
|
|
984
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
985
|
+
sys.exit(1)
|
|
986
|
+
|
|
987
|
+
|
|
836
988
|
@main.command('usage')
|
|
837
989
|
@click.option('--days', type=int, help='Filter by days')
|
|
838
990
|
@click.option('--api', help='Filter by API config')
|
claude_dev_cli/config.py
CHANGED
|
@@ -9,6 +9,18 @@ from pydantic import BaseModel, Field
|
|
|
9
9
|
from claude_dev_cli.secure_storage import SecureStorage
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
class ContextConfig(BaseModel):
|
|
13
|
+
"""Global context gathering configuration."""
|
|
14
|
+
|
|
15
|
+
auto_context_default: bool = False # Default for --auto-context flag
|
|
16
|
+
max_file_lines: int = 1000 # Maximum lines per file in context
|
|
17
|
+
max_related_files: int = 5 # Maximum related files to include
|
|
18
|
+
max_diff_lines: int = 200 # Maximum lines of diff to include
|
|
19
|
+
include_git: bool = True # Include git context by default
|
|
20
|
+
include_dependencies: bool = True # Include dependencies by default
|
|
21
|
+
include_tests: bool = True # Include test files by default
|
|
22
|
+
|
|
23
|
+
|
|
12
24
|
class APIConfig(BaseModel):
|
|
13
25
|
"""Configuration for a Claude API key."""
|
|
14
26
|
|
|
@@ -31,6 +43,13 @@ class ProjectProfile(BaseModel):
|
|
|
31
43
|
coding_style: Optional[str] = None # Preferred coding style
|
|
32
44
|
test_framework: Optional[str] = None # Preferred test framework
|
|
33
45
|
preferences: Dict[str, str] = Field(default_factory=dict) # Custom preferences
|
|
46
|
+
|
|
47
|
+
# Context gathering configuration
|
|
48
|
+
max_context_files: int = 5 # Maximum number of related files to include
|
|
49
|
+
max_diff_lines: int = 200 # Maximum lines of diff to include
|
|
50
|
+
max_file_lines: int = 1000 # Maximum lines per file in context
|
|
51
|
+
include_tests_by_default: bool = True # Include test files in review context
|
|
52
|
+
context_depth: int = 2 # How deep to search for related modules
|
|
34
53
|
|
|
35
54
|
|
|
36
55
|
class Config:
|
|
@@ -67,6 +86,7 @@ class Config:
|
|
|
67
86
|
"project_profiles": [],
|
|
68
87
|
"default_model": "claude-3-5-sonnet-20241022",
|
|
69
88
|
"max_tokens": 4096,
|
|
89
|
+
"context": ContextConfig().model_dump(),
|
|
70
90
|
}
|
|
71
91
|
self._save_config(default_config)
|
|
72
92
|
return default_config
|
|
@@ -238,3 +258,8 @@ class Config:
|
|
|
238
258
|
def get_max_tokens(self) -> int:
|
|
239
259
|
"""Get default max tokens."""
|
|
240
260
|
return self._data.get("max_tokens", 4096)
|
|
261
|
+
|
|
262
|
+
def get_context_config(self) -> ContextConfig:
|
|
263
|
+
"""Get context gathering configuration."""
|
|
264
|
+
context_data = self._data.get("context", {})
|
|
265
|
+
return ContextConfig(**context_data) if context_data else ContextConfig()
|
claude_dev_cli/context.py
CHANGED
|
@@ -16,11 +16,30 @@ class ContextItem:
|
|
|
16
16
|
content: str
|
|
17
17
|
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
18
18
|
|
|
19
|
+
def truncate(self, max_lines: Optional[int] = None) -> 'ContextItem':
|
|
20
|
+
"""Truncate content to specified number of lines."""
|
|
21
|
+
if max_lines is None:
|
|
22
|
+
return self
|
|
23
|
+
|
|
24
|
+
lines = self.content.split('\n')
|
|
25
|
+
if len(lines) <= max_lines:
|
|
26
|
+
return self
|
|
27
|
+
|
|
28
|
+
truncated_lines = lines[:max_lines]
|
|
29
|
+
truncated_lines.append(f"\n... (truncated {len(lines) - max_lines} more lines)")
|
|
30
|
+
|
|
31
|
+
return ContextItem(
|
|
32
|
+
type=self.type,
|
|
33
|
+
content='\n'.join(truncated_lines),
|
|
34
|
+
metadata={**self.metadata, 'truncated': True, 'original_lines': len(lines)}
|
|
35
|
+
)
|
|
36
|
+
|
|
19
37
|
def format_for_prompt(self) -> str:
|
|
20
38
|
"""Format this context item for inclusion in a prompt."""
|
|
21
39
|
if self.type == 'file':
|
|
22
40
|
path = self.metadata.get('path', 'unknown')
|
|
23
|
-
|
|
41
|
+
truncated_note = " (truncated)" if self.metadata.get('truncated') else ""
|
|
42
|
+
return f"# File: {path}{truncated_note}\n\n{self.content}\n"
|
|
24
43
|
elif self.type == 'git':
|
|
25
44
|
return f"# Git Context\n\n{self.content}\n"
|
|
26
45
|
elif self.type == 'dependency':
|
|
@@ -165,8 +184,13 @@ class GitContext:
|
|
|
165
184
|
except Exception:
|
|
166
185
|
return []
|
|
167
186
|
|
|
168
|
-
def gather(self, include_diff: bool = False) -> ContextItem:
|
|
169
|
-
"""Gather all git context.
|
|
187
|
+
def gather(self, include_diff: bool = False, max_diff_lines: int = 200) -> ContextItem:
|
|
188
|
+
"""Gather all git context.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
include_diff: Include staged diff in context
|
|
192
|
+
max_diff_lines: Maximum lines of diff to include
|
|
193
|
+
"""
|
|
170
194
|
parts = []
|
|
171
195
|
|
|
172
196
|
branch = self.get_current_branch()
|
|
@@ -186,7 +210,12 @@ class GitContext:
|
|
|
186
210
|
if include_diff:
|
|
187
211
|
staged = self.get_staged_diff()
|
|
188
212
|
if staged:
|
|
189
|
-
|
|
213
|
+
diff_lines = staged.split('\n')
|
|
214
|
+
if len(diff_lines) > max_diff_lines:
|
|
215
|
+
truncated_diff = '\n'.join(diff_lines[:max_diff_lines])
|
|
216
|
+
parts.append(f"\nStaged changes (truncated {len(diff_lines) - max_diff_lines} lines):\n{truncated_diff}\n... (diff truncated)")
|
|
217
|
+
else:
|
|
218
|
+
parts.append(f"\nStaged changes:\n{staged}")
|
|
190
219
|
|
|
191
220
|
content = "\n".join(parts) if parts else "No git context available"
|
|
192
221
|
|
|
@@ -310,10 +339,25 @@ class DependencyAnalyzer:
|
|
|
310
339
|
|
|
311
340
|
|
|
312
341
|
class ErrorContext:
|
|
313
|
-
"""Parse and format error context."""
|
|
342
|
+
"""Parse and format error context for multiple languages."""
|
|
314
343
|
|
|
315
344
|
@staticmethod
|
|
316
|
-
def
|
|
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]:
|
|
317
361
|
"""Parse Python traceback into structured data."""
|
|
318
362
|
lines = error_text.split('\n')
|
|
319
363
|
|
|
@@ -362,24 +406,183 @@ class ErrorContext:
|
|
|
362
406
|
'raw': error_text
|
|
363
407
|
}
|
|
364
408
|
|
|
365
|
-
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'}
|
|
366
564
|
|
|
367
565
|
@staticmethod
|
|
368
566
|
def format_for_ai(error_text: str) -> str:
|
|
369
|
-
"""Format error for AI consumption."""
|
|
567
|
+
"""Format error for AI consumption with language detection."""
|
|
370
568
|
parsed = ErrorContext.parse_traceback(error_text)
|
|
371
569
|
|
|
372
570
|
if 'error_type' not in parsed:
|
|
373
571
|
return error_text
|
|
374
572
|
|
|
573
|
+
language = parsed.get('language', 'unknown')
|
|
375
574
|
parts = [
|
|
575
|
+
f"Language: {language.title()}",
|
|
376
576
|
f"Error Type: {parsed['error_type']}",
|
|
377
577
|
f"Error Message: {parsed.get('error_message', 'N/A')}",
|
|
378
578
|
"\nStack Trace:"
|
|
379
579
|
]
|
|
380
580
|
|
|
381
581
|
for i, frame in enumerate(parsed.get('frames', []), 1):
|
|
382
|
-
|
|
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')}")
|
|
383
586
|
if 'code' in frame:
|
|
384
587
|
parts.append(f" > {frame['code']}")
|
|
385
588
|
|
|
@@ -400,29 +603,42 @@ class ErrorContext:
|
|
|
400
603
|
class ContextGatherer:
|
|
401
604
|
"""Main context gathering coordinator."""
|
|
402
605
|
|
|
403
|
-
def __init__(self, project_root: Optional[Path] = None):
|
|
606
|
+
def __init__(self, project_root: Optional[Path] = None, max_file_lines: int = 1000, max_related_files: int = 5):
|
|
404
607
|
self.project_root = project_root or Path.cwd()
|
|
405
608
|
self.git = GitContext(self.project_root)
|
|
406
609
|
self.dependencies = DependencyAnalyzer(self.project_root)
|
|
407
610
|
self.error_parser = ErrorContext()
|
|
611
|
+
self.max_file_lines = max_file_lines
|
|
612
|
+
self.max_related_files = max_related_files
|
|
408
613
|
|
|
409
614
|
def gather_for_file(
|
|
410
615
|
self,
|
|
411
616
|
file_path: Path,
|
|
412
617
|
include_git: bool = True,
|
|
413
618
|
include_dependencies: bool = True,
|
|
414
|
-
include_related: bool = True
|
|
619
|
+
include_related: bool = True,
|
|
620
|
+
max_lines: Optional[int] = None
|
|
415
621
|
) -> Context:
|
|
416
|
-
"""Gather context for a specific file operation.
|
|
622
|
+
"""Gather context for a specific file operation.
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
file_path: Path to the file to gather context for
|
|
626
|
+
include_git: Include git context
|
|
627
|
+
include_dependencies: Include dependency information
|
|
628
|
+
include_related: Include related files
|
|
629
|
+
max_lines: Maximum lines per file (uses instance default if None)
|
|
630
|
+
"""
|
|
417
631
|
context = Context()
|
|
632
|
+
max_lines = max_lines or self.max_file_lines
|
|
418
633
|
|
|
419
634
|
# Add the file itself
|
|
420
635
|
if file_path.exists():
|
|
421
|
-
|
|
636
|
+
item = ContextItem(
|
|
422
637
|
type='file',
|
|
423
638
|
content=file_path.read_text(),
|
|
424
639
|
metadata={'path': str(file_path)}
|
|
425
|
-
)
|
|
640
|
+
)
|
|
641
|
+
context.add(item.truncate(max_lines))
|
|
426
642
|
|
|
427
643
|
# Add git context
|
|
428
644
|
if include_git and self.git.is_git_repo():
|
|
@@ -438,21 +654,31 @@ class ContextGatherer:
|
|
|
438
654
|
self,
|
|
439
655
|
error_text: str,
|
|
440
656
|
file_path: Optional[Path] = None,
|
|
441
|
-
include_git: bool = True
|
|
657
|
+
include_git: bool = True,
|
|
658
|
+
max_lines: Optional[int] = None
|
|
442
659
|
) -> Context:
|
|
443
|
-
"""Gather context for error debugging.
|
|
660
|
+
"""Gather context for error debugging.
|
|
661
|
+
|
|
662
|
+
Args:
|
|
663
|
+
error_text: The error message or traceback
|
|
664
|
+
file_path: Optional file path related to the error
|
|
665
|
+
include_git: Include git context
|
|
666
|
+
max_lines: Maximum lines per file (uses instance default if None)
|
|
667
|
+
"""
|
|
444
668
|
context = Context()
|
|
669
|
+
max_lines = max_lines or self.max_file_lines
|
|
445
670
|
|
|
446
671
|
# Add error context
|
|
447
672
|
context.add(self.error_parser.gather(error_text))
|
|
448
673
|
|
|
449
674
|
# Add file if provided
|
|
450
675
|
if file_path and file_path.exists():
|
|
451
|
-
|
|
676
|
+
item = ContextItem(
|
|
452
677
|
type='file',
|
|
453
678
|
content=file_path.read_text(),
|
|
454
679
|
metadata={'path': str(file_path)}
|
|
455
|
-
)
|
|
680
|
+
)
|
|
681
|
+
context.add(item.truncate(max_lines))
|
|
456
682
|
|
|
457
683
|
# Add git context
|
|
458
684
|
if include_git and self.git.is_git_repo():
|
|
@@ -464,16 +690,27 @@ class ContextGatherer:
|
|
|
464
690
|
self,
|
|
465
691
|
file_path: Path,
|
|
466
692
|
include_git: bool = True,
|
|
467
|
-
include_tests: bool = True
|
|
693
|
+
include_tests: bool = True,
|
|
694
|
+
max_lines: Optional[int] = None
|
|
468
695
|
) -> Context:
|
|
469
|
-
"""Gather context for code review.
|
|
696
|
+
"""Gather context for code review.
|
|
697
|
+
|
|
698
|
+
Args:
|
|
699
|
+
file_path: Path to the file to review
|
|
700
|
+
include_git: Include git context
|
|
701
|
+
include_tests: Try to find and include test files
|
|
702
|
+
max_lines: Maximum lines per file (uses instance default if None)
|
|
703
|
+
"""
|
|
470
704
|
context = self.gather_for_file(
|
|
471
705
|
file_path,
|
|
472
706
|
include_git=include_git,
|
|
473
707
|
include_dependencies=True,
|
|
474
|
-
include_related=True
|
|
708
|
+
include_related=True,
|
|
709
|
+
max_lines=max_lines
|
|
475
710
|
)
|
|
476
711
|
|
|
712
|
+
max_lines = max_lines or self.max_file_lines
|
|
713
|
+
|
|
477
714
|
# Try to find test file
|
|
478
715
|
if include_tests:
|
|
479
716
|
test_patterns = [
|
|
@@ -484,11 +721,13 @@ class ContextGatherer:
|
|
|
484
721
|
|
|
485
722
|
for test_file in test_patterns:
|
|
486
723
|
if test_file.exists():
|
|
487
|
-
|
|
724
|
+
item = ContextItem(
|
|
488
725
|
type='file',
|
|
489
726
|
content=test_file.read_text(),
|
|
490
727
|
metadata={'path': str(test_file), 'is_test': True}
|
|
491
|
-
)
|
|
728
|
+
)
|
|
729
|
+
context.add(item.truncate(max_lines))
|
|
492
730
|
break
|
|
493
731
|
|
|
494
732
|
return context
|
|
733
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: claude-dev-cli
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.2
|
|
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
|
|
@@ -79,12 +79,17 @@ A powerful command-line tool for developers using Claude AI with multi-API routi
|
|
|
79
79
|
- **Variable Substitution**: Use {{variable}} placeholders for dynamic content
|
|
80
80
|
- **Categories**: Organize templates by category (review, testing, debugging, etc.)
|
|
81
81
|
|
|
82
|
-
### 🧠 Context Intelligence (
|
|
83
|
-
- **Auto-Context**: `--auto-context` flag for intelligent context gathering
|
|
82
|
+
### 🧠 Context Intelligence (v0.8.0+)
|
|
83
|
+
- **Auto-Context**: `--auto-context` flag on 7 commands for intelligent context gathering
|
|
84
|
+
- `ask`, `review`, `debug`, `refactor` (v0.8.0)
|
|
85
|
+
- `git commit`, `generate tests`, `generate docs` (v0.8.1)
|
|
84
86
|
- **Git Integration**: Automatically include branch, commits, modified files
|
|
85
87
|
- **Dependency Analysis**: Parse imports and include related files
|
|
86
|
-
- **Error Parsing
|
|
88
|
+
- **Multi-Language Error Parsing** (v0.8.2): Python, JavaScript/TypeScript, Go, Rust, Java
|
|
89
|
+
- **Context Summary** (v0.8.2): Preview context before API calls with `cdc context summary`
|
|
90
|
+
- **Smart Truncation**: Prevent token limits with configurable file size limits
|
|
87
91
|
- **Project Memory**: Remember preferences per project
|
|
92
|
+
- **Global Config**: Set context defaults in `~/.claude-dev-cli/config.json`
|
|
88
93
|
|
|
89
94
|
### 🎒 TOON Format Support (Optional)
|
|
90
95
|
- **Token Reduction**: 30-60% fewer tokens than JSON
|
|
@@ -156,6 +161,9 @@ cdc generate tests mymodule.py -o tests/test_mymodule.py
|
|
|
156
161
|
# Generate tests with interactive refinement
|
|
157
162
|
cdc generate tests mymodule.py --interactive
|
|
158
163
|
|
|
164
|
+
# Generate tests with context (includes dependencies, related files) - NEW in v0.8.1
|
|
165
|
+
cdc generate tests mymodule.py --auto-context
|
|
166
|
+
|
|
159
167
|
# Code review
|
|
160
168
|
cdc review mymodule.py
|
|
161
169
|
|
|
@@ -174,6 +182,9 @@ cdc generate docs mymodule.py
|
|
|
174
182
|
# Generate docs with interactive refinement
|
|
175
183
|
cdc generate docs mymodule.py --interactive
|
|
176
184
|
|
|
185
|
+
# Generate docs with context (includes dependencies) - NEW in v0.8.1
|
|
186
|
+
cdc generate docs mymodule.py --auto-context
|
|
187
|
+
|
|
177
188
|
# Refactor with context (includes related files)
|
|
178
189
|
cdc refactor legacy_code.py --auto-context
|
|
179
190
|
|
|
@@ -183,9 +194,13 @@ cdc refactor legacy_code.py --interactive
|
|
|
183
194
|
# Git commit message
|
|
184
195
|
git add .
|
|
185
196
|
cdc git commit
|
|
197
|
+
|
|
198
|
+
# Git commit message with context (includes history, branch) - NEW in v0.8.1
|
|
199
|
+
git add .
|
|
200
|
+
cdc git commit --auto-context
|
|
186
201
|
```
|
|
187
202
|
|
|
188
|
-
### 4. Context-Aware Operations (
|
|
203
|
+
### 4. Context-Aware Operations (v0.8.0+)
|
|
189
204
|
|
|
190
205
|
```bash
|
|
191
206
|
# Auto-context includes: git info, dependencies, related files
|
|
@@ -194,9 +209,16 @@ cdc git commit
|
|
|
194
209
|
cdc review mymodule.py --auto-context
|
|
195
210
|
# ✓ Context gathered (git, dependencies, tests)
|
|
196
211
|
|
|
197
|
-
# Debug with parsed error details
|
|
212
|
+
# Debug with parsed error details (multi-language support)
|
|
198
213
|
python broken.py 2>&1 | cdc debug -f broken.py --auto-context
|
|
214
|
+
node app.js 2>&1 | cdc debug --auto-context # JavaScript/TypeScript
|
|
215
|
+
go run main.go 2>&1 | cdc debug --auto-context # Go
|
|
199
216
|
# ✓ Context gathered (error details, git context)
|
|
217
|
+
# Supports: Python, JavaScript, TypeScript, Go, Rust, Java
|
|
218
|
+
|
|
219
|
+
# Preview context before making API calls - NEW in v0.8.2
|
|
220
|
+
cdc context summary mymodule.py
|
|
221
|
+
# Shows: files, sizes, lines, estimated tokens, truncation warnings
|
|
200
222
|
|
|
201
223
|
# Ask questions with file context
|
|
202
224
|
cdc ask -f mycode.py --auto-context "how can I improve this?"
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
claude_dev_cli/__init__.py,sha256=
|
|
2
|
-
claude_dev_cli/cli.py,sha256=
|
|
1
|
+
claude_dev_cli/__init__.py,sha256=zz-bOjjR01C_VWxFEsIWbdJui0kBuDLpDfJV7yAFFRc,469
|
|
2
|
+
claude_dev_cli/cli.py,sha256=BjTST8JSd8quldFxs09ZzlAPGt-rFIYhKUCWfj1pIn4,55100
|
|
3
3
|
claude_dev_cli/commands.py,sha256=RKGx2rv56PM6eErvA2uoQ20hY8babuI5jav8nCUyUOk,3964
|
|
4
|
-
claude_dev_cli/config.py,sha256=
|
|
5
|
-
claude_dev_cli/context.py,sha256=
|
|
4
|
+
claude_dev_cli/config.py,sha256=OLx0xWDf1RIK6RIxl5OKVS4aOSMZZOKxBDmzfQX-muk,9745
|
|
5
|
+
claude_dev_cli/context.py,sha256=1TlLzpREFZDEIuU7RAtlkjxARKWZpnxHHvK283sUAZE,26714
|
|
6
6
|
claude_dev_cli/core.py,sha256=yaLjEixDvPzvUy4fJ2UB7nMpPPLyKACjR-RuM-1OQBY,4780
|
|
7
7
|
claude_dev_cli/history.py,sha256=iQlqgTnXCsyCq5q-XaDl7V5MyPKQ3bx7o_k76-xWSAA,6863
|
|
8
8
|
claude_dev_cli/secure_storage.py,sha256=TK3WOaU7a0yTOtzdP_t_28fDRp2lovANNAC6MBdm4nQ,7096
|
|
@@ -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.
|
|
21
|
-
claude_dev_cli-0.8.
|
|
22
|
-
claude_dev_cli-0.8.
|
|
23
|
-
claude_dev_cli-0.8.
|
|
24
|
-
claude_dev_cli-0.8.
|
|
25
|
-
claude_dev_cli-0.8.
|
|
20
|
+
claude_dev_cli-0.8.2.dist-info/licenses/LICENSE,sha256=DGueuJwMJtMwgLO5mWlS0TaeBrFwQuNpNZ22PU9J2bw,1062
|
|
21
|
+
claude_dev_cli-0.8.2.dist-info/METADATA,sha256=4Pyai7Is7NJezxG5ypTm5rML1wEqIlOwHI5GZDlFFs0,15708
|
|
22
|
+
claude_dev_cli-0.8.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
23
|
+
claude_dev_cli-0.8.2.dist-info/entry_points.txt,sha256=zymgUIIVpFTARkFmxAuW2A4BQsNITh_L0uU-XunytHg,85
|
|
24
|
+
claude_dev_cli-0.8.2.dist-info/top_level.txt,sha256=m7MF6LOIuTe41IT5Fgt0lc-DK1EgM4gUU_IZwWxK0pg,15
|
|
25
|
+
claude_dev_cli-0.8.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|