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.
- claude_dev_cli/__init__.py +1 -1
- claude_dev_cli/cli.py +309 -0
- claude_dev_cli/input_sources.py +231 -0
- claude_dev_cli/multi_file_handler.py +348 -0
- {claude_dev_cli-0.11.0.dist-info → claude_dev_cli-0.12.1.dist-info}/METADATA +72 -5
- {claude_dev_cli-0.11.0.dist-info → claude_dev_cli-0.12.1.dist-info}/RECORD +10 -8
- {claude_dev_cli-0.11.0.dist-info → claude_dev_cli-0.12.1.dist-info}/WHEEL +0 -0
- {claude_dev_cli-0.11.0.dist-info → claude_dev_cli-0.12.1.dist-info}/entry_points.txt +0 -0
- {claude_dev_cli-0.11.0.dist-info → claude_dev_cli-0.12.1.dist-info}/licenses/LICENSE +0 -0
- {claude_dev_cli-0.11.0.dist-info → claude_dev_cli-0.12.1.dist-info}/top_level.txt +0 -0
claude_dev_cli/__init__.py
CHANGED
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.
|
|
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
|
[](https://badge.fury.io/py/claude-dev-cli)
|
|
48
55
|
[](https://www.python.org/downloads/)
|
|
49
|
-
[](https://github.com/thinmanj/claude-dev-cli)
|
|
50
57
|
[](https://opensource.org/licenses/MIT)
|
|
51
58
|
[](https://github.com/thinmanj/homebrew-tap)
|
|
52
59
|
[](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.
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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=
|
|
2
|
-
claude_dev_cli/cli.py,sha256=
|
|
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.
|
|
22
|
-
claude_dev_cli-0.
|
|
23
|
-
claude_dev_cli-0.
|
|
24
|
-
claude_dev_cli-0.
|
|
25
|
-
claude_dev_cli-0.
|
|
26
|
-
claude_dev_cli-0.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|