claude-dev-cli 0.16.2__py3-none-any.whl → 0.18.0__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.

@@ -0,0 +1,535 @@
1
+ """Intelligent context gathering for ticket execution.
2
+
3
+ Pre-processes tickets to gather relevant context from the codebase,
4
+ including similar code, dependencies, framework patterns, and project structure.
5
+ This context helps the AI generate better, more consistent implementations.
6
+ """
7
+
8
+ import os
9
+ import re
10
+ from pathlib import Path
11
+ from typing import List, Dict, Any, Optional, Set
12
+ from dataclasses import dataclass, field
13
+ from collections import defaultdict
14
+
15
+ from claude_dev_cli.tickets.backend import Ticket
16
+ from claude_dev_cli.core import ClaudeClient
17
+
18
+
19
+ @dataclass
20
+ class CodeContext:
21
+ """Context gathered from existing codebase."""
22
+
23
+ # Project structure
24
+ project_root: Path
25
+ language: str
26
+ framework: Optional[str] = None
27
+
28
+ # Dependencies
29
+ dependencies: List[str] = field(default_factory=list)
30
+ installed_packages: Dict[str, str] = field(default_factory=dict) # name: version
31
+
32
+ # Similar code
33
+ similar_files: List[Dict[str, Any]] = field(default_factory=list) # path, similarity, purpose
34
+ similar_functions: List[Dict[str, Any]] = field(default_factory=list) # name, file, signature
35
+ similar_patterns: List[str] = field(default_factory=list) # patterns found in codebase
36
+
37
+ # Project conventions
38
+ naming_conventions: Dict[str, str] = field(default_factory=dict) # type: pattern
39
+ directory_structure: Dict[str, List[str]] = field(default_factory=dict) # purpose: paths
40
+ common_imports: List[str] = field(default_factory=list)
41
+
42
+ # Related files
43
+ related_models: List[str] = field(default_factory=list)
44
+ related_views: List[str] = field(default_factory=list)
45
+ related_controllers: List[str] = field(default_factory=list)
46
+ related_tests: List[str] = field(default_factory=list)
47
+
48
+ # Configuration
49
+ config_files: List[str] = field(default_factory=list)
50
+ env_variables: List[str] = field(default_factory=list)
51
+
52
+ def format_for_prompt(self) -> str:
53
+ """Format context for AI prompt."""
54
+ sections = []
55
+
56
+ sections.append(f"## Project Context\n")
57
+ sections.append(f"**Language:** {self.language}")
58
+ if self.framework:
59
+ sections.append(f"**Framework:** {self.framework}")
60
+ sections.append(f"**Root:** {self.project_root}\n")
61
+
62
+ if self.dependencies:
63
+ sections.append(f"\n## Dependencies ({len(self.dependencies)})")
64
+ for dep in self.dependencies[:20]: # Limit to 20
65
+ version = self.installed_packages.get(dep, "unknown")
66
+ sections.append(f"- {dep} ({version})")
67
+
68
+ if self.directory_structure:
69
+ sections.append(f"\n## Project Structure")
70
+ for purpose, paths in self.directory_structure.items():
71
+ sections.append(f"**{purpose}:** {', '.join(paths[:5])}")
72
+
73
+ if self.naming_conventions:
74
+ sections.append(f"\n## Naming Conventions")
75
+ for type_name, pattern in self.naming_conventions.items():
76
+ sections.append(f"- {type_name}: {pattern}")
77
+
78
+ if self.similar_files:
79
+ sections.append(f"\n## Similar Existing Code")
80
+ for file_info in self.similar_files[:5]:
81
+ sections.append(f"- {file_info['path']}: {file_info['purpose']}")
82
+
83
+ if self.similar_functions:
84
+ sections.append(f"\n## Related Functions")
85
+ for func in self.similar_functions[:10]:
86
+ sections.append(f"- {func['name']} in {func['file']}")
87
+
88
+ if self.common_imports:
89
+ sections.append(f"\n## Common Imports")
90
+ sections.append(", ".join(self.common_imports[:15]))
91
+
92
+ if self.related_models or self.related_views or self.related_controllers:
93
+ sections.append(f"\n## Related Files")
94
+ if self.related_models:
95
+ sections.append(f"Models: {', '.join(self.related_models[:5])}")
96
+ if self.related_views:
97
+ sections.append(f"Views: {', '.join(self.related_views[:5])}")
98
+ if self.related_controllers:
99
+ sections.append(f"Controllers: {', '.join(self.related_controllers[:5])}")
100
+
101
+ return "\n".join(sections)
102
+
103
+
104
+ class TicketContextGatherer:
105
+ """Gathers relevant context for ticket implementation.
106
+
107
+ Analyzes the existing codebase to provide context that helps
108
+ the AI generate better, more consistent code.
109
+ """
110
+
111
+ def __init__(self, project_root: Optional[Path] = None):
112
+ """Initialize context gatherer.
113
+
114
+ Args:
115
+ project_root: Root of the project (default: current directory)
116
+ """
117
+ self.project_root = project_root or Path.cwd()
118
+ self._file_cache: Dict[str, str] = {}
119
+ self._extension_map = {
120
+ '.py': 'Python',
121
+ '.js': 'JavaScript',
122
+ '.ts': 'TypeScript',
123
+ '.go': 'Go',
124
+ '.rs': 'Rust',
125
+ '.java': 'Java',
126
+ '.rb': 'Ruby',
127
+ '.php': 'PHP',
128
+ '.cs': 'C#',
129
+ '.cpp': 'C++',
130
+ '.c': 'C'
131
+ }
132
+
133
+ def gather_context(self, ticket: Ticket, ai_client: Optional[ClaudeClient] = None) -> CodeContext:
134
+ """Gather all relevant context for implementing a ticket.
135
+
136
+ Args:
137
+ ticket: Ticket to gather context for
138
+ ai_client: Optional AI client for semantic analysis
139
+
140
+ Returns:
141
+ CodeContext with all gathered information
142
+ """
143
+ context = CodeContext(project_root=self.project_root, language=self._detect_language())
144
+
145
+ # Gather different types of context
146
+ context.framework = self._detect_framework(context.language)
147
+ context.dependencies = self._find_dependencies(context.language)
148
+ context.installed_packages = self._get_installed_packages(context.language)
149
+ context.directory_structure = self._analyze_directory_structure()
150
+ context.naming_conventions = self._detect_naming_conventions(context.language)
151
+ context.common_imports = self._find_common_imports(context.language)
152
+ context.config_files = self._find_config_files()
153
+
154
+ # Find similar code based on ticket description
155
+ if ticket.description or ticket.title:
156
+ search_terms = self._extract_search_terms(ticket)
157
+ context.similar_files = self._find_similar_files(search_terms, context.language)
158
+ context.similar_functions = self._find_similar_functions(search_terms, context.language)
159
+ context.similar_patterns = self._find_patterns(search_terms)
160
+
161
+ # Find related files based on ticket type
162
+ if ticket.ticket_type in ['feature', 'bug', 'refactor']:
163
+ context.related_models = self._find_models(context.language, context.framework)
164
+ context.related_views = self._find_views(context.language, context.framework)
165
+ context.related_controllers = self._find_controllers(context.language, context.framework)
166
+ context.related_tests = self._find_tests(context.language)
167
+
168
+ # Use AI for semantic similarity (optional)
169
+ if ai_client:
170
+ context = self._enhance_with_ai(context, ticket, ai_client)
171
+
172
+ return context
173
+
174
+ def _detect_language(self) -> str:
175
+ """Detect primary programming language."""
176
+ extensions_count = defaultdict(int)
177
+
178
+ for file_path in self.project_root.rglob('*'):
179
+ if file_path.is_file() and not self._should_ignore(file_path):
180
+ ext = file_path.suffix.lower()
181
+ if ext in self._extension_map:
182
+ extensions_count[ext] += 1
183
+
184
+ if not extensions_count:
185
+ return "Unknown"
186
+
187
+ primary_ext = max(extensions_count, key=extensions_count.get)
188
+ return self._extension_map.get(primary_ext, "Unknown")
189
+
190
+ def _detect_framework(self, language: str) -> Optional[str]:
191
+ """Detect framework being used."""
192
+ framework_indicators = {
193
+ 'Python': {
194
+ 'Django': ['manage.py', 'settings.py', 'wsgi.py'],
195
+ 'Flask': ['app.py', 'wsgi.py', '__init__.py'],
196
+ 'FastAPI': ['main.py', 'app.py'],
197
+ },
198
+ 'JavaScript': {
199
+ 'React': ['package.json'], # Check for "react" in package.json
200
+ 'Vue': ['vue.config.js', 'package.json'],
201
+ 'Express': ['package.json'], # Check for "express"
202
+ },
203
+ 'TypeScript': {
204
+ 'Next.js': ['next.config.js', 'next.config.ts'],
205
+ 'NestJS': ['nest-cli.json'],
206
+ },
207
+ 'Go': {
208
+ 'Gin': ['go.mod'], # Check imports
209
+ 'Echo': ['go.mod'],
210
+ },
211
+ 'Ruby': {
212
+ 'Rails': ['Gemfile', 'config.ru', 'app/'],
213
+ }
214
+ }
215
+
216
+ if language not in framework_indicators:
217
+ return None
218
+
219
+ for framework, files in framework_indicators[language].items():
220
+ if all((self.project_root / f).exists() or
221
+ any(self.project_root.rglob(f)) for f in files):
222
+ return framework
223
+
224
+ return None
225
+
226
+ def _find_dependencies(self, language: str) -> List[str]:
227
+ """Find project dependencies."""
228
+ dep_files = {
229
+ 'Python': ['requirements.txt', 'pyproject.toml', 'setup.py', 'Pipfile'],
230
+ 'JavaScript': ['package.json'],
231
+ 'TypeScript': ['package.json'],
232
+ 'Go': ['go.mod'],
233
+ 'Ruby': ['Gemfile'],
234
+ 'PHP': ['composer.json'],
235
+ 'Rust': ['Cargo.toml'],
236
+ 'Java': ['pom.xml', 'build.gradle'],
237
+ }
238
+
239
+ dependencies = []
240
+
241
+ for dep_file in dep_files.get(language, []):
242
+ file_path = self.project_root / dep_file
243
+ if file_path.exists():
244
+ deps = self._parse_dependency_file(file_path, language)
245
+ dependencies.extend(deps)
246
+
247
+ return list(set(dependencies)) # Remove duplicates
248
+
249
+ def _parse_dependency_file(self, file_path: Path, language: str) -> List[str]:
250
+ """Parse a dependency file to extract package names."""
251
+ try:
252
+ content = file_path.read_text()
253
+ deps = []
254
+
255
+ if file_path.name == 'requirements.txt':
256
+ for line in content.splitlines():
257
+ line = line.strip()
258
+ if line and not line.startswith('#'):
259
+ # Extract package name (before ==, >=, etc.)
260
+ pkg = re.split(r'[=<>!]', line)[0].strip()
261
+ deps.append(pkg)
262
+
263
+ elif file_path.name == 'package.json':
264
+ import json
265
+ data = json.loads(content)
266
+ deps.extend(data.get('dependencies', {}).keys())
267
+ deps.extend(data.get('devDependencies', {}).keys())
268
+
269
+ elif file_path.name == 'pyproject.toml':
270
+ # Simple parsing - look for dependencies section
271
+ in_deps = False
272
+ for line in content.splitlines():
273
+ if 'dependencies' in line:
274
+ in_deps = True
275
+ elif in_deps and '=' in line:
276
+ pkg = line.split('=')[0].strip(' "\'')
277
+ if pkg and not pkg.startswith('['):
278
+ deps.append(pkg)
279
+ elif in_deps and line.strip().startswith('['):
280
+ in_deps = False
281
+
282
+ return deps
283
+
284
+ except Exception:
285
+ return []
286
+
287
+ def _get_installed_packages(self, language: str) -> Dict[str, str]:
288
+ """Get currently installed packages with versions."""
289
+ # This would ideally check the actual environment
290
+ # For now, return empty dict (can be enhanced later)
291
+ return {}
292
+
293
+ def _analyze_directory_structure(self) -> Dict[str, List[str]]:
294
+ """Analyze project directory structure."""
295
+ structure = defaultdict(list)
296
+
297
+ common_patterns = {
298
+ 'models': ['models/', 'model/', 'entities/', 'domain/'],
299
+ 'views': ['views/', 'templates/', 'pages/'],
300
+ 'controllers': ['controllers/', 'handlers/', 'routes/'],
301
+ 'services': ['services/', 'business/', 'logic/'],
302
+ 'utils': ['utils/', 'helpers/', 'common/'],
303
+ 'tests': ['tests/', 'test/', '__tests__/', 'spec/'],
304
+ 'config': ['config/', 'settings/', 'conf/'],
305
+ 'static': ['static/', 'public/', 'assets/'],
306
+ }
307
+
308
+ for purpose, patterns in common_patterns.items():
309
+ for pattern in patterns:
310
+ paths = list(self.project_root.glob(f"**/{pattern}"))
311
+ if paths:
312
+ structure[purpose].extend([str(p.relative_to(self.project_root)) for p in paths[:5]])
313
+
314
+ return dict(structure)
315
+
316
+ def _detect_naming_conventions(self, language: str) -> Dict[str, str]:
317
+ """Detect naming conventions used in the project."""
318
+ conventions = {}
319
+
320
+ # Sample files to detect patterns
321
+ sample_files = list(self.project_root.rglob('*.py' if language == 'Python' else '*'))[:50]
322
+
323
+ # Detect function naming
324
+ func_names = []
325
+ class_names = []
326
+
327
+ for file_path in sample_files:
328
+ if file_path.is_file() and not self._should_ignore(file_path):
329
+ try:
330
+ content = file_path.read_text()
331
+
332
+ # Extract function names
333
+ func_names.extend(re.findall(r'def\s+(\w+)\s*\(', content))
334
+ func_names.extend(re.findall(r'function\s+(\w+)\s*\(', content))
335
+
336
+ # Extract class names
337
+ class_names.extend(re.findall(r'class\s+(\w+)', content))
338
+
339
+ except Exception:
340
+ continue
341
+
342
+ # Analyze patterns
343
+ if func_names:
344
+ if all('_' in name or name.islower() for name in func_names[:10]):
345
+ conventions['functions'] = 'snake_case'
346
+ elif all(name[0].islower() and any(c.isupper() for c in name[1:]) for name in func_names[:10] if len(name) > 1):
347
+ conventions['functions'] = 'camelCase'
348
+
349
+ if class_names:
350
+ if all(name[0].isupper() for name in class_names[:10]):
351
+ conventions['classes'] = 'PascalCase'
352
+
353
+ return conventions
354
+
355
+ def _find_common_imports(self, language: str) -> List[str]:
356
+ """Find most commonly used imports."""
357
+ import_counts = defaultdict(int)
358
+
359
+ sample_files = list(self.project_root.rglob('*.py' if language == 'Python' else '*'))[:100]
360
+
361
+ for file_path in sample_files:
362
+ if file_path.is_file() and not self._should_ignore(file_path):
363
+ try:
364
+ content = file_path.read_text()
365
+
366
+ if language == 'Python':
367
+ imports = re.findall(r'(?:from|import)\s+([\w.]+)', content)
368
+ for imp in imports:
369
+ import_counts[imp.split('.')[0]] += 1
370
+
371
+ except Exception:
372
+ continue
373
+
374
+ # Return top 15 most common
375
+ sorted_imports = sorted(import_counts.items(), key=lambda x: x[1], reverse=True)
376
+ return [imp[0] for imp in sorted_imports[:15]]
377
+
378
+ def _find_config_files(self) -> List[str]:
379
+ """Find configuration files."""
380
+ config_patterns = [
381
+ 'config.*', 'settings.*', '.env*', '*.config.*',
382
+ 'docker-compose.yml', 'Dockerfile', 'Makefile'
383
+ ]
384
+
385
+ config_files = []
386
+ for pattern in config_patterns:
387
+ config_files.extend(str(p.relative_to(self.project_root))
388
+ for p in self.project_root.glob(pattern))
389
+
390
+ return config_files[:10]
391
+
392
+ def _extract_search_terms(self, ticket: Ticket) -> List[str]:
393
+ """Extract key search terms from ticket."""
394
+ text = f"{ticket.title} {ticket.description}"
395
+
396
+ # Extract important words (exclude common words)
397
+ stop_words = {'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by'}
398
+ words = re.findall(r'\b\w{4,}\b', text.lower())
399
+
400
+ return [w for w in words if w not in stop_words][:10]
401
+
402
+ def _find_similar_files(self, search_terms: List[str], language: str) -> List[Dict[str, Any]]:
403
+ """Find files with similar purpose."""
404
+ similar = []
405
+
406
+ ext = '.py' if language == 'Python' else '.*'
407
+ files = list(self.project_root.rglob(f'*{ext}'))[:200]
408
+
409
+ for file_path in files:
410
+ if file_path.is_file() and not self._should_ignore(file_path):
411
+ rel_path = str(file_path.relative_to(self.project_root))
412
+
413
+ # Check if filename or path contains search terms
414
+ matches = sum(1 for term in search_terms if term in rel_path.lower())
415
+
416
+ if matches > 0:
417
+ similar.append({
418
+ 'path': rel_path,
419
+ 'similarity': matches,
420
+ 'purpose': self._guess_file_purpose(file_path)
421
+ })
422
+
423
+ # Sort by similarity
424
+ similar.sort(key=lambda x: x['similarity'], reverse=True)
425
+ return similar[:5]
426
+
427
+ def _find_similar_functions(self, search_terms: List[str], language: str) -> List[Dict[str, Any]]:
428
+ """Find functions with similar names or purposes."""
429
+ functions = []
430
+
431
+ ext = '.py' if language == 'Python' else '.*'
432
+ files = list(self.project_root.rglob(f'*{ext}'))[:100]
433
+
434
+ for file_path in files:
435
+ if file_path.is_file() and not self._should_ignore(file_path):
436
+ try:
437
+ content = file_path.read_text()
438
+
439
+ # Extract function definitions
440
+ func_matches = re.findall(r'def\s+(\w+)\s*\((.*?)\):', content)
441
+
442
+ for func_name, params in func_matches:
443
+ if any(term in func_name.lower() for term in search_terms):
444
+ functions.append({
445
+ 'name': func_name,
446
+ 'file': str(file_path.relative_to(self.project_root)),
447
+ 'signature': f"{func_name}({params})"
448
+ })
449
+
450
+ except Exception:
451
+ continue
452
+
453
+ return functions[:10]
454
+
455
+ def _find_patterns(self, search_terms: List[str]) -> List[str]:
456
+ """Find code patterns related to search terms."""
457
+ # This could be enhanced with more sophisticated pattern detection
458
+ patterns = []
459
+
460
+ # Look for common patterns like decorators, base classes, etc.
461
+ # For now, return empty (can be enhanced)
462
+
463
+ return patterns
464
+
465
+ def _find_models(self, language: str, framework: Optional[str]) -> List[str]:
466
+ """Find model files."""
467
+ return self._find_files_by_pattern(['models/', 'model/', 'entities/'], language)
468
+
469
+ def _find_views(self, language: str, framework: Optional[str]) -> List[str]:
470
+ """Find view/template files."""
471
+ return self._find_files_by_pattern(['views/', 'templates/', 'pages/'], language)
472
+
473
+ def _find_controllers(self, language: str, framework: Optional[str]) -> List[str]:
474
+ """Find controller/handler files."""
475
+ return self._find_files_by_pattern(['controllers/', 'handlers/', 'routes/'], language)
476
+
477
+ def _find_tests(self, language: str) -> List[str]:
478
+ """Find test files."""
479
+ return self._find_files_by_pattern(['tests/', 'test/', '__tests__/'], language)
480
+
481
+ def _find_files_by_pattern(self, patterns: List[str], language: str) -> List[str]:
482
+ """Find files matching directory patterns."""
483
+ files = []
484
+
485
+ for pattern in patterns:
486
+ paths = list(self.project_root.glob(f"**/{pattern}**"))
487
+ files.extend(str(p.relative_to(self.project_root))
488
+ for p in paths if p.is_file())
489
+
490
+ return files[:10]
491
+
492
+ def _guess_file_purpose(self, file_path: Path) -> str:
493
+ """Guess the purpose of a file from its path and content."""
494
+ path_str = str(file_path).lower()
495
+
496
+ if 'model' in path_str:
497
+ return 'Data model'
498
+ elif 'view' in path_str or 'template' in path_str:
499
+ return 'View/Template'
500
+ elif 'controller' in path_str or 'handler' in path_str:
501
+ return 'Controller/Handler'
502
+ elif 'service' in path_str:
503
+ return 'Business logic'
504
+ elif 'util' in path_str or 'helper' in path_str:
505
+ return 'Utility functions'
506
+ elif 'test' in path_str:
507
+ return 'Tests'
508
+ else:
509
+ return 'General code'
510
+
511
+ def _should_ignore(self, path: Path) -> bool:
512
+ """Check if path should be ignored."""
513
+ ignore_patterns = [
514
+ '.git', '__pycache__', 'node_modules', '.venv', 'venv',
515
+ 'dist', 'build', '.pytest_cache', '.mypy_cache', '.ruff_cache',
516
+ '.tox', 'htmlcov', '.coverage', '*.pyc', '*.pyo', '*.egg-info'
517
+ ]
518
+
519
+ path_str = str(path)
520
+ return any(pattern in path_str for pattern in ignore_patterns)
521
+
522
+ def _enhance_with_ai(
523
+ self,
524
+ context: CodeContext,
525
+ ticket: Ticket,
526
+ ai_client: ClaudeClient
527
+ ) -> CodeContext:
528
+ """Use AI to enhance context with semantic understanding."""
529
+ # Could use AI to:
530
+ # 1. Identify most relevant files semantically
531
+ # 2. Suggest architectural patterns
532
+ # 3. Find non-obvious related code
533
+
534
+ # For now, return as-is (can be enhanced later)
535
+ return context