claude-dev-cli 0.7.0__py3-none-any.whl → 0.8.1__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.7.0"
12
+ __version__ = "0.8.1"
13
13
  __author__ = "Julio"
14
14
  __license__ = "MIT"
15
15
 
claude_dev_cli/cli.py CHANGED
@@ -57,6 +57,7 @@ except Exception:
57
57
  @click.option('-a', '--api', help='API config to use')
58
58
  @click.option('-m', '--model', help='Claude model to use')
59
59
  @click.option('--stream/--no-stream', default=True, help='Stream response')
60
+ @click.option('--auto-context', is_flag=True, help='Automatically include git, dependencies, and related files')
60
61
  @click.pass_context
61
62
  def ask(
62
63
  ctx: click.Context,
@@ -65,7 +66,8 @@ def ask(
65
66
  system: Optional[str],
66
67
  api: Optional[str],
67
68
  model: Optional[str],
68
- stream: bool
69
+ stream: bool,
70
+ auto_context: bool
69
71
  ) -> None:
70
72
  """Ask Claude a question (single-shot mode)."""
71
73
  console = ctx.obj['console']
@@ -73,7 +75,18 @@ def ask(
73
75
  # Build prompt
74
76
  prompt_parts = []
75
77
 
76
- if file:
78
+ # Gather context if requested
79
+ if auto_context and file:
80
+ from claude_dev_cli.context import ContextGatherer
81
+
82
+ with console.status("[bold blue]Gathering context..."):
83
+ gatherer = ContextGatherer()
84
+ context = gatherer.gather_for_file(Path(file))
85
+ context_info = context.format_for_prompt()
86
+
87
+ console.print("[dim]✓ Context gathered[/dim]")
88
+ prompt_parts.append(context_info)
89
+ elif file:
77
90
  with open(file, 'r') as f:
78
91
  file_content = f.read()
79
92
  prompt_parts.append(f"File: {file}\n\n{file_content}\n\n")
@@ -448,20 +461,37 @@ def generate() -> None:
448
461
  @click.option('-o', '--output', type=click.Path(), help='Output file path')
449
462
  @click.option('-a', '--api', help='API config to use')
450
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')
451
465
  @click.pass_context
452
466
  def gen_tests(
453
467
  ctx: click.Context,
454
468
  file_path: str,
455
469
  output: Optional[str],
456
470
  api: Optional[str],
457
- interactive: bool
471
+ interactive: bool,
472
+ auto_context: bool
458
473
  ) -> None:
459
474
  """Generate pytest tests for a Python file."""
460
475
  console = ctx.obj['console']
461
476
 
462
477
  try:
463
- with console.status("[bold blue]Generating tests..."):
464
- result = generate_tests(file_path, api_config_name=api)
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)
465
495
 
466
496
  if interactive:
467
497
  # Show initial result
@@ -517,20 +547,37 @@ def gen_tests(
517
547
  @click.option('-o', '--output', type=click.Path(), help='Output file path')
518
548
  @click.option('-a', '--api', help='API config to use')
519
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')
520
551
  @click.pass_context
521
552
  def gen_docs(
522
553
  ctx: click.Context,
523
554
  file_path: str,
524
555
  output: Optional[str],
525
556
  api: Optional[str],
526
- interactive: bool
557
+ interactive: bool,
558
+ auto_context: bool
527
559
  ) -> None:
528
560
  """Generate documentation for a Python file."""
529
561
  console = ctx.obj['console']
530
562
 
531
563
  try:
532
- with console.status("[bold blue]Generating documentation..."):
533
- result = generate_docs(file_path, api_config_name=api)
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)
534
581
 
535
582
  if interactive:
536
583
  console.print("\n[bold]Initial Documentation:[/bold]\n")
@@ -584,19 +631,42 @@ def gen_docs(
584
631
  @click.argument('file_path', type=click.Path(exists=True))
585
632
  @click.option('-a', '--api', help='API config to use')
586
633
  @click.option('-i', '--interactive', is_flag=True, help='Interactive follow-up questions')
634
+ @click.option('--auto-context', is_flag=True, help='Automatically include git, dependencies, and related files')
587
635
  @click.pass_context
588
636
  def review(
589
637
  ctx: click.Context,
590
638
  file_path: str,
591
639
  api: Optional[str],
592
- interactive: bool
640
+ interactive: bool,
641
+ auto_context: bool
593
642
  ) -> None:
594
643
  """Review code for bugs and improvements."""
595
644
  console = ctx.obj['console']
596
645
 
597
646
  try:
647
+ # Gather context if requested
648
+ context_info = ""
649
+ if auto_context:
650
+ from claude_dev_cli.context import ContextGatherer
651
+
652
+ with console.status("[bold blue]Gathering context..."):
653
+ gatherer = ContextGatherer()
654
+ context = gatherer.gather_for_review(Path(file_path))
655
+ context_info = context.format_for_prompt()
656
+
657
+ console.print("[dim]✓ Context gathered (git, dependencies, tests)[/dim]")
658
+
598
659
  with console.status("[bold blue]Reviewing code..."):
599
- result = code_review(file_path, api_config_name=api)
660
+ # If we have context, prepend it to the file analysis
661
+ if context_info:
662
+ # Read file separately for context-aware review
663
+ result = code_review(file_path, api_config_name=api)
664
+ # The context module already includes the file, so we use it differently
665
+ client = ClaudeClient(api_config_name=api)
666
+ enhanced_prompt = f"{context_info}\n\nPlease review this code for bugs and improvements."
667
+ result = client.call(enhanced_prompt)
668
+ else:
669
+ result = code_review(file_path, api_config_name=api)
600
670
 
601
671
  md = Markdown(result)
602
672
  console.print(md)
@@ -633,12 +703,14 @@ def review(
633
703
  @click.option('-f', '--file', type=click.Path(exists=True), help='File to debug')
634
704
  @click.option('-e', '--error', help='Error message to analyze')
635
705
  @click.option('-a', '--api', help='API config to use')
706
+ @click.option('--auto-context', is_flag=True, help='Automatically include git context and parse error details')
636
707
  @click.pass_context
637
708
  def debug(
638
709
  ctx: click.Context,
639
710
  file: Optional[str],
640
711
  error: Optional[str],
641
- api: Optional[str]
712
+ api: Optional[str],
713
+ auto_context: bool
642
714
  ) -> None:
643
715
  """Debug code and analyze errors."""
644
716
  console = ctx.obj['console']
@@ -648,13 +720,33 @@ def debug(
648
720
  if not sys.stdin.isatty():
649
721
  stdin_content = sys.stdin.read().strip()
650
722
 
723
+ error_text = error or stdin_content
724
+
651
725
  try:
652
- with console.status("[bold blue]Analyzing error..."):
653
- result = debug_code(
654
- file_path=file,
655
- error_message=error or stdin_content,
656
- api_config_name=api
657
- )
726
+ # Gather context if requested
727
+ if auto_context and error_text:
728
+ from claude_dev_cli.context import ContextGatherer
729
+
730
+ with console.status("[bold blue]Gathering context..."):
731
+ gatherer = ContextGatherer()
732
+ file_path = Path(file) if file else None
733
+ context = gatherer.gather_for_error(error_text, file_path=file_path)
734
+ context_info = context.format_for_prompt()
735
+
736
+ console.print("[dim]✓ Context gathered (error details, git context)[/dim]")
737
+
738
+ # Use context-aware analysis
739
+ client = ClaudeClient(api_config_name=api)
740
+ enhanced_prompt = f"{context_info}\n\nPlease analyze this error and suggest fixes."
741
+ result = client.call(enhanced_prompt)
742
+ else:
743
+ # Original behavior
744
+ with console.status("[bold blue]Analyzing error..."):
745
+ result = debug_code(
746
+ file_path=file,
747
+ error_message=error_text,
748
+ api_config_name=api
749
+ )
658
750
 
659
751
  md = Markdown(result)
660
752
  console.print(md)
@@ -669,20 +761,38 @@ def debug(
669
761
  @click.option('-o', '--output', type=click.Path(), help='Output file path')
670
762
  @click.option('-a', '--api', help='API config to use')
671
763
  @click.option('-i', '--interactive', is_flag=True, help='Interactive refinement mode')
764
+ @click.option('--auto-context', is_flag=True, help='Automatically include git, dependencies, and related files')
672
765
  @click.pass_context
673
766
  def refactor(
674
767
  ctx: click.Context,
675
768
  file_path: str,
676
769
  output: Optional[str],
677
770
  api: Optional[str],
678
- interactive: bool
771
+ interactive: bool,
772
+ auto_context: bool
679
773
  ) -> None:
680
774
  """Suggest refactoring improvements."""
681
775
  console = ctx.obj['console']
682
776
 
683
777
  try:
684
- with console.status("[bold blue]Analyzing code..."):
685
- result = refactor_code(file_path, api_config_name=api)
778
+ # Gather context if requested
779
+ if auto_context:
780
+ from claude_dev_cli.context import ContextGatherer
781
+
782
+ with console.status("[bold blue]Gathering context..."):
783
+ gatherer = ContextGatherer()
784
+ context = gatherer.gather_for_file(Path(file_path))
785
+ context_info = context.format_for_prompt()
786
+
787
+ console.print("[dim]✓ Context gathered[/dim]")
788
+
789
+ # Use context-aware refactoring
790
+ client = ClaudeClient(api_config_name=api)
791
+ enhanced_prompt = f"{context_info}\n\nPlease suggest refactoring improvements for the main file."
792
+ result = client.call(enhanced_prompt)
793
+ else:
794
+ with console.status("[bold blue]Analyzing code..."):
795
+ result = refactor_code(file_path, api_config_name=api)
686
796
 
687
797
  if interactive:
688
798
  console.print("\n[bold]Initial Refactoring:[/bold]\n")
@@ -740,14 +850,31 @@ def git() -> None:
740
850
 
741
851
  @git.command('commit')
742
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')
743
854
  @click.pass_context
744
- def git_commit(ctx: click.Context, api: Optional[str]) -> None:
855
+ def git_commit(ctx: click.Context, api: Optional[str], auto_context: bool) -> None:
745
856
  """Generate commit message from staged changes."""
746
857
  console = ctx.obj['console']
747
858
 
748
859
  try:
749
- with console.status("[bold blue]Analyzing changes..."):
750
- result = git_commit_message(api_config_name=api)
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)
751
878
 
752
879
  console.print("\n[bold green]Suggested commit message:[/bold green]")
753
880
  console.print(Panel(result, border_style="green"))
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
 
@@ -25,6 +37,19 @@ class ProjectProfile(BaseModel):
25
37
  api_config: str # Name of the API config to use
26
38
  system_prompt: Optional[str] = None
27
39
  allowed_commands: List[str] = Field(default_factory=lambda: ["all"])
40
+
41
+ # Project memory - preferences and patterns
42
+ auto_context: bool = False # Default value for --auto-context flag
43
+ coding_style: Optional[str] = None # Preferred coding style
44
+ test_framework: Optional[str] = None # Preferred test framework
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
28
53
 
29
54
 
30
55
  class Config:
@@ -61,6 +86,7 @@ class Config:
61
86
  "project_profiles": [],
62
87
  "default_model": "claude-3-5-sonnet-20241022",
63
88
  "max_tokens": 4096,
89
+ "context": ContextConfig().model_dump(),
64
90
  }
65
91
  self._save_config(default_config)
66
92
  return default_config
@@ -232,3 +258,8 @@ class Config:
232
258
  def get_max_tokens(self) -> int:
233
259
  """Get default max tokens."""
234
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()
@@ -0,0 +1,559 @@
1
+ """Intelligent context gathering for AI operations."""
2
+
3
+ import ast
4
+ import json
5
+ import re
6
+ import subprocess
7
+ from pathlib import Path
8
+ from typing import Any, Dict, List, Optional, Set
9
+ from dataclasses import dataclass, field
10
+
11
+
12
+ @dataclass
13
+ class ContextItem:
14
+ """A single piece of context information."""
15
+ type: str # 'file', 'git', 'dependency', 'error'
16
+ content: str
17
+ metadata: Dict[str, Any] = field(default_factory=dict)
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
+
37
+ def format_for_prompt(self) -> str:
38
+ """Format this context item for inclusion in a prompt."""
39
+ if self.type == 'file':
40
+ path = self.metadata.get('path', 'unknown')
41
+ truncated_note = " (truncated)" if self.metadata.get('truncated') else ""
42
+ return f"# File: {path}{truncated_note}\n\n{self.content}\n"
43
+ elif self.type == 'git':
44
+ return f"# Git Context\n\n{self.content}\n"
45
+ elif self.type == 'dependency':
46
+ return f"# Dependencies\n\n{self.content}\n"
47
+ elif self.type == 'error':
48
+ return f"# Error Context\n\n{self.content}\n"
49
+ else:
50
+ return self.content
51
+
52
+
53
+ @dataclass
54
+ class Context:
55
+ """Collection of context items."""
56
+ items: List[ContextItem] = field(default_factory=list)
57
+
58
+ def add(self, item: ContextItem) -> None:
59
+ """Add a context item."""
60
+ self.items.append(item)
61
+
62
+ def format_for_prompt(self) -> str:
63
+ """Format all context items for inclusion in a prompt."""
64
+ if not self.items:
65
+ return ""
66
+
67
+ parts = ["# Context Information\n"]
68
+ for item in self.items:
69
+ parts.append(item.format_for_prompt())
70
+
71
+ return "\n".join(parts)
72
+
73
+ def get_by_type(self, context_type: str) -> List[ContextItem]:
74
+ """Get all context items of a specific type."""
75
+ return [item for item in self.items if item.type == context_type]
76
+
77
+
78
+ class GitContext:
79
+ """Gather Git-related context."""
80
+
81
+ def __init__(self, cwd: Optional[Path] = None):
82
+ self.cwd = cwd or Path.cwd()
83
+
84
+ def is_git_repo(self) -> bool:
85
+ """Check if current directory is a git repository."""
86
+ try:
87
+ result = subprocess.run(
88
+ ['git', 'rev-parse', '--git-dir'],
89
+ cwd=self.cwd,
90
+ capture_output=True,
91
+ text=True
92
+ )
93
+ return result.returncode == 0
94
+ except Exception:
95
+ return False
96
+
97
+ def get_current_branch(self) -> Optional[str]:
98
+ """Get the current git branch."""
99
+ try:
100
+ result = subprocess.run(
101
+ ['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
102
+ cwd=self.cwd,
103
+ capture_output=True,
104
+ text=True,
105
+ check=True
106
+ )
107
+ return result.stdout.strip()
108
+ except Exception:
109
+ return None
110
+
111
+ def get_recent_commits(self, count: int = 5) -> List[Dict[str, str]]:
112
+ """Get recent commit messages."""
113
+ try:
114
+ result = subprocess.run(
115
+ ['git', '--no-pager', 'log', f'-{count}', '--pretty=format:%h|%s|%an|%ar'],
116
+ cwd=self.cwd,
117
+ capture_output=True,
118
+ text=True,
119
+ check=True
120
+ )
121
+
122
+ commits = []
123
+ for line in result.stdout.strip().split('\n'):
124
+ if line:
125
+ parts = line.split('|', 3)
126
+ if len(parts) == 4:
127
+ commits.append({
128
+ 'hash': parts[0],
129
+ 'message': parts[1],
130
+ 'author': parts[2],
131
+ 'date': parts[3]
132
+ })
133
+ return commits
134
+ except Exception:
135
+ return []
136
+
137
+ def get_staged_diff(self) -> Optional[str]:
138
+ """Get diff of staged changes."""
139
+ try:
140
+ result = subprocess.run(
141
+ ['git', '--no-pager', 'diff', '--cached'],
142
+ cwd=self.cwd,
143
+ capture_output=True,
144
+ text=True,
145
+ check=True
146
+ )
147
+ return result.stdout if result.stdout else None
148
+ except Exception:
149
+ return None
150
+
151
+ def get_unstaged_diff(self) -> Optional[str]:
152
+ """Get diff of unstaged changes."""
153
+ try:
154
+ result = subprocess.run(
155
+ ['git', '--no-pager', 'diff'],
156
+ cwd=self.cwd,
157
+ capture_output=True,
158
+ text=True,
159
+ check=True
160
+ )
161
+ return result.stdout if result.stdout else None
162
+ except Exception:
163
+ return None
164
+
165
+ def get_modified_files(self) -> List[str]:
166
+ """Get list of modified files."""
167
+ try:
168
+ result = subprocess.run(
169
+ ['git', 'status', '--porcelain'],
170
+ cwd=self.cwd,
171
+ capture_output=True,
172
+ text=True,
173
+ check=True
174
+ )
175
+
176
+ files = []
177
+ for line in result.stdout.strip().split('\n'):
178
+ if line:
179
+ # Format: "XY filename"
180
+ parts = line.strip().split(maxsplit=1)
181
+ if len(parts) == 2:
182
+ files.append(parts[1])
183
+ return files
184
+ except Exception:
185
+ return []
186
+
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
+ """
194
+ parts = []
195
+
196
+ branch = self.get_current_branch()
197
+ if branch:
198
+ parts.append(f"Branch: {branch}")
199
+
200
+ commits = self.get_recent_commits(5)
201
+ if commits:
202
+ parts.append("\nRecent commits:")
203
+ for commit in commits:
204
+ parts.append(f" {commit['hash']} - {commit['message']} ({commit['date']})")
205
+
206
+ modified = self.get_modified_files()
207
+ if modified:
208
+ parts.append(f"\nModified files: {', '.join(modified[:10])}")
209
+
210
+ if include_diff:
211
+ staged = self.get_staged_diff()
212
+ if staged:
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}")
219
+
220
+ content = "\n".join(parts) if parts else "No git context available"
221
+
222
+ return ContextItem(
223
+ type='git',
224
+ content=content,
225
+ metadata={'branch': branch, 'modified_count': len(modified)}
226
+ )
227
+
228
+
229
+ class DependencyAnalyzer:
230
+ """Analyze project dependencies and imports."""
231
+
232
+ def __init__(self, project_root: Path):
233
+ self.project_root = project_root
234
+
235
+ def find_python_imports(self, file_path: Path) -> Set[str]:
236
+ """Extract imports from a Python file."""
237
+ imports = set()
238
+
239
+ try:
240
+ with open(file_path, 'r') as f:
241
+ tree = ast.parse(f.read())
242
+
243
+ for node in ast.walk(tree):
244
+ if isinstance(node, ast.Import):
245
+ for name in node.names:
246
+ imports.add(name.name.split('.')[0])
247
+ elif isinstance(node, ast.ImportFrom):
248
+ if node.module:
249
+ imports.add(node.module.split('.')[0])
250
+ except Exception:
251
+ pass
252
+
253
+ return imports
254
+
255
+ def find_related_files(self, file_path: Path, max_depth: int = 2) -> List[Path]:
256
+ """Find files related to the given file through imports."""
257
+ if not file_path.suffix == '.py':
258
+ return []
259
+
260
+ related = []
261
+ imports = self.find_python_imports(file_path)
262
+
263
+ # Look for local modules
264
+ for imp in imports:
265
+ # Try as module file
266
+ module_file = self.project_root / f"{imp}.py"
267
+ if module_file.exists() and module_file != file_path:
268
+ related.append(module_file)
269
+
270
+ # Try as package
271
+ package_init = self.project_root / imp / "__init__.py"
272
+ if package_init.exists():
273
+ related.append(package_init)
274
+
275
+ return related[:5] # Limit to avoid too many files
276
+
277
+ def get_dependency_files(self) -> List[Path]:
278
+ """Find dependency configuration files."""
279
+ files = []
280
+
281
+ # Python
282
+ for name in ['requirements.txt', 'setup.py', 'pyproject.toml', 'Pipfile']:
283
+ file = self.project_root / name
284
+ if file.exists():
285
+ files.append(file)
286
+
287
+ # Node.js
288
+ for name in ['package.json', 'package-lock.json']:
289
+ file = self.project_root / name
290
+ if file.exists():
291
+ files.append(file)
292
+
293
+ # Other
294
+ for name in ['Gemfile', 'go.mod', 'Cargo.toml']:
295
+ file = self.project_root / name
296
+ if file.exists():
297
+ files.append(file)
298
+
299
+ return files
300
+
301
+ def gather(self, target_file: Optional[Path] = None) -> ContextItem:
302
+ """Gather dependency context."""
303
+ parts = []
304
+
305
+ # Include dependency files
306
+ dep_files = self.get_dependency_files()
307
+ if dep_files:
308
+ parts.append("Dependency files:")
309
+ for file in dep_files[:3]: # Limit
310
+ parts.append(f" - {file.name}")
311
+ try:
312
+ content = file.read_text()
313
+ # Include only relevant parts
314
+ if file.suffix == '.json':
315
+ data = json.loads(content)
316
+ if 'dependencies' in data:
317
+ parts.append(f" Dependencies: {', '.join(list(data['dependencies'].keys())[:10])}")
318
+ elif file.suffix == '.txt':
319
+ lines = content.split('\n')[:20]
320
+ parts.append(f" Requirements: {', '.join([l.split('==')[0] for l in lines if l and not l.startswith('#')])}")
321
+ except Exception:
322
+ pass
323
+
324
+ # Related files if target specified
325
+ if target_file and target_file.exists():
326
+ related = self.find_related_files(target_file)
327
+ if related:
328
+ parts.append(f"\nRelated files for {target_file.name}:")
329
+ for file in related:
330
+ parts.append(f" - {file.relative_to(self.project_root)}")
331
+
332
+ content = "\n".join(parts) if parts else "No dependency context found"
333
+
334
+ return ContextItem(
335
+ type='dependency',
336
+ content=content,
337
+ metadata={'dependency_files': [str(f) for f in dep_files]}
338
+ )
339
+
340
+
341
+ class ErrorContext:
342
+ """Parse and format error context."""
343
+
344
+ @staticmethod
345
+ def parse_traceback(error_text: str) -> Dict[str, Any]:
346
+ """Parse Python traceback into structured data."""
347
+ lines = error_text.split('\n')
348
+
349
+ # Find traceback start
350
+ traceback_start = -1
351
+ for i, line in enumerate(lines):
352
+ if 'Traceback' in line:
353
+ traceback_start = i
354
+ break
355
+
356
+ if traceback_start == -1:
357
+ return {'raw': error_text}
358
+
359
+ # Extract frames
360
+ frames = []
361
+ current_frame = {}
362
+
363
+ for line in lines[traceback_start + 1:]:
364
+ if line.startswith(' File '):
365
+ if current_frame:
366
+ frames.append(current_frame)
367
+
368
+ # Parse: File "path", line X, in function
369
+ match = re.match(r'\s*File "([^"]+)", line (\d+), in (.+)', line)
370
+ if match:
371
+ current_frame = {
372
+ 'file': match.group(1),
373
+ 'line': int(match.group(2)),
374
+ 'function': match.group(3)
375
+ }
376
+ elif line.startswith(' ') and current_frame:
377
+ current_frame['code'] = line.strip()
378
+ elif line and not line.startswith(' '):
379
+ # Error message
380
+ if current_frame:
381
+ frames.append(current_frame)
382
+ current_frame = {}
383
+
384
+ error_type = line.split(':')[0] if ':' in line else line
385
+ error_message = line.split(':', 1)[1].strip() if ':' in line else ''
386
+
387
+ return {
388
+ 'frames': frames,
389
+ 'error_type': error_type,
390
+ 'error_message': error_message,
391
+ 'raw': error_text
392
+ }
393
+
394
+ return {'frames': frames, 'raw': error_text}
395
+
396
+ @staticmethod
397
+ def format_for_ai(error_text: str) -> str:
398
+ """Format error for AI consumption."""
399
+ parsed = ErrorContext.parse_traceback(error_text)
400
+
401
+ if 'error_type' not in parsed:
402
+ return error_text
403
+
404
+ parts = [
405
+ f"Error Type: {parsed['error_type']}",
406
+ f"Error Message: {parsed.get('error_message', 'N/A')}",
407
+ "\nStack Trace:"
408
+ ]
409
+
410
+ 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')}")
412
+ if 'code' in frame:
413
+ parts.append(f" > {frame['code']}")
414
+
415
+ return "\n".join(parts)
416
+
417
+ def gather(self, error_text: str) -> ContextItem:
418
+ """Gather error context."""
419
+ formatted = self.format_for_ai(error_text)
420
+ parsed = self.parse_traceback(error_text)
421
+
422
+ return ContextItem(
423
+ type='error',
424
+ content=formatted,
425
+ metadata=parsed
426
+ )
427
+
428
+
429
+ class ContextGatherer:
430
+ """Main context gathering coordinator."""
431
+
432
+ def __init__(self, project_root: Optional[Path] = None, max_file_lines: int = 1000, max_related_files: int = 5):
433
+ self.project_root = project_root or Path.cwd()
434
+ self.git = GitContext(self.project_root)
435
+ self.dependencies = DependencyAnalyzer(self.project_root)
436
+ self.error_parser = ErrorContext()
437
+ self.max_file_lines = max_file_lines
438
+ self.max_related_files = max_related_files
439
+
440
+ def gather_for_file(
441
+ self,
442
+ file_path: Path,
443
+ include_git: bool = True,
444
+ include_dependencies: bool = True,
445
+ include_related: bool = True,
446
+ max_lines: Optional[int] = None
447
+ ) -> Context:
448
+ """Gather context for a specific file operation.
449
+
450
+ Args:
451
+ file_path: Path to the file to gather context for
452
+ include_git: Include git context
453
+ include_dependencies: Include dependency information
454
+ include_related: Include related files
455
+ max_lines: Maximum lines per file (uses instance default if None)
456
+ """
457
+ context = Context()
458
+ max_lines = max_lines or self.max_file_lines
459
+
460
+ # Add the file itself
461
+ if file_path.exists():
462
+ item = ContextItem(
463
+ type='file',
464
+ content=file_path.read_text(),
465
+ metadata={'path': str(file_path)}
466
+ )
467
+ context.add(item.truncate(max_lines))
468
+
469
+ # Add git context
470
+ if include_git and self.git.is_git_repo():
471
+ context.add(self.git.gather(include_diff=False))
472
+
473
+ # Add dependency context
474
+ if include_dependencies:
475
+ context.add(self.dependencies.gather(target_file=file_path if include_related else None))
476
+
477
+ return context
478
+
479
+ def gather_for_error(
480
+ self,
481
+ error_text: str,
482
+ file_path: Optional[Path] = None,
483
+ include_git: bool = True,
484
+ max_lines: Optional[int] = None
485
+ ) -> Context:
486
+ """Gather context for error debugging.
487
+
488
+ Args:
489
+ error_text: The error message or traceback
490
+ file_path: Optional file path related to the error
491
+ include_git: Include git context
492
+ max_lines: Maximum lines per file (uses instance default if None)
493
+ """
494
+ context = Context()
495
+ max_lines = max_lines or self.max_file_lines
496
+
497
+ # Add error context
498
+ context.add(self.error_parser.gather(error_text))
499
+
500
+ # Add file if provided
501
+ if file_path and file_path.exists():
502
+ item = ContextItem(
503
+ type='file',
504
+ content=file_path.read_text(),
505
+ metadata={'path': str(file_path)}
506
+ )
507
+ context.add(item.truncate(max_lines))
508
+
509
+ # Add git context
510
+ if include_git and self.git.is_git_repo():
511
+ context.add(self.git.gather(include_diff=False))
512
+
513
+ return context
514
+
515
+ def gather_for_review(
516
+ self,
517
+ file_path: Path,
518
+ include_git: bool = True,
519
+ include_tests: bool = True,
520
+ max_lines: Optional[int] = None
521
+ ) -> Context:
522
+ """Gather context for code review.
523
+
524
+ Args:
525
+ file_path: Path to the file to review
526
+ include_git: Include git context
527
+ include_tests: Try to find and include test files
528
+ max_lines: Maximum lines per file (uses instance default if None)
529
+ """
530
+ context = self.gather_for_file(
531
+ file_path,
532
+ include_git=include_git,
533
+ include_dependencies=True,
534
+ include_related=True,
535
+ max_lines=max_lines
536
+ )
537
+
538
+ max_lines = max_lines or self.max_file_lines
539
+
540
+ # Try to find test file
541
+ if include_tests:
542
+ test_patterns = [
543
+ self.project_root / "tests" / f"test_{file_path.name}",
544
+ self.project_root / f"test_{file_path.name}",
545
+ file_path.parent / f"test_{file_path.name}"
546
+ ]
547
+
548
+ for test_file in test_patterns:
549
+ if test_file.exists():
550
+ item = ContextItem(
551
+ type='file',
552
+ content=test_file.read_text(),
553
+ metadata={'path': str(test_file), 'is_test': True}
554
+ )
555
+ context.add(item.truncate(max_lines))
556
+ break
557
+
558
+ return context
559
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-dev-cli
3
- Version: 0.7.0
3
+ Version: 0.8.1
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,6 +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 (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)
86
+ - **Git Integration**: Automatically include branch, commits, modified files
87
+ - **Dependency Analysis**: Parse imports and include related files
88
+ - **Error Parsing**: Structured Python traceback parsing
89
+ - **Smart Truncation**: Prevent token limits with configurable file size limits
90
+ - **Project Memory**: Remember preferences per project
91
+ - **Global Config**: Set context defaults in `~/.claude-dev-cli/config.json`
92
+
82
93
  ### 🎒 TOON Format Support (Optional)
83
94
  - **Token Reduction**: 30-60% fewer tokens than JSON
84
95
  - **Cost Savings**: Reduce API costs significantly
@@ -149,14 +160,20 @@ cdc generate tests mymodule.py -o tests/test_mymodule.py
149
160
  # Generate tests with interactive refinement
150
161
  cdc generate tests mymodule.py --interactive
151
162
 
163
+ # Generate tests with context (includes dependencies, related files) - NEW in v0.8.1
164
+ cdc generate tests mymodule.py --auto-context
165
+
152
166
  # Code review
153
167
  cdc review mymodule.py
154
168
 
169
+ # Code review with auto-context (includes git, dependencies, tests)
170
+ cdc review mymodule.py --auto-context
171
+
155
172
  # Code review with interactive follow-up questions
156
173
  cdc review mymodule.py --interactive
157
174
 
158
- # Debug errors
159
- python script.py 2>&1 | cdc debug
175
+ # Debug errors with intelligent error parsing
176
+ python script.py 2>&1 | cdc debug --auto-context
160
177
 
161
178
  # Generate documentation
162
179
  cdc generate docs mymodule.py
@@ -164,8 +181,11 @@ cdc generate docs mymodule.py
164
181
  # Generate docs with interactive refinement
165
182
  cdc generate docs mymodule.py --interactive
166
183
 
167
- # Refactor suggestions
168
- cdc refactor legacy_code.py
184
+ # Generate docs with context (includes dependencies) - NEW in v0.8.1
185
+ cdc generate docs mymodule.py --auto-context
186
+
187
+ # Refactor with context (includes related files)
188
+ cdc refactor legacy_code.py --auto-context
169
189
 
170
190
  # Refactor with interactive refinement
171
191
  cdc refactor legacy_code.py --interactive
@@ -173,6 +193,32 @@ cdc refactor legacy_code.py --interactive
173
193
  # Git commit message
174
194
  git add .
175
195
  cdc git commit
196
+
197
+ # Git commit message with context (includes history, branch) - NEW in v0.8.1
198
+ git add .
199
+ cdc git commit --auto-context
200
+ ```
201
+
202
+ ### 4. Context-Aware Operations (NEW in v0.8.0)
203
+
204
+ ```bash
205
+ # Auto-context includes: git info, dependencies, related files
206
+
207
+ # Review with full project context
208
+ cdc review mymodule.py --auto-context
209
+ # ✓ Context gathered (git, dependencies, tests)
210
+
211
+ # Debug with parsed error details
212
+ python broken.py 2>&1 | cdc debug -f broken.py --auto-context
213
+ # ✓ Context gathered (error details, git context)
214
+
215
+ # Ask questions with file context
216
+ cdc ask -f mycode.py --auto-context "how can I improve this?"
217
+ # ✓ Context gathered
218
+
219
+ # Refactor with related files
220
+ cdc refactor app.py --auto-context
221
+ # Automatically includes imported modules and dependencies
176
222
  ```
177
223
 
178
224
  ### 5. Custom Templates
@@ -1,7 +1,8 @@
1
- claude_dev_cli/__init__.py,sha256=sRGmW8jYKXWyBKqr2_-3AornNC32eNj4sESrXfoTUoQ,469
2
- claude_dev_cli/cli.py,sha256=feTRI1fQoDPJiB7JM45ZU2I1jEjXX643191H_U0rMio,44612
1
+ claude_dev_cli/__init__.py,sha256=0g1KP2ohJ45KCYW9EkcRcTA885E4Gqm1kDQbmIOdC-k,469
2
+ claude_dev_cli/cli.py,sha256=FjVq9QanwxM8WqSZPPM4-b50v9Ycy9ZDHEg9gBo-kCk,51304
3
3
  claude_dev_cli/commands.py,sha256=RKGx2rv56PM6eErvA2uoQ20hY8babuI5jav8nCUyUOk,3964
4
- claude_dev_cli/config.py,sha256=RGX0sKplHUsrJJmU-4FuWWjoTbQVgWaMT8DgRUofrR4,8134
4
+ claude_dev_cli/config.py,sha256=OLx0xWDf1RIK6RIxl5OKVS4aOSMZZOKxBDmzfQX-muk,9745
5
+ claude_dev_cli/context.py,sha256=Z3QYq4ZHAqpuv_xPZtXcBeWf0LCelzkybj8cBz2nBAo,19523
5
6
  claude_dev_cli/core.py,sha256=yaLjEixDvPzvUy4fJ2UB7nMpPPLyKACjR-RuM-1OQBY,4780
6
7
  claude_dev_cli/history.py,sha256=iQlqgTnXCsyCq5q-XaDl7V5MyPKQ3bx7o_k76-xWSAA,6863
7
8
  claude_dev_cli/secure_storage.py,sha256=TK3WOaU7a0yTOtzdP_t_28fDRp2lovANNAC6MBdm4nQ,7096
@@ -16,9 +17,9 @@ claude_dev_cli/plugins/base.py,sha256=H4HQet1I-a3WLCfE9F06Lp8NuFvVoIlou7sIgyJFK-
16
17
  claude_dev_cli/plugins/diff_editor/__init__.py,sha256=gqR5S2TyIVuq-sK107fegsutQ7Z-sgAIEbtc71FhXIM,101
17
18
  claude_dev_cli/plugins/diff_editor/plugin.py,sha256=M1bUoqpasD3ZNQo36Fu_8g92uySPZyG_ujMbj5UplsU,3073
18
19
  claude_dev_cli/plugins/diff_editor/viewer.py,sha256=1IOXIKw_01ppJx5C1dQt9Kr6U1TdAHT8_iUT5r_q0NM,17169
19
- claude_dev_cli-0.7.0.dist-info/licenses/LICENSE,sha256=DGueuJwMJtMwgLO5mWlS0TaeBrFwQuNpNZ22PU9J2bw,1062
20
- claude_dev_cli-0.7.0.dist-info/METADATA,sha256=YIN5U4urelKIEFjhd-s2EyHsPb-qEvhhqDwOek2hjPw,13364
21
- claude_dev_cli-0.7.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
22
- claude_dev_cli-0.7.0.dist-info/entry_points.txt,sha256=zymgUIIVpFTARkFmxAuW2A4BQsNITh_L0uU-XunytHg,85
23
- claude_dev_cli-0.7.0.dist-info/top_level.txt,sha256=m7MF6LOIuTe41IT5Fgt0lc-DK1EgM4gUU_IZwWxK0pg,15
24
- claude_dev_cli-0.7.0.dist-info/RECORD,,
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,,