claude-dev-cli 0.10.1__py3-none-any.whl → 0.12.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.
claude_dev_cli/cli.py CHANGED
@@ -806,41 +806,83 @@ def generate() -> None:
806
806
 
807
807
 
808
808
  @generate.command('tests')
809
- @click.argument('file_path', type=click.Path(exists=True))
810
- @click.option('-o', '--output', type=click.Path(), help='Output file path')
809
+ @click.argument('paths', nargs=-1, type=click.Path(exists=True))
810
+ @click.option('-o', '--output', type=click.Path(), help='Output file path (single file only)')
811
811
  @click.option('-a', '--api', help='API config to use')
812
812
  @click.option('-i', '--interactive', is_flag=True, help='Interactive refinement mode')
813
813
  @click.option('--auto-context', is_flag=True, help='Include dependencies and related files')
814
+ @click.option('--max-files', type=int, default=10, help='Maximum files to process (default: 10)')
814
815
  @click.pass_context
815
816
  def gen_tests(
816
817
  ctx: click.Context,
817
- file_path: str,
818
+ paths: tuple,
818
819
  output: Optional[str],
819
820
  api: Optional[str],
820
821
  interactive: bool,
821
- auto_context: bool
822
+ auto_context: bool,
823
+ max_files: int
822
824
  ) -> None:
823
- """Generate pytest tests for a Python file."""
825
+ """Generate pytest tests for Python files.
826
+
827
+ Can generate tests for multiple files or directories:
828
+
829
+ cdc generate tests file1.py file2.py
830
+ cdc generate tests src/
831
+ """
824
832
  console = ctx.obj['console']
833
+ from claude_dev_cli.path_utils import expand_paths
825
834
 
826
835
  try:
836
+ if not paths:
837
+ console.print("[yellow]No files specified. Provide file paths.[/yellow]")
838
+ return
839
+
840
+ files = expand_paths(list(paths), max_files=max_files)
841
+ if not files:
842
+ console.print("[yellow]No files found.[/yellow]")
843
+ return
844
+
845
+ # Output only works with single file
846
+ if output and len(files) > 1:
847
+ console.print("[yellow]Warning: --output only works with single file. Ignoring output option.[/yellow]")
848
+ output = None
849
+
850
+ if len(files) > 1:
851
+ console.print(f"\n[bold]Generating tests for {len(files)} file(s):[/bold]")
852
+ for f in files[:5]:
853
+ console.print(f" • {f}")
854
+ if len(files) > 5:
855
+ console.print(f" ... and {len(files) - 5} more")
856
+ console.print()
857
+
858
+ # Build combined prompt
859
+ files_content = ""
860
+ for file_path in files:
861
+ try:
862
+ with open(file_path, 'r') as f:
863
+ content = f.read()
864
+ files_content += f"\n\n## File: {file_path}\n\n```\n{content}\n```\n"
865
+ except Exception as e:
866
+ console.print(f"[yellow]Warning: Could not read {file_path}: {e}[/yellow]")
867
+
827
868
  if auto_context:
828
869
  from claude_dev_cli.context import ContextGatherer
829
870
 
830
871
  with console.status("[bold blue]Gathering context..."):
831
872
  gatherer = ContextGatherer()
832
- context = gatherer.gather_for_file(Path(file_path), include_git=False)
873
+ context = gatherer.gather_for_file(files[0], include_git=False)
833
874
  context_info = context.format_for_prompt()
834
875
 
835
876
  console.print("[dim]✓ Context gathered (dependencies, related files)[/dim]")
836
877
 
837
- # Use context-aware test generation
838
878
  client = ClaudeClient(api_config_name=api)
839
- enhanced_prompt = f"{context_info}\n\nPlease generate comprehensive pytest tests for the main file, including fixtures, edge cases, and proper mocking where needed."
840
- result = client.call(enhanced_prompt)
879
+ prompt = f"{context_info}\n\nFiles:{files_content}\n\nPlease generate comprehensive pytest tests for these files, including fixtures, edge cases, and proper mocking where needed."
841
880
  else:
