claude-dev-cli 0.11.0__py3-none-any.whl → 0.12.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.11.0"
12
+ __version__ = "0.12.1"
13
13
  __author__ = "Julio"
14
14
  __license__ = "MIT"
15
15
 
claude_dev_cli/cli.py CHANGED
@@ -1060,6 +1060,315 @@ def gen_docs(
1060
1060
  sys.exit(1)
1061
1061
 
1062
1062
 
1063
+ @generate.command('code')
1064
+ @click.option('--description', help='Inline code specification')
1065
+ @click.option('-f', '--file', 'spec_file', type=click.Path(exists=True), help='Read specification from file')
1066
+ @click.option('--pdf', type=click.Path(exists=True), help='Read specification from PDF')
1067
+ @click.option('--url', help='Fetch specification from URL')
1068
+ @click.option('-o', '--output', required=True, type=click.Path(), help='Output file path (required)')
1069
+ @click.option('--language', help='Target language (auto-detected from output extension)')
1070
+ @click.option('-m', '--model', help='Model profile to use')
1071
+ @click.option('-a', '--api', help='API config to use')
1072
+ @click.option('-i', '--interactive', is_flag=True, help='Interactive refinement mode')
1073
+ @click.option('--auto-context', is_flag=True, help='Include project context')
1074
+ @click.pass_context
1075
+ def gen_code(
1076
+ ctx: click.Context,
1077
+ description: Optional[str],
1078
+ spec_file: Optional[str],
1079
+ pdf: Optional[str],
1080
+ url: Optional[str],
1081
+ output: str,
1082
+ language: Optional[str],
1083
+ model: Optional[str],
1084
+ api: Optional[str],
1085
+ interactive: bool,
1086
+ auto_context: bool
1087
+ ) -> None:
1088
+ """Generate code from a specification.
1089
+
1090
+ Reads specification from text, file, PDF, or URL and generates complete code.
1091
+
1092
+ Examples:
1093
+ cdc generate code --description "REST API client" -o client.py
1094
+ cdc generate code --file spec.md -o implementation.go
1095
+ cdc generate code --pdf requirements.pdf -o app.js
1096
+ cdc generate code --url https://example.com/spec -o service.py
1097
+ """
1098
+ console = ctx.obj['console']
1099
+ from claude_dev_cli.input_sources import get_input_content
1100
+ from pathlib import Path
1101
+
1102
+ try:
1103
+ # Get specification content
1104
+ spec_content, source_desc = get_input_content(
1105
+ description=description,
1106
+ file_path=spec_file,
1107
+ pdf_path=pdf,
1108
+ url=url,
1109
+ console=console
1110
+ )
1111
+
1112
+ # Detect language from output extension if not specified
1113
+ if not language:
1114
+ ext = Path(output).suffix.lstrip('.')
1115
+ language_map = {
1116
+ 'py': 'Python',
1117
+ 'js': 'JavaScript',
1118
+ 'ts': 'TypeScript',
1119
+ 'go': 'Go',
1120
+ 'rs': 'Rust',
1121
+ 'java': 'Java',
1122
+ 'cpp': 'C++',
1123
+ 'c': 'C',
1124
+ 'cs': 'C#',
1125
+ 'rb': 'Ruby',
1126
+ 'php': 'PHP',
1127
+ 'swift': 'Swift',
1128
+ 'kt': 'Kotlin',
1129
+ }
1130
+ language = language_map.get(ext, ext.upper() if ext else None)
1131
+
1132
+ console.print(f"[cyan]Generating code from:[/cyan] {source_desc}")
1133
+ if language:
1134
+ console.print(f"[cyan]Target language:[/cyan] {language}")
1135
+ console.print(f"[cyan]Output:[/cyan] {output}\n")
1136
+
1137
+ # Build prompt
1138
+ prompt = f"Specification:\n\n{spec_content}\n\n"
1139
+ if language:
1140
+ prompt += f"Generate complete, production-ready {language} code that implements this specification. "
1141
+ else:
1142
+ prompt += "Generate complete, production-ready code that implements this specification. "
1143
+ prompt += "Include proper error handling, documentation, and best practices."
1144
+
1145
+ # Add context if requested
1146
+ if auto_context:
1147
+ from claude_dev_cli.context import ContextGatherer
1148
+
1149
+ with console.status("[bold blue]Gathering project context..."):
1150
+ gatherer = ContextGatherer()
1151
+ # Gather context from current directory
1152
+ context = gatherer.gather_for_file(".", include_git=True)
1153
+ context_info = context.format_for_prompt()
1154
+
1155
+ console.print("[dim]✓ Context gathered[/dim]")
1156
+ prompt = f"{context_info}\n\n{prompt}"
1157
+
1158
+ # Generate code
1159
+ with console.status(f"[bold blue]Generating code..."):
1160
+ client = ClaudeClient(api_config_name=api)
1161
+ result = client.call(prompt, model=model)
1162
+
1163
+ # Interactive refinement
1164
+ if interactive:
1165
+ console.print("\n[bold]Initial Code:[/bold]\n")
1166
+ console.print(result)
1167
+
1168
+ client = ClaudeClient(api_config_name=api)
1169
+ conversation_context = [result]
1170
+
1171
+ while True:
1172
+ console.print("\n[dim]Commands: 'save' to save and exit, 'exit' to discard, or ask for changes[/dim]")
1173
+ user_input = console.input("[cyan]You:[/cyan] ").strip()
1174
+
1175
+ if user_input.lower() == 'exit':
1176
+ console.print("[yellow]Discarded changes[/yellow]")
1177
+ return
1178
+
1179
+ if user_input.lower() == 'save':
1180
+ result = conversation_context[-1]
1181
+ break
1182
+
1183
+ if not user_input:
1184
+ continue
1185
+
1186
+ # Get refinement
1187
+ refinement_prompt = f"Previous code:\n\n{conversation_context[-1]}\n\nUser request: {user_input}\n\nProvide the updated code."
1188
+
1189
+ console.print("\n[bold green]Claude:[/bold green] ", end='')
1190
+ response_parts = []
1191
+ for chunk in client.call_streaming(refinement_prompt, model=model):
1192
+ console.print(chunk, end='')
1193
+ response_parts.append(chunk)
1194
+ console.print()
1195
+
1196
+ result = ''.join(response_parts)
1197
+ conversation_context.append(result)
1198
+
1199
+ # Save output
1200
+ Path(output).write_text(result)
1201
+ console.print(f"\n[green]✓[/green] Code saved to: {output}")
1202
+
1203
+ except Exception as e:
1204
+ console.print(f"[red]Error: {e}[/red]")
1205
+ sys.exit(1)
1206
+
1207
+
1208
+ @generate.command('feature')
1209
+ @click.argument('paths', nargs=-1, type=click.Path(exists=True))
1210
+ @click.option('--description', help='Inline feature specification')
1211
+ @click.option('-f', '--file', 'spec_file', type=click.Path(exists=True), help='Read specification from file')
1212
+ @click.option('--pdf', type=click.Path(exists=True), help='Read specification from PDF')
1213
+ @click.option('--url', help='Fetch specification from URL')
1214
+ @click.option('--max-files', type=int, default=10, help='Maximum files to modify (default: 10)')
1215
+ @click.option('-m', '--model', help='Model profile to use')
1216
+ @click.option('-a', '--api', help='API config to use')
1217
+ @click.option('-i', '--interactive', is_flag=True, help='Interactive refinement mode')
1218
+ @click.option('--auto-context', is_flag=True, help='Include project context')
1219
+ @click.option('--preview', is_flag=True, help='Preview changes without applying')
1220
+ @click.pass_context
1221
+ def gen_feature(
1222
+ ctx: click.Context,
1223
+ paths: tuple,
1224
+ description: Optional[str],
1225
+ spec_file: Optional[str],
1226
+ pdf: Optional[str],
1227
+ url: Optional[str],
1228
+ max_files: int,
1229
+ model: Optional[str],
1230
+ api: Optional[str],
1231
+ interactive: bool,
1232
+ auto_context: bool,
1233
+ preview: bool
1234
+ ) -> None:
1235
+ """Generate code to add a feature to existing project.
1236
+
1237
+ Analyzes existing code and generates changes to implement a feature.
1238
+
1239
+ Examples:
1240
+ cdc generate feature --description "Add authentication" src/
1241
+ cdc generate feature --file feature-spec.md
1242
+ cdc generate feature --pdf requirements.pdf --preview
1243
+ cdc generate feature --url https://example.com/spec src/
1244
+ """
1245
+ console = ctx.obj['console']
1246
+ from claude_dev_cli.input_sources import get_input_content
1247
+ from claude_dev_cli.path_utils import expand_paths, auto_detect_files
1248
+
1249
+ try:
1250
+ # Get feature specification
1251
+ spec_content, source_desc = get_input_content(
1252
+ description=description,
1253
+ file_path=spec_file,
1254
+ pdf_path=pdf,
1255
+ url=url,
1256
+ console=console
1257
+ )
1258
+
1259
+ # Determine files to analyze
1260
+ if paths:
1261
+ files = expand_paths(list(paths), max_files=max_files)
1262
+ else:
1263
+ files = auto_detect_files()
1264
+ if files:
1265
+ console.print(f"[dim]Auto-detected {len(files)} file(s) from project[/dim]")
1266
+
1267
+ if not files:
1268
+ console.print("[yellow]No files found. Specify paths or run in a project directory.[/yellow]")
1269
+ return
1270
+
1271
+ console.print(f"[cyan]Feature specification from:[/cyan] {source_desc}")
1272
+ console.print(f"[cyan]Analyzing:[/cyan] {len(files)} file(s)\n")
1273
+
1274
+ # Show files
1275
+ if len(files) > 1:
1276
+ console.print(f"[bold]Files to analyze:[/bold]")
1277
+ for f in files[:5]:
1278
+ console.print(f" • {f}")
1279
+ if len(files) > 5:
1280
+ console.print(f" ... and {len(files) - 5} more")
1281
+ console.print()
1282
+
1283
+ # Build codebase content
1284
+ codebase_content = ""
1285
+ for file_path in files:
1286
+ try:
1287
+ with open(file_path, 'r') as f:
1288
+ content = f.read()
1289
+ codebase_content += f"\n\n## File: {file_path}\n\n```\n{content}\n```\n"
1290
+ except Exception as e:
1291
+ console.print(f"[yellow]Warning: Could not read {file_path}: {e}[/yellow]")
1292
+
1293
+ # Build prompt
1294
+ prompt = f"Feature Specification:\n\n{spec_content}\n\n"
1295
+ prompt += f"Existing Codebase:{codebase_content}\n\n"
1296
+ prompt += "Analyze the existing code and provide:\n"
1297
+ prompt += "1. Implementation plan for the feature\n"
1298
+ prompt += "2. List of files to modify or create\n"
1299
+ prompt += "3. Complete code changes (diffs or new files)\n"
1300
+ prompt += "4. Any necessary setup or configuration changes\n\n"
1301
+ prompt += "Be specific and provide complete, working code."
1302
+
1303
+ # Add context if requested
1304
+ if auto_context:
1305
+ from claude_dev_cli.context import ContextGatherer
1306
+
1307
+ with console.status("[bold blue]Gathering project context..."):
1308
+ gatherer = ContextGatherer()
1309
+ context = gatherer.gather_for_file(files[0], include_git=True)
1310
+ context_info = context.format_for_prompt()
1311
+
1312
+ console.print("[dim]✓ Context gathered[/dim]")
1313
+ prompt = f"{context_info}\n\n{prompt}"
1314
+
1315
+ # Generate feature implementation
1316
+ with console.status(f"[bold blue]Analyzing codebase and generating feature implementation..."):
1317
+ client = ClaudeClient(api_config_name=api)
1318
+ result = client.call(prompt, model=model)
1319
+
1320
+ # Show result
1321
+ from rich.markdown import Markdown
1322
+ md = Markdown(result)
1323
+ console.print(md)
1324
+
1325
+ if preview:
1326
+ console.print("\n[yellow]Preview mode - no changes applied[/yellow]")
1327
+ console.print("[dim]Remove --preview flag to apply changes[/dim]")
1328
+ return
1329
+
1330
+ # Interactive refinement
1331
+ if interactive:
1332
+ client = ClaudeClient(api_config_name=api)
1333
+ conversation_context = [result]
1334
+
1335
+ while True:
1336
+ console.print("\n[dim]Ask for changes, 'apply' to confirm, or 'exit' to cancel[/dim]")
1337
+ user_input = console.input("[cyan]You:[/cyan] ").strip()
1338
+
1339
+ if user_input.lower() == 'exit':
1340
+ console.print("[yellow]Cancelled[/yellow]")
1341
+ return
1342
+
1343
+ if user_input.lower() == 'apply':
1344
+ console.print("[green]✓[/green] Implementation plan ready")
1345
+ console.print("[dim]Apply the changes manually from the output above[/dim]")
1346
+ return
1347
+
1348
+ if not user_input:
1349
+ continue
1350
+
1351
+ # Get refinement
1352
+ refinement_prompt = f"Previous implementation plan:\n\n{conversation_context[-1]}\n\nUser request: {user_input}\n\nProvide the updated implementation."
1353
+
1354
+ console.print("\n[bold green]Claude:[/bold green] ", end='')
1355
+ response_parts = []
1356
+ for chunk in client.call_streaming(refinement_prompt, model=model):
1357
+ console.print(chunk, end='')
1358
+ response_parts.append(chunk)
1359
+ console.print()
1360
+
1361
+ result = ''.join(response_parts)
1362
+ conversation_context.append(result)
1363
+ else:
1364
+ console.print("\n[green]✓[/green] Feature implementation generated")
1365
+ console.print("[dim]Apply the changes manually from the output above[/dim]")
1366
+
1367
+ except Exception as e:
1368
+ console.print(f"[red]Error: {e}[/red]")
1369
+ sys.exit(1)
1370
+
1371
+
1063
1372
  @main.command('review')
1064
1373
  @click.argument('paths', nargs=-1, type=click.Path(exists=True))
1065
1374
  @click.option('-a', '--api', help='API config to use')
@@ -0,0 +1,231 @@
1
+ """Input source handlers for reading specifications from various sources."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional, Tuple
5
+ from rich.console import Console
6
+
7
+
8
+ def read_text_input(text: str) -> str:
9
+ """Read text input directly."""
10
+ return text
11
+
12
+
13
+ def read_file_input(file_path: str) -> str:
14
+ """Read input from a file.
15
+
16
+ Args:
17
+ file_path: Path to the file
18
+
19
+ Returns:
20
+ File contents as string
21
+
22
+ Raises:
23
+ FileNotFoundError: If file doesn't exist
24
+ IOError: If file can't be read
25
+ """
26
+ path = Path(file_path)
27
+ if not path.exists():
28
+ raise FileNotFoundError(f"File not found: {file_path}")
29
+
30
+ return path.read_text(encoding='utf-8')
31
+
32
+
33
+ def read_pdf_input(pdf_path: str) -> str:
34
+ """Read text content from a PDF file.
35
+
36
+ Args:
37
+ pdf_path: Path to the PDF file
38
+
39
+ Returns:
40
+ Extracted text from PDF
41
+
42
+ Raises:
43
+ ImportError: If pypdf is not installed
44
+ FileNotFoundError: If PDF doesn't exist
45
+ Exception: If PDF can't be parsed
46
+ """
47
+ try:
48
+ from pypdf import PdfReader
49
+ except ImportError:
50
+ raise ImportError(
51
+ "PDF support requires pypdf. Install with: "
52
+ "pip install 'claude-dev-cli[generation]'"
53
+ )
54
+
55
+ path = Path(pdf_path)
56
+ if not path.exists():
57
+ raise FileNotFoundError(f"PDF not found: {pdf_path}")
58
+
59
+ try:
60
+ reader = PdfReader(path)
61
+ text_parts = []
62
+ for page in reader.pages:
63
+ text = page.extract_text()
64
+ if text:
65
+ text_parts.append(text)
66
+
67
+ if not text_parts:
68
+ raise ValueError(f"No text could be extracted from PDF: {pdf_path}")
69
+
70
+ return "\n\n".join(text_parts)
71
+ except Exception as e:
72
+ raise Exception(f"Failed to read PDF {pdf_path}: {str(e)}")
73
+
74
+
75
+ def read_url_input(url: str) -> str:
76
+ """Fetch and extract text content from a URL.
77
+
78
+ Args:
79
+ url: URL to fetch
80
+
81
+ Returns:
82
+ Extracted text content
83
+
84
+ Raises:
85
+ ImportError: If requests or beautifulsoup4 are not installed
86
+ Exception: If URL can't be fetched or parsed
87
+ """
88
+ try:
89
+ import requests
90
+ from bs4 import BeautifulSoup
91
+ except ImportError:
92
+ raise ImportError(
93
+ "URL support requires requests and beautifulsoup4. Install with: "
94
+ "pip install 'claude-dev-cli[generation]'"
95
+ )
96
+
97
+ # Validate URL format
98
+ if not url.startswith(('http://', 'https://')):
99
+ raise ValueError(f"Invalid URL format: {url}")
100
+
101
+ try:
102
+ # Fetch content
103
+ response = requests.get(url, timeout=30, headers={
104
+ 'User-Agent': 'claude-dev-cli/0.11.0'
105
+ })
106
+ response.raise_for_status()
107
+
108
+ # Determine content type
109
+ content_type = response.headers.get('Content-Type', '').lower()
110
+
111
+ if 'text/plain' in content_type:
112
+ # Plain text - return as-is
113
+ return response.text
114
+
115
+ elif 'text/html' in content_type or 'application/xhtml' in content_type:
116
+ # HTML - extract text
117
+ soup = BeautifulSoup(response.content, 'html.parser')
118
+
119
+ # Remove script and style elements
120
+ for script in soup(['script', 'style', 'nav', 'footer', 'header']):
121
+ script.decompose()
122
+
123
+ # Get text
124
+ text = soup.get_text(separator='\n', strip=True)
125
+
126
+ # Clean up extra whitespace
127
+ lines = [line.strip() for line in text.splitlines() if line.strip()]
128
+ return '\n'.join(lines)
129
+
130
+ elif 'application/json' in content_type:
131
+ # JSON - return formatted
132
+ import json
133
+ return json.dumps(response.json(), indent=2)
134
+
135
+ else:
136
+ # Unknown content type - try to decode as text
137
+ return response.text
138
+
139
+ except requests.RequestException as e:
140
+ raise Exception(f"Failed to fetch URL {url}: {str(e)}")
141
+ except Exception as e:
142
+ raise Exception(f"Failed to parse content from {url}: {str(e)}")
143
+
144
+
145
+ def get_input_content(
146
+ description: Optional[str] = None,
147
+ file_path: Optional[str] = None,
148
+ pdf_path: Optional[str] = None,
149
+ url: Optional[str] = None,
150
+ console: Optional[Console] = None
151
+ ) -> Tuple[str, str]:
152
+ """Get input content from one of the available sources.
153
+
154
+ Args:
155
+ description: Direct text description
156
+ file_path: Path to file
157
+ pdf_path: Path to PDF
158
+ url: URL to fetch
159
+ console: Rich console for output
160
+
161
+ Returns:
162
+ Tuple of (content, source_description)
163
+
164
+ Raises:
165
+ ValueError: If multiple sources or no sources provided
166
+ Various exceptions from individual read functions
167
+ """
168
+ if console is None:
169
+ console = Console()
170
+
171
+ # Count how many sources are provided
172
+ sources = [
173
+ ('description', description),
174
+ ('file', file_path),
175
+ ('pdf', pdf_path),
176
+ ('url', url)
177
+ ]
178
+ provided_sources = [(name, value) for name, value in sources if value]
179
+
180
+ if len(provided_sources) == 0:
181
+ raise ValueError(
182
+ "No input source provided. Use one of:\n"
183
+ " --description TEXT\n"
184
+ " -f/--file PATH\n"
185
+ " --pdf PATH\n"
186
+ " --url URL"
187
+ )
188
+
189
+ if len(provided_sources) > 1:
190
+ source_names = [name for name, _ in provided_sources]
191
+ raise ValueError(
192
+ f"Multiple input sources provided: {', '.join(source_names)}. "
193
+ f"Please use only one."
194
+ )
195
+
196
+ source_type, source_value = provided_sources[0]
197
+
198
+ # Read from the appropriate source
199
+ if source_type == 'description':
200
+ content = read_text_input(source_value)
201
+ source_desc = "text description"
202
+
203
+ elif source_type == 'file':
204
+ console.print(f"[cyan]Reading from file:[/cyan] {source_value}")
205
+ content = read_file_input(source_value)
206
+ source_desc = f"file: {source_value}"
207
+
208
+ elif source_type == 'pdf':
209
+ console.print(f"[cyan]Extracting text from PDF:[/cyan] {source_value}")
210
+ try:
211
+ content = read_pdf_input(source_value)
212
+ console.print(f"[green]✓[/green] Extracted {len(content)} characters from PDF")
213
+ source_desc = f"PDF: {source_value}"
214
+ except ImportError as e:
215
+ console.print(f"[red]Error:[/red] {str(e)}")
216
+ raise
217
+
218
+ elif source_type == 'url':
219
+ console.print(f"[cyan]Fetching content from URL:[/cyan] {source_value}")
220
+ try:
221
+ content = read_url_input(source_value)
222
+ console.print(f"[green]✓[/green] Fetched {len(content)} characters from URL")
223
+ source_desc = f"URL: {source_value}"
224
+ except ImportError as e:
225
+ console.print(f"[red]Error:[/red] {str(e)}")
226
+ raise
227
+
228
+ else:
229
+ raise ValueError(f"Unknown source type: {source_type}")
230
+
231
+ return content, source_desc
@@ -0,0 +1,348 @@
1
+ """Multi-file handler for parsing and applying AI-generated file changes."""
2
+
3
+ import re
4
+ import difflib
5
+ from pathlib import Path
6
+ from typing import List, Tuple, Optional, Literal
7
+ from dataclasses import dataclass
8
+ from rich.console import Console
9
+ from rich.tree import Tree
10
+ from rich.panel import Panel
11
+
12
+
13
+ @dataclass
14
+ class FileChange:
15
+ """Represents a single file change."""
16
+ path: str
17
+ content: str
18
+ change_type: Literal["create", "modify", "delete"]
19
+ original_content: Optional[str] = None
20
+
21
+ @property
22
+ def line_count(self) -> int:
23
+ """Count lines in content."""
24
+ return len(self.content.splitlines()) if self.content else 0
25
+
26
+ @property
27
+ def diff(self) -> Optional[str]:
28
+ """Generate unified diff for modifications."""
29
+ if self.change_type != "modify" or not self.original_content:
30
+ return None
31
+
32
+ original_lines = self.original_content.splitlines(keepends=True)
33
+ new_lines = self.content.splitlines(keepends=True)
34
+
35
+ diff_lines = list(difflib.unified_diff(
36
+ original_lines,
37
+ new_lines,
38
+ fromfile=f"a/{self.path}",
39
+ tofile=f"b/{self.path}",
40
+ lineterm=''
41
+ ))
42
+
43
+ return ''.join(diff_lines) if diff_lines else None
44
+
45
+
46
+ class MultiFileResponse:
47
+ """Parses and handles multi-file AI responses."""
48
+
49
+ def __init__(self):
50
+ self.files: List[FileChange] = []
51
+
52
+ def parse_response(self, text: str, base_path: Optional[Path] = None) -> None:
53
+ """Parse AI response to extract file changes.
54
+
55
+ Supports formats:
56
+ - ## File: path/to/file.ext
57
+ - ## Create: path/to/file.ext
58
+ - ## Modify: path/to/file.ext
59
+ - ## Delete: path/to/file.ext
60
+
61
+ Args:
62
+ text: AI response text
63
+ base_path: Base directory to check for existing files (for modifications)
64
+ """
65
+ self.files = []
66
+
67
+ # Pattern to match file markers and code blocks
68
+ # Matches: ## File: path or ## Create: path or ## Modify: path or ## Delete: path
69
+ file_pattern = r'^##\s+(File|Create|Modify|Delete):\s*(.+?)$'
70
+ code_block_pattern = r'```(\w+)?\n(.*?)```'
71
+
72
+ lines = text.split('\n')
73
+ i = 0
74
+
75
+ while i < len(lines):
76
+ line = lines[i].strip()
77
+ match = re.match(file_pattern, line, re.IGNORECASE)
78
+
79
+ if match:
80
+ action = match.group(1).lower()
81
+ file_path = match.group(2).strip()
82
+
83
+ # Map actions to change types
84
+ if action in ('file', 'create'):
85
+ change_type = 'create'
86
+ elif action == 'modify':
87
+ change_type = 'modify'
88
+ elif action == 'delete':
89
+ change_type = 'delete'
90
+ else:
91
+ change_type = 'create'
92
+
93
+ # For delete, no content needed
94
+ if change_type == 'delete':
95
+ self.files.append(FileChange(
96
+ path=file_path,
97
+ content='',
98
+ change_type='delete'
99
+ ))
100
+ i += 1
101
+ continue
102
+
103
+ # Extract code block following the file marker
104
+ remaining_text = '\n'.join(lines[i+1:])
105
+ code_match = re.search(code_block_pattern, remaining_text, re.DOTALL)
106
+
107
+ if code_match:
108
+ content = code_match.group(2).strip()
109
+
110
+ # Check if file exists for modifications
111
+ original_content = None
112
+ if change_type == 'modify' and base_path:
113
+ file_full_path = base_path / file_path
114
+ if file_full_path.exists():
115
+ original_content = file_full_path.read_text()
116
+ change_type = 'modify'
117
+ else:
118
+ # File doesn't exist, treat as create
119
+ change_type = 'create'
120
+
121
+ self.files.append(FileChange(
122
+ path=file_path,
123
+ content=content,
124
+ change_type=change_type,
125
+ original_content=original_content
126
+ ))
127
+
128
+ # Skip past the code block
129
+ lines_used = code_match.group(0).count('\n')
130
+ i += lines_used + 1
131
+ else:
132
+ # No code block found, skip this line
133
+ i += 1
134
+ else:
135
+ i += 1
136
+
137
+ def validate_paths(self, base_path: Path) -> List[str]:
138
+ """Validate file paths for security.
139
+
140
+ Returns list of validation errors, empty if all valid.
141
+ """
142
+ errors = []
143
+ base_path = base_path.resolve()
144
+
145
+ for file_change in self.files:
146
+ # Check for absolute paths
147
+ if Path(file_change.path).is_absolute():
148
+ errors.append(f"Absolute path not allowed: {file_change.path}")
149
+ continue
150
+
151
+ # Resolve the path
152
+ try:
153
+ full_path = (base_path / file_change.path).resolve()
154
+
155
+ # Check if resolved path is within base_path
156
+ if not str(full_path).startswith(str(base_path)):
157
+ errors.append(f"Path traversal detected: {file_change.path}")
158
+
159
+ except Exception as e:
160
+ errors.append(f"Invalid path {file_change.path}: {str(e)}")
161
+
162
+ return errors
163
+
164
+ def build_tree(self, base_path: Path) -> str:
165
+ """Generate visual directory tree.
166
+
167
+ Returns formatted tree string with colors and status indicators.
168
+ """
169
+ if not self.files:
170
+ return "No files"
171
+
172
+ # Build tree structure
173
+ tree = Tree(f"[bold cyan]{base_path.name or base_path}/[/bold cyan]")
174
+
175
+ # Group files by directory
176
+ dirs = {}
177
+ for file_change in self.files:
178
+ parts = Path(file_change.path).parts
179
+ current = dirs
180
+
181
+ for i, part in enumerate(parts[:-1]):
182
+ if part not in current:
183
+ current[part] = {}
184
+ current = current[part]
185
+
186
+ # Store file info at leaf
187
+ filename = parts[-1]
188
+ current[filename] = file_change
189
+
190
+ def add_to_tree(node, items):
191
+ """Recursively add items to tree."""
192
+ for name, value in sorted(items.items()):
193
+ if isinstance(value, dict):
194
+ # Directory
195
+ branch = node.add(f"[bold blue]{name}/[/bold blue]")
196
+ add_to_tree(branch, value)
197
+ elif isinstance(value, FileChange):
198
+ # File
199
+ if value.change_type == 'create':
200
+ status = "[green](new)[/green]"
201
+ elif value.change_type == 'modify':
202
+ status = "[yellow](modified)[/yellow]"
203
+ elif value.change_type == 'delete':
204
+ status = "[red](deleted)[/red]"
205
+ else:
206
+ status = ""
207
+
208
+ lines = f"{value.line_count} lines" if value.line_count > 0 else ""
209
+ node.add(f"{name} {status} [dim]{lines}[/dim]")
210
+
211
+ add_to_tree(tree, dirs)
212
+
213
+ return tree
214
+
215
+ def preview(self, console: Console, base_path: Path) -> None:
216
+ """Show formatted preview with tree and summary."""
217
+ if not self.files:
218
+ console.print("[yellow]No files to change[/yellow]")
219
+ return
220
+
221
+ # Show tree
222
+ console.print("\n[bold]File Structure:[/bold]")
223
+ console.print(self.build_tree(base_path))
224
+
225
+ # Summary
226
+ creates = sum(1 for f in self.files if f.change_type == 'create')
227
+ modifies = sum(1 for f in self.files if f.change_type == 'modify')
228
+ deletes = sum(1 for f in self.files if f.change_type == 'delete')
229
+ total_lines = sum(f.line_count for f in self.files)
230
+
231
+ summary_parts = []
232
+ if creates:
233
+ summary_parts.append(f"[green]{creates} created[/green]")
234
+ if modifies:
235
+ summary_parts.append(f"[yellow]{modifies} modified[/yellow]")
236
+ if deletes:
237
+ summary_parts.append(f"[red]{deletes} deleted[/red]")
238
+
239
+ summary = ", ".join(summary_parts)
240
+ console.print(f"\n[bold]Summary:[/bold] {summary} ({total_lines} total lines)\n")
241
+
242
+ def write_all(self, base_path: Path, dry_run: bool = False, console: Optional[Console] = None) -> None:
243
+ """Write all file changes to disk.
244
+
245
+ Args:
246
+ base_path: Base directory for file operations
247
+ dry_run: If True, don't actually write files
248
+ console: Rich console for output
249
+ """
250
+ if console is None:
251
+ console = Console()
252
+
253
+ base_path = base_path.resolve()
254
+ base_path.mkdir(parents=True, exist_ok=True)
255
+
256
+ for file_change in self.files:
257
+ full_path = base_path / file_change.path
258
+
259
+ if dry_run:
260
+ if file_change.change_type == 'create':
261
+ console.print(f"[dim]Would create: {file_change.path}[/dim]")
262
+ elif file_change.change_type == 'modify':
263
+ console.print(f"[dim]Would modify: {file_change.path}[/dim]")
264
+ elif file_change.change_type == 'delete':
265
+ console.print(f"[dim]Would delete: {file_change.path}[/dim]")
266
+ continue
267
+
268
+ # Actual file operations
269
+ if file_change.change_type == 'delete':
270
+ if full_path.exists():
271
+ full_path.unlink()
272
+ console.print(f"[red]✗[/red] Deleted: {file_change.path}")
273
+ else:
274
+ # Create parent directories
275
+ full_path.parent.mkdir(parents=True, exist_ok=True)
276
+
277
+ # Write file
278
+ full_path.write_text(file_change.content)
279
+
280
+ if file_change.change_type == 'create':
281
+ console.print(f"[green]✓[/green] Created: {file_change.path}")
282
+ elif file_change.change_type == 'modify':
283
+ console.print(f"[yellow]✓[/yellow] Modified: {file_change.path}")
284
+
285
+ def confirm(self, console: Console) -> bool:
286
+ """Interactive confirmation prompt.
287
+
288
+ Returns True if user confirms, False otherwise.
289
+ """
290
+ if not self.files:
291
+ return False
292
+
293
+ while True:
294
+ response = console.input("\n[cyan]Continue?[/cyan] [dim](Y/n/preview/help)[/dim] ").strip().lower()
295
+
296
+ if response in ('y', 'yes', ''):
297
+ return True
298
+ elif response in ('n', 'no'):
299
+ return False
300
+ elif response == 'preview':
301
+ # Show individual file contents
302
+ for i, file_change in enumerate(self.files, 1):
303
+ console.print(f"\n[bold]File {i}/{len(self.files)}:[/bold] {file_change.path}")
304
+
305
+ if file_change.change_type == 'delete':
306
+ console.print("[red]This file will be deleted[/red]")
307
+ elif file_change.change_type == 'modify' and file_change.diff:
308
+ console.print("[yellow]Diff:[/yellow]")
309
+ console.print(Panel(file_change.diff, border_style="yellow"))
310
+ else:
311
+ preview = file_change.content[:500]
312
+ if len(file_change.content) > 500:
313
+ preview += "\n... (truncated)"
314
+ console.print(Panel(preview, border_style="green"))
315
+ elif response == 'help':
316
+ console.print("""
317
+ [bold]Options:[/bold]
318
+ y, yes - Proceed with changes
319
+ n, no - Cancel
320
+ preview - Show file contents/diffs
321
+ help - Show this help
322
+ """)
323
+ else:
324
+ console.print("[red]Invalid response. Type 'help' for options.[/red]")
325
+
326
+
327
+ def extract_code_blocks(text: str) -> List[Tuple[str, str, str]]:
328
+ """Extract code blocks from markdown text.
329
+
330
+ Returns list of (file_marker, language, code) tuples.
331
+ """
332
+ pattern = r'^##\s+(File|Create|Modify|Delete):\s*(.+?)$.*?```(\w+)?\n(.*?)```'
333
+ matches = re.findall(pattern, text, re.MULTILINE | re.DOTALL)
334
+
335
+ results = []
336
+ for match in matches:
337
+ action = match[0]
338
+ path = match[1].strip()
339
+ language = match[2] if match[2] else ''
340
+ code = match[3].strip()
341
+ results.append((f"{action}: {path}", language, code))
342
+
343
+ return results
344
+
345
+
346
+ def count_lines(content: str) -> int:
347
+ """Count non-empty lines in content."""
348
+ return len([line for line in content.splitlines() if line.strip()])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-dev-cli
3
- Version: 0.11.0
3
+ Version: 0.12.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
@@ -33,6 +33,10 @@ Provides-Extra: toon
33
33
  Requires-Dist: toon-format>=0.1.0; extra == "toon"
34
34
  Provides-Extra: plugins
35
35
  Requires-Dist: pygments>=2.0.0; extra == "plugins"
36
+ Provides-Extra: generation
37
+ Requires-Dist: pypdf>=3.0.0; extra == "generation"
38
+ Requires-Dist: requests>=2.28.0; extra == "generation"
39
+ Requires-Dist: beautifulsoup4>=4.0.0; extra == "generation"
36
40
  Provides-Extra: dev
37
41
  Requires-Dist: pytest>=7.0.0; extra == "dev"
38
42
  Requires-Dist: black>=23.0.0; extra == "dev"
@@ -40,13 +44,16 @@ Requires-Dist: ruff>=0.1.0; extra == "dev"
40
44
  Requires-Dist: mypy>=1.0.0; extra == "dev"
41
45
  Requires-Dist: toon-format>=0.1.0; extra == "dev"
42
46
  Requires-Dist: pygments>=2.0.0; extra == "dev"
47
+ Requires-Dist: pypdf>=3.0.0; extra == "dev"
48
+ Requires-Dist: requests>=2.28.0; extra == "dev"
49
+ Requires-Dist: beautifulsoup4>=4.0.0; extra == "dev"
43
50
  Dynamic: license-file
44
51
 
45
52
  # Claude Dev CLI
46
53
 
47
54
  [![PyPI version](https://badge.fury.io/py/claude-dev-cli.svg)](https://badge.fury.io/py/claude-dev-cli)
48
55
  [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
49
- [![Tests](https://img.shields.io/badge/tests-285%20passing-brightgreen.svg)](https://github.com/thinmanj/claude-dev-cli)
56
+ [![Tests](https://img.shields.io/badge/tests-303%20passing-brightgreen.svg)](https://github.com/thinmanj/claude-dev-cli)
50
57
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
51
58
  [![Homebrew](https://img.shields.io/badge/homebrew-available-orange.svg)](https://github.com/thinmanj/homebrew-tap)
52
59
  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
@@ -72,6 +79,26 @@ A powerful command-line tool for developers using Claude AI with multi-API routi
72
79
  - `smart`: Claude Sonnet 4 ($3.00/$15.00 per Mtok) - default
73
80
  - `powerful`: Claude Opus 4 ($15.00/$75.00 per Mtok)
74
81
 
82
+ ### 🚀 Code Generation (v0.12.0+)
83
+ - **Generate Code from Specs**: Create new code from descriptions, files, PDFs, or URLs
84
+ - `cdc generate code --description "REST API client" -o client.py`
85
+ - Multiple input sources: text, files (.md, .txt), PDFs, URLs
86
+ - Auto-detects target language from file extension
87
+ - Interactive refinement mode
88
+ - **Add Features to Projects**: Analyze existing code and generate implementation plans
89
+ - `cdc generate feature --description "Add authentication" src/`
90
+ - Multi-file analysis and modification
91
+ - Preview mode to review changes before applying
92
+ - Supports same input sources as code generation
93
+ - **Multiple Input Sources**:
94
+ - `--description TEXT`: Inline specification
95
+ - `-f/--file PATH`: Read from file
96
+ - `--pdf PATH`: Extract from PDF
97
+ - `--url URL`: Fetch from URL
98
+ - **Optional Dependencies**: Install with `pip install 'claude-dev-cli[generation]'`
99
+ - Enables PDF and URL support
100
+ - Graceful fallback if not installed
101
+
75
102
  ### 📁 Multi-File Support (v0.11.0+)
76
103
  - **Batch Processing**: Review, refactor, test, or document multiple files at once
77
104
  - **Directory Support**: Process all code files in a directory with `--max-files` limit
@@ -149,8 +176,14 @@ brew install thinmanj/tap/claude-dev-cli
149
176
  # Basic installation
150
177
  pip install claude-dev-cli
151
178
 
179
+ # With code generation support (PDF & URL input)
180
+ pip install claude-dev-cli[generation]
181
+
152
182
  # With TOON support (30-60% token reduction)
153
183
  pip install claude-dev-cli[toon]
184
+
185
+ # With all optional features
186
+ pip install claude-dev-cli[generation,toon]
154
187
  ```
155
188
 
156
189
  ### Via pipx (Recommended for CLI tools)
@@ -159,8 +192,14 @@ pip install claude-dev-cli[toon]
159
192
  # Isolated installation
160
193
  pipx install claude-dev-cli
161
194
 
195
+ # With code generation support
196
+ pipx install claude-dev-cli[generation]
197
+
162
198
  # With TOON support
163
199
  pipx install claude-dev-cli[toon]
200
+
201
+ # With all optional features
202
+ pipx install claude-dev-cli[generation,toon]
164
203
  ```
165
204
 
166
205
  ## Quick Start
@@ -242,7 +281,35 @@ cdc review -m powerful complex_file.py # More thorough review
242
281
  cdc generate tests -m smart mymodule.py # Balanced approach
243
282
  ```
244
283
 
245
- ### 3. Developer Commands
284
+ ### 3. Code Generation Commands (NEW in v0.12.0)
285
+
286
+ ```bash
287
+ # Generate code from specification
288
+ cdc generate code --description "REST API client for weather data" -o client.py
289
+ cdc generate code --file spec.md -o implementation.go
290
+ cdc generate code --pdf requirements.pdf -o app.js
291
+ cdc generate code --url https://example.com/api-spec -o service.py
292
+
293
+ # Generate code with interactive refinement
294
+ cdc generate code --description "Database ORM" -o orm.py --interactive
295
+
296
+ # Generate code with project context
297
+ cdc generate code --file spec.md -o service.py --auto-context
298
+
299
+ # Add features to existing project
300
+ cdc generate feature --description "Add user authentication with JWT" src/
301
+ cdc generate feature --file feature-spec.md
302
+ cdc generate feature --pdf product-requirements.pdf --preview
303
+ cdc generate feature --url https://example.com/feature-spec
304
+
305
+ # Preview feature changes before applying
306
+ cdc generate feature --description "Add caching layer" src/ --preview
307
+
308
+ # Interactive feature implementation
309
+ cdc generate feature --description "Add logging" src/ --interactive
310
+ ```
311
+
312
+ ### 4. Developer Commands
246
313
 
247
314
  ```bash
248
315
  # Generate tests (single file)
@@ -316,7 +383,7 @@ git add .
316
383
  cdc git commit --auto-context
317
384
  ```
318
385
 
319
- ### 4. Context-Aware Operations (v0.8.0+)
386
+ ### 5. Context-Aware Operations (v0.8.0+)
320
387
 
321
388
  ```bash
322
389
  # Auto-context includes: git info, dependencies, related files
@@ -345,7 +412,7 @@ cdc refactor app.py --auto-context
345
412
  # Automatically includes imported modules and dependencies
346
413
  ```
347
414
 
348
- ### 5. Custom Templates
415
+ ### 6. Custom Templates
349
416
 
350
417
  ```bash
351
418
  # List all templates (built-in and user)
@@ -1,10 +1,12 @@
1
- claude_dev_cli/__init__.py,sha256=XSFQv1m1nRRX-T9POeBmjHrA0pylgusfmwtA92XCEzo,470
2
- claude_dev_cli/cli.py,sha256=RkoR4ocGUfP5A37yl_xObdNdd0QKeyqvEf2YOq1CL0U,79000
1
+ claude_dev_cli/__init__.py,sha256=m4e1TLJ_BCtWjtXr5lJcRupHDnyE30CEI2yqfFmbcWo,470
2
+ claude_dev_cli/cli.py,sha256=GDURdznUC9Dy_B9x8MS-9J3JrMbwiVOofSWHalmsRaU,91797
3
3
  claude_dev_cli/commands.py,sha256=RKGx2rv56PM6eErvA2uoQ20hY8babuI5jav8nCUyUOk,3964
4
4
  claude_dev_cli/config.py,sha256=ZnPvzwlXsoY9YhqTl4S__fwY1MzJXKIaYK0nIIelNXk,19978
5
5
  claude_dev_cli/context.py,sha256=1TlLzpREFZDEIuU7RAtlkjxARKWZpnxHHvK283sUAZE,26714
6
6
  claude_dev_cli/core.py,sha256=4tKBgPQzvhM-jtlHaIy2K54vc2yIb4ycNDPLpoIoqN0,6621
7
7
  claude_dev_cli/history.py,sha256=26EjNW68JuFQJhUp1j8UdB19S-eYz3eqevkpCOATwP0,10510
8
+ claude_dev_cli/input_sources.py,sha256=pFX5pU8uAUW_iujYdV3z1c_6F0KbKTWMNG0ChvKbxC8,7115
9
+ claude_dev_cli/multi_file_handler.py,sha256=3Rgy9NetKvd4tFhGyeY-44CYApnNShKAEErWOOARrJw,13331
8
10
  claude_dev_cli/path_utils.py,sha256=FFwweSkXe9OiG2Dej_UDKcY8-ZCYjL89ow6c7LZGe80,5564
9
11
  claude_dev_cli/secure_storage.py,sha256=KcZuQMLTbQpMAi2Cyh-_JkNcK9vHzAITOgjTcM9sr98,8161
10
12
  claude_dev_cli/template_manager.py,sha256=wtcrNuxFoJLJIPmIxUzrPKrE8kUvdqEd53EnG3jARhg,9277
@@ -18,9 +20,9 @@ claude_dev_cli/plugins/base.py,sha256=H4HQet1I-a3WLCfE9F06Lp8NuFvVoIlou7sIgyJFK-
18
20
  claude_dev_cli/plugins/diff_editor/__init__.py,sha256=gqR5S2TyIVuq-sK107fegsutQ7Z-sgAIEbtc71FhXIM,101
19
21
  claude_dev_cli/plugins/diff_editor/plugin.py,sha256=M1bUoqpasD3ZNQo36Fu_8g92uySPZyG_ujMbj5UplsU,3073
20
22
  claude_dev_cli/plugins/diff_editor/viewer.py,sha256=1IOXIKw_01ppJx5C1dQt9Kr6U1TdAHT8_iUT5r_q0NM,17169
21
- claude_dev_cli-0.11.0.dist-info/licenses/LICENSE,sha256=DGueuJwMJtMwgLO5mWlS0TaeBrFwQuNpNZ22PU9J2bw,1062
22
- claude_dev_cli-0.11.0.dist-info/METADATA,sha256=5fvuAb55hqZwnB_j-wzQNFv6ZXZbdnvEbAzt9pvmxtM,21330
23
- claude_dev_cli-0.11.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
24
- claude_dev_cli-0.11.0.dist-info/entry_points.txt,sha256=zymgUIIVpFTARkFmxAuW2A4BQsNITh_L0uU-XunytHg,85
25
- claude_dev_cli-0.11.0.dist-info/top_level.txt,sha256=m7MF6LOIuTe41IT5Fgt0lc-DK1EgM4gUU_IZwWxK0pg,15
26
- claude_dev_cli-0.11.0.dist-info/RECORD,,
23
+ claude_dev_cli-0.12.1.dist-info/licenses/LICENSE,sha256=DGueuJwMJtMwgLO5mWlS0TaeBrFwQuNpNZ22PU9J2bw,1062
24
+ claude_dev_cli-0.12.1.dist-info/METADATA,sha256=HGhP9cVZEsXsOH9hhtZXRhjfqWrQvsdlxUcWAvzEZgU,24018
25
+ claude_dev_cli-0.12.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
26
+ claude_dev_cli-0.12.1.dist-info/entry_points.txt,sha256=zymgUIIVpFTARkFmxAuW2A4BQsNITh_L0uU-XunytHg,85
27
+ claude_dev_cli-0.12.1.dist-info/top_level.txt,sha256=m7MF6LOIuTe41IT5Fgt0lc-DK1EgM4gUU_IZwWxK0pg,15
28
+ claude_dev_cli-0.12.1.dist-info/RECORD,,