842
- with console.status("[bold blue]Generating tests..."):
843
- result = generate_tests(file_path, api_config_name=api)
881
+ with console.status(f"[bold blue]Generating tests for {len(files)} file(s)..."):
882
+ client = ClaudeClient(api_config_name=api)
883
+ prompt = f"Files to test:{files_content}\n\nPlease generate comprehensive pytest tests for these files, including fixtures, edge cases, and proper mocking where needed."
884
+
885
+ result = client.call(prompt)
844
886
 
845
887
  if interactive:
846
888
  # Show initial result
@@ -892,41 +934,83 @@ def gen_tests(
892
934
 
893
935
 
894
936
  @generate.command('docs')
895
- @click.argument('file_path', type=click.Path(exists=True))
896
- @click.option('-o', '--output', type=click.Path(), help='Output file path')
937
+ @click.argument('paths', nargs=-1, type=click.Path(exists=True))
938
+ @click.option('-o', '--output', type=click.Path(), help='Output file path (single file only)')
897
939
  @click.option('-a', '--api', help='API config to use')
898
940
  @click.option('-i', '--interactive', is_flag=True, help='Interactive refinement mode')
899
941
  @click.option('--auto-context', is_flag=True, help='Include dependencies and related files')
942
+ @click.option('--max-files', type=int, default=10, help='Maximum files to process (default: 10)')
900
943
  @click.pass_context
901
944
  def gen_docs(
902
945
  ctx: click.Context,
903
- file_path: str,
946
+ paths: tuple,
904
947
  output: Optional[str],
905
948
  api: Optional[str],
906
949
  interactive: bool,
907
- auto_context: bool
950
+ auto_context: bool,
951
+ max_files: int
908
952
  ) -> None:
909
- """Generate documentation for a Python file."""
953
+ """Generate documentation for files.
954
+
955
+ Can generate docs for multiple files or directories:
956
+
957
+ cdc generate docs file1.py file2.py
958
+ cdc generate docs src/
959
+ """
910
960
  console = ctx.obj['console']
961
+ from claude_dev_cli.path_utils import expand_paths
911
962
 
912
963
  try:
964
+ if not paths:
965
+ console.print("[yellow]No files specified. Provide file paths.[/yellow]")
966
+ return
967
+
968
+ files = expand_paths(list(paths), max_files=max_files)
969
+ if not files:
970
+ console.print("[yellow]No files found.[/yellow]")
971
+ return
972
+
973
+ # Output only works with single file
974
+ if output and len(files) > 1:
975
+ console.print("[yellow]Warning: --output only works with single file. Ignoring output option.[/yellow]")
976
+ output = None
977
+
978
+ if len(files) > 1:
979
+ console.print(f"\n[bold]Generating docs for {len(files)} file(s):[/bold]")
980
+ for f in files[:5]:
981
+ console.print(f" • {f}")
982
+ if len(files) > 5:
983
+ console.print(f" ... and {len(files) - 5} more")
984
+ console.print()
985
+
986
+ # Build combined prompt
987
+ files_content = ""
988
+ for file_path in files:
989
+ try:
990
+ with open(file_path, 'r') as f:
991
+ content = f.read()
992
+ files_content += f"\n\n## File: {file_path}\n\n```\n{content}\n```\n"
993
+ except Exception as e:
994
+ console.print(f"[yellow]Warning: Could not read {file_path}: {e}[/yellow]")
995
+
913
996
  if auto_context:
914
997
  from claude_dev_cli.context import ContextGatherer
915
998
 
916
999
  with console.status("[bold blue]Gathering context..."):
917
1000
  gatherer = ContextGatherer()
918
- context = gatherer.gather_for_file(Path(file_path), include_git=False)
1001
+ context = gatherer.gather_for_file(files[0], include_git=False)
919
1002
  context_info = context.format_for_prompt()
920
1003
 
921
1004
  console.print("[dim]✓ Context gathered (dependencies, related files)[/dim]")
922
1005
 
923
- # Use context-aware documentation generation
924
1006
  client = ClaudeClient(api_config_name=api)
925
- enhanced_prompt = f"{context_info}\n\nPlease generate comprehensive documentation for the main file, including API reference, usage examples, and integration notes."
926
- result = client.call(enhanced_prompt)
1007
+ prompt = f"{context_info}\n\nFiles:{files_content}\n\nPlease generate comprehensive documentation for these files, including API reference, usage examples, and integration notes."
927
1008
  else:
928
- with console.status("[bold blue]Generating documentation..."):
929
- result = generate_docs(file_path, api_config_name=api)
1009
+ with console.status(f"[bold blue]Generating documentation for {len(files)} file(s)..."):
1010
+ client = ClaudeClient(api_config_name=api)
1011
+ prompt = f"Files to document:{files_content}\n\nPlease generate comprehensive documentation for these files, including API reference, usage examples, and integration notes."
1012
+
1013
+ result = client.call(prompt)
930
1014
 
931
1015
  if interactive:
932
1016
  console.print("\n[bold]Initial Documentation:[/bold]\n")
@@ -976,23 +1060,375 @@ def gen_docs(
976
1060
  sys.exit(1)
977
1061
 
978
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, model=model)
1161
+ result = client.call(prompt)
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, model=model)
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):
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, model=model)
1318
+ result = client.call(prompt)
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, model=model)
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):
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
+
979
1372
  @main.command('review')
980
- @click.argument('file_path', type=click.Path(exists=True))
1373
+ @click.argument('paths', nargs=-1, type=click.Path(exists=True))
981
1374
  @click.option('-a', '--api', help='API config to use')
982
1375
  @click.option('-i', '--interactive', is_flag=True, help='Interactive follow-up questions')
983
1376
  @click.option('--auto-context', is_flag=True, help='Automatically include git, dependencies, and related files')
1377
+ @click.option('--max-files', type=int, default=20, help='Maximum files to review (default: 20)')
984
1378
  @click.pass_context
985
1379
  def review(
986
1380
  ctx: click.Context,
987
- file_path: str,
1381
+ paths: tuple,
988
1382
  api: Optional[str],
989
1383
  interactive: bool,
990
- auto_context: bool
1384
+ auto_context: bool,
1385
+ max_files: int
991
1386
  ) -> None:
992
- """Review code for bugs and improvements."""
1387
+ """Review code for bugs and improvements.
1388
+
1389
+ Can review multiple files, directories, or auto-detect git changes:
1390
+
1391
+ cdc review file1.py file2.py # Multiple files
1392
+ cdc review src/ # Directory
1393
+ cdc review # Auto-detect git changes
1394
+ """
993
1395
  console = ctx.obj['console']
1396
+ from claude_dev_cli.path_utils import expand_paths, auto_detect_files
994
1397
 
995
1398
  try:
1399
+ # Determine files to review
1400
+ if paths:
1401
+ # Expand paths (handles directories, multiple files)
1402
+ files = expand_paths(list(paths), max_files=max_files)
1403
+ else:
1404
+ # Auto-detect files from git
1405
+ files = auto_detect_files()
1406
+ if files:
1407
+ console.print(f"[dim]Auto-detected {len(files)} file(s) from git changes[/dim]")
1408
+
1409
+ if not files:
1410
+ console.print("[yellow]No files to review. Specify files or make some changes.[/yellow]")
1411
+ return
1412
+
1413
+ # Show files being reviewed
1414
+ if len(files) > 1:
1415
+ console.print(f"\n[bold]Reviewing {len(files)} file(s):[/bold]")
1416
+ for f in files[:10]: # Show first 10
1417
+ console.print(f" • {f}")
1418
+ if len(files) > 10:
1419
+ console.print(f" ... and {len(files) - 10} more")
1420
+ console.print()
1421
+
1422
+ # Build combined prompt for multiple files
1423
+ files_content = ""
1424
+ for file_path in files:
1425
+ try:
1426
+ with open(file_path, 'r') as f:
1427
+ content = f.read()
1428
+ files_content += f"\n\n## File: {file_path}\n\n```\n{content}\n```\n"
1429
+ except Exception as e:
1430
+ console.print(f"[yellow]Warning: Could not read {file_path}: {e}[/yellow]")
1431
+
996
1432
  # Gather context if requested
997
1433
  context_info = ""
998
1434
  if auto_context:
@@ -1000,31 +1436,24 @@ def review(
1000
1436
 
1001
1437
  with console.status("[bold blue]Gathering context..."):
1002
1438
  gatherer = ContextGatherer()
1003
- context = gatherer.gather_for_review(Path(file_path))
1439
+ # Use first file for context gathering
1440
+ context = gatherer.gather_for_review(files[0])
1004
1441
  context_info = context.format_for_prompt()
1005
1442
 
1006
1443
  console.print("[dim]✓ Context gathered (git, dependencies, tests)[/dim]")
1007
1444
 
1008
- with console.status("[bold blue]Reviewing code..."):
1009
- # If we have context, prepend it to the file analysis
1445
+ with console.status(f"[bold blue]Reviewing {len(files)} file(s)..."):
1446
+ client = ClaudeClient(api_config_name=api)
1010
1447
  if context_info:
1011
- # Read file separately for context-aware review
1012
- result = code_review(file_path, api_config_name=api)
1013
- # The context module already includes the file, so we use it differently
1014
- client = ClaudeClient(api_config_name=api)
1015
- enhanced_prompt = f"{context_info}\n\nPlease review this code for bugs and improvements."
1016
- result = client.call(enhanced_prompt)
1448
+ prompt = f"{context_info}\n\nFiles to review:{files_content}\n\nPlease review this code for bugs and improvements."
1017
1449
  else:
1018
- result = code_review(file_path, api_config_name=api)
1450
+ prompt = f"Files to review:{files_content}\n\nPlease review this code for bugs, security issues, and improvements."
1451
+ result = client.call(prompt)
1019
1452
 
1020
1453
  md = Markdown(result)
1021
1454
  console.print(md)
1022
1455
 
1023
1456
  if interactive:
1024
- client = ClaudeClient(api_config_name=api)
1025
- with open(file_path, 'r') as f:
1026
- file_content = f.read()
1027
-
1028
1457
  console.print("\n[dim]Ask follow-up questions about the review, or 'exit' to quit[/dim]")
1029
1458
 
1030
1459
  while True:
@@ -1036,7 +1465,7 @@ def review(
1036
1465
  if not user_input:
1037
1466
  continue
1038
1467
 
1039
- follow_up_prompt = f"Code review:\n\n{result}\n\nOriginal code:\n\n{file_content}\n\nUser question: {user_input}"
1468
+ follow_up_prompt = f"Code review:\n\n{result}\n\nFiles:{files_content}\n\nUser question: {user_input}"
1040
1469
 
1041
1470
  console.print("\n[bold green]Claude:[/bold green] ", end='')
1042
1471
  for chunk in client.call_streaming(follow_up_prompt):
@@ -1106,49 +1535,95 @@ def debug(
1106
1535
 
1107
1536
 
1108
1537
  @main.command('refactor')
1109
- @click.argument('file_path', type=click.Path(exists=True))
1110
- @click.option('-o', '--output', type=click.Path(), help='Output file path')
1538
+ @click.argument('paths', nargs=-1, type=click.Path(exists=True))
1539
+ @click.option('-o', '--output', type=click.Path(), help='Output file path (single file only)')
1111
1540
  @click.option('-a', '--api', help='API config to use')
1112
1541
  @click.option('-i', '--interactive', is_flag=True, help='Interactive refinement mode')
1113
1542
  @click.option('--auto-context', is_flag=True, help='Automatically include git, dependencies, and related files')
1543
+ @click.option('--max-files', type=int, default=20, help='Maximum files to refactor (default: 20)')
1114
1544
  @click.pass_context
1115
1545
  def refactor(
1116
1546
  ctx: click.Context,
1117
- file_path: str,
1547
+ paths: tuple,
1118
1548
  output: Optional[str],
1119
1549
  api: Optional[str],
1120
1550
  interactive: bool,
1121
- auto_context: bool
1551
+ auto_context: bool,
1552
+ max_files: int
1122
1553
  ) -> None:
1123
- """Suggest refactoring improvements."""
1554
+ """Suggest refactoring improvements.
1555
+
1556
+ Can refactor multiple files, directories, or auto-detect git changes:
1557
+
1558
+ cdc refactor file1.py file2.py # Multiple files
1559
+ cdc refactor src/ # Directory
1560
+ cdc refactor # Auto-detect git changes
1561
+ """
1124
1562
  console = ctx.obj['console']
1563
+ from claude_dev_cli.path_utils import expand_paths, auto_detect_files
1125
1564
 
1126
1565
  try:
1566
+ # Determine files to refactor
1567
+ if paths:
1568
+ files = expand_paths(list(paths), max_files=max_files)
1569
+ else:
1570
+ files = auto_detect_files()
1571
+ if files:
1572
+ console.print(f"[dim]Auto-detected {len(files)} file(s) from git changes[/dim]")
1573
+
1574
+ if not files:
1575
+ console.print("[yellow]No files to refactor. Specify files or make some changes.[/yellow]")
1576
+ return
1577
+
1578
+ # Output only works with single file
1579
+ if output and len(files) > 1:
1580
+ console.print("[yellow]Warning: --output only works with single file. Ignoring output option.[/yellow]")
1581
+ output = None
1582
+
1583
+ # Show files being refactored
1584
+ if len(files) > 1:
1585
+ console.print(f"\n[bold]Refactoring {len(files)} file(s):[/bold]")
1586
+ for f in files[:10]:
1587
+ console.print(f" • {f}")
1588
+ if len(files) > 10:
1589
+ console.print(f" ... and {len(files) - 10} more")
1590
+ console.print()
1591
+
1592
+ # Build combined prompt
1593
+ files_content = ""
1594
+ for file_path in files:
1595
+ try:
1596
+ with open(file_path, 'r') as f:
1597
+ content = f.read()
1598
+ files_content += f"\n\n## File: {file_path}\n\n```\n{content}\n```\n"
1599
+ except Exception as e:
1600
+ console.print(f"[yellow]Warning: Could not read {file_path}: {e}[/yellow]")
1601
+
1127
1602
  # Gather context if requested
1128
1603
  if auto_context:
1129
1604
  from claude_dev_cli.context import ContextGatherer
1130
1605
 
1131
1606
  with console.status("[bold blue]Gathering context..."):
1132
1607
  gatherer = ContextGatherer()
1133
- context = gatherer.gather_for_file(Path(file_path))
1608
+ context = gatherer.gather_for_file(files[0])
1134
1609
  context_info = context.format_for_prompt()
1135
1610
 
1136
1611
  console.print("[dim]✓ Context gathered[/dim]")
1137
1612
 
1138
- # Use context-aware refactoring
1139
1613
  client = ClaudeClient(api_config_name=api)
1140
- enhanced_prompt = f"{context_info}\n\nPlease suggest refactoring improvements for the main file."
1141
- result = client.call(enhanced_prompt)
1614
+ prompt = f"{context_info}\n\nFiles:{files_content}\n\nPlease suggest refactoring improvements."
1142
1615
  else:
1143
- with console.status("[bold blue]Analyzing code..."):
1144
- result = refactor_code(file_path, api_config_name=api)
1616
+ with console.status(f"[bold blue]Analyzing {len(files)} file(s)..."):
1617
+ client = ClaudeClient(api_config_name=api)
1618
+ prompt = f"Files to refactor:{files_content}\n\nPlease suggest refactoring improvements focusing on code quality, maintainability, and performance."
1619
+
1620
+ result = client.call(prompt)
1145
1621
 
1146
1622
  if interactive:
1147
1623
  console.print("\n[bold]Initial Refactoring:[/bold]\n")
1148
1624
  md = Markdown(result)
1149
1625
  console.print(md)
1150
1626
 
1151
- client = ClaudeClient(api_config_name=api)
1152
1627
  conversation_context = [result]
1153
1628
 
1154
1629
  while True:
@@ -1233,6 +1708,95 @@ def git_commit(ctx: click.Context, api: Optional[str], auto_context: bool) -> No
1233
1708
  sys.exit(1)
1234
1709
 
1235
1710
 
1711
+ @git.command('review')
1712
+ @click.option('-a', '--api', help='API config to use')
1713
+ @click.option('--staged', is_flag=True, help='Review only staged changes')
1714
+ @click.option('--branch', help='Review changes in branch (e.g., main..HEAD)')
1715
+ @click.option('-i', '--interactive', is_flag=True, help='Interactive follow-up questions')
1716
+ @click.pass_context
1717
+ def git_review(
1718
+ ctx: click.Context,
1719
+ api: Optional[str],
1720
+ staged: bool,
1721
+ branch: Optional[str],
1722
+ interactive: bool
1723
+ ) -> None:
1724
+ """Review git changes.
1725
+
1726
+ Examples:
1727
+ cdc git review --staged # Review staged changes
1728
+ cdc git review # Review all changes
1729
+ cdc git review --branch main..HEAD # Review branch changes
1730
+ """
1731
+ console = ctx.obj['console']
1732
+ from claude_dev_cli.path_utils import get_git_changes
1733
+
1734
+ try:
1735
+ # Get changed files
1736
+ if branch:
1737
+ files = get_git_changes(commit_range=branch)
1738
+ scope = f"branch {branch}"
1739
+ elif staged:
1740
+ files = get_git_changes(staged_only=True)
1741
+ scope = "staged changes"
1742
+ else:
1743
+ files = get_git_changes(staged_only=False)
1744
+ scope = "all changes"
1745
+
1746
+ if not files:
1747
+ console.print(f"[yellow]No changes found in {scope}.[/yellow]")
1748
+ return
1749
+
1750
+ console.print(f"\n[bold]Reviewing {len(files)} changed file(s) from {scope}:[/bold]")
1751
+ for f in files[:10]:
1752
+ console.print(f" • {f}")
1753
+ if len(files) > 10:
1754
+ console.print(f" ... and {len(files) - 10} more")
1755
+ console.print()
1756
+
1757
+ # Build files content
1758
+ files_content = ""
1759
+ for file_path in files:
1760
+ try:
1761
+ with open(file_path, 'r') as f:
1762
+ content = f.read()
1763
+ files_content += f"\n\n## File: {file_path}\n\n```\n{content}\n```\n"
1764
+ except Exception as e:
1765
+ console.print(f"[yellow]Warning: Could not read {file_path}: {e}[/yellow]")
1766
+
1767
+ # Review
1768
+ with console.status(f"[bold blue]Reviewing {len(files)} file(s)..."):
1769
+ client = ClaudeClient(api_config_name=api)
1770
+ prompt = f"Changed files in {scope}:{files_content}\n\nPlease review these git changes for bugs, security issues, code quality, and potential improvements. Focus on what changed and why it might be problematic."
1771
+ result = client.call(prompt)
1772
+
1773
+ md = Markdown(result)
1774
+ console.print(md)
1775
+
1776
+ if interactive:
1777
+ console.print("\n[dim]Ask follow-up questions about the review, or 'exit' to quit[/dim]")
1778
+
1779
+ while True:
1780
+ user_input = console.input("\n[cyan]You:[/cyan] ").strip()
1781
+
1782
+ if user_input.lower() == 'exit':
1783
+ break
1784
+
1785
+ if not user_input:
1786
+ continue
1787
+
1788
+ follow_up_prompt = f"Code review:\n\n{result}\n\nChanged files:{files_content}\n\nUser question: {user_input}"
1789
+
1790
+ console.print("\n[bold green]Claude:[/bold green] ", end='')
1791
+ for chunk in client.call_streaming(follow_up_prompt):
1792
+ console.print(chunk, end='')
1793
+ console.print()
1794
+
1795
+ except Exception as e:
1796
+ console.print(f"[red]Error: {e}[/red]")
1797
+ sys.exit(1)
1798
+
1799
+
1236
1800
  @main.group()
1237
1801
  def context() -> None:
1238
1802
  """Context gathering tools and information."